From 76c203aae625cc15824a8e33899ce54a455fa567 Mon Sep 17 00:00:00 2001 From: hallidayo <22655069+Hallidayo@users.noreply.github.com> Date: Tue, 26 Dec 2023 23:50:40 +0000 Subject: [PATCH] feat: dateformat and timezone customization (#506) --- .../app/(marketing)/singleplayer/client.tsx | 9 +- .../create-single-player-document.action.ts | 3 +- .../src/pages/api/stripe/webhook/index.ts | 5 +- .../app/(dashboard)/admin/users/[id]/page.tsx | 4 +- .../documents/[id]/edit-document.tsx | 18 +-- .../(dashboard)/documents/upload-document.tsx | 1 + .../app/(signing)/sign/[token]/date-field.tsx | 36 ++++- .../(signing)/sign/[token]/email-field.tsx | 6 +- .../src/app/(signing)/sign/[token]/form.tsx | 9 +- .../app/(signing)/sign/[token]/name-field.tsx | 6 +- .../src/app/(signing)/sign/[token]/page.tsx | 13 +- .../sign/[token]/signature-field.tsx | 6 +- .../sign/[token]/signing-field-container.tsx | 24 +++- .../components/formatter/document-status.tsx | 4 +- .../src/components/formatter/locale-date.tsx | 6 +- .../forms/edit-document/add-subject.action.ts | 10 +- package-lock.json | 6 + packages/email/templates/document-invite.tsx | 6 +- packages/lib/client-only/recipient-type.ts | 3 +- packages/lib/constants/date-formats.ts | 71 ++++++++++ packages/lib/constants/time-zones.ts | 44 ++++++ packages/lib/next-auth/auth-options.ts | 5 +- packages/lib/package.json | 1 + .../document-meta/upsert-document-meta.ts | 8 ++ .../document/duplicate-document-by-id.ts | 2 + .../get-document-meta-by-document-id.ts | 13 ++ .../lib/server-only/document/get-stats.ts | 3 +- .../server-only/document/update-document.ts | 2 +- .../field/sign-field-with-token.ts | 13 +- packages/lib/utils/recipient-formatter.ts | 2 +- .../migration.sql | 3 + packages/prisma/schema.prisma | 2 + packages/prisma/types/document-with-data.ts | 2 +- .../prisma/types/document-with-recipient.ts | 2 +- packages/prisma/types/field-with-signature.ts | 2 +- packages/trpc/server/trpc.ts | 2 +- packages/ui/lib/utils.ts | 3 +- packages/ui/primitives/combobox.tsx | 70 +++++----- packages/ui/primitives/command.tsx | 2 +- .../primitives/document-flow/add-fields.tsx | 8 +- .../document-flow/add-signature.tsx | 12 +- .../primitives/document-flow/add-signers.tsx | 8 +- .../primitives/document-flow/add-subject.tsx | 127 +++++++++++++++--- .../document-flow/add-subject.types.ts | 7 +- .../ui/primitives/multiselect-combobox.tsx | 82 +++++++++++ 45 files changed, 546 insertions(+), 125 deletions(-) create mode 100644 packages/lib/constants/date-formats.ts create mode 100644 packages/lib/constants/time-zones.ts create mode 100644 packages/lib/server-only/document/get-document-meta-by-document-id.ts create mode 100644 packages/prisma/migrations/20231207134820_add_document_meta_dateformat_timezone/migration.sql create mode 100644 packages/ui/primitives/multiselect-combobox.tsx diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx index 7b500d295..aab736b28 100644 --- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx +++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx @@ -8,18 +8,19 @@ import { useRouter } from 'next/navigation'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { base64 } from '@documenso/lib/universal/base64'; import { putFile } from '@documenso/lib/universal/upload/put-file'; -import { DocumentDataType, Field, Prisma, Recipient } from '@documenso/prisma/client'; +import type { Field, Recipient } from '@documenso/prisma/client'; +import { DocumentDataType, Prisma } from '@documenso/prisma/client'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone'; import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields'; -import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types'; +import type { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types'; import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature'; -import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types'; +import type { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types'; import { DocumentFlowFormContainer, DocumentFlowFormContainerHeader, } from '@documenso/ui/primitives/document-flow/document-flow-root'; -import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; +import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { useToast } from '@documenso/ui/primitives/use-toast'; diff --git a/apps/marketing/src/components/(marketing)/single-player-mode/create-single-player-document.action.ts b/apps/marketing/src/components/(marketing)/single-player-mode/create-single-player-document.action.ts index 29321f31e..6fb8b4e7d 100644 --- a/apps/marketing/src/components/(marketing)/single-player-mode/create-single-player-document.action.ts +++ b/apps/marketing/src/components/(marketing)/single-player-mode/create-single-player-document.action.ts @@ -10,6 +10,7 @@ import { z } from 'zod'; import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed'; +import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email'; import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf'; import { alphaid } from '@documenso/lib/universal/id'; @@ -215,7 +216,7 @@ const mapField = ( signer: TCreateSinglePlayerDocumentSchema['signer'], ) => { const customText = match(field.type) - .with(FieldType.DATE, () => DateTime.now().toFormat('yyyy-MM-dd hh:mm a')) + .with(FieldType.DATE, () => DateTime.now().toFormat(DEFAULT_DOCUMENT_DATE_FORMAT)) .with(FieldType.EMAIL, () => signer.email) .with(FieldType.NAME, () => signer.name) .otherwise(() => ''); diff --git a/apps/marketing/src/pages/api/stripe/webhook/index.ts b/apps/marketing/src/pages/api/stripe/webhook/index.ts index ad9bfe808..b7daaef0f 100644 --- a/apps/marketing/src/pages/api/stripe/webhook/index.ts +++ b/apps/marketing/src/pages/api/stripe/webhook/index.ts @@ -1,4 +1,4 @@ -import { NextApiRequest, NextApiResponse } from 'next'; +import type { NextApiRequest, NextApiResponse } from 'next'; import { randomBytes } from 'crypto'; import { readFileSync } from 'fs'; @@ -7,7 +7,8 @@ import { buffer } from 'micro'; import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf'; import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf'; import { redis } from '@documenso/lib/server-only/redis'; -import { Stripe, stripe } from '@documenso/lib/server-only/stripe'; +import type { Stripe } from '@documenso/lib/server-only/stripe'; +import { stripe } from '@documenso/lib/server-only/stripe'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { updateFile } from '@documenso/lib/universal/upload/update-file'; import { prisma } from '@documenso/prisma'; diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index 3baf5d63b..9ae270d28 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -9,7 +9,6 @@ import type { z } from 'zod'; import { trpc } from '@documenso/trpc/react'; import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema'; import { Button } from '@documenso/ui/primitives/button'; -import { Combobox } from '@documenso/ui/primitives/combobox'; import { Form, FormControl, @@ -19,6 +18,7 @@ import { FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; +import { MultiSelectCombobox } from '@documenso/ui/primitives/multiselect-combobox'; import { useToast } from '@documenso/ui/primitives/use-toast'; const ZUserFormSchema = ZUpdateProfileMutationByAdminSchema.omit({ id: true }); @@ -117,7 +117,7 @@ export default function UserPage({ params }: { params: { id: number } }) {
Roles - onChange(values)} /> diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index 027d845b2..95951a714 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -4,21 +4,21 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; -import { DocumentData, Field, Recipient, User } from '@documenso/prisma/client'; -import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; +import type { DocumentData, Field, Recipient, User } from '@documenso/prisma/client'; +import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { cn } from '@documenso/ui/lib/utils'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields'; -import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types'; +import type { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types'; import { AddSignersFormPartial } from '@documenso/ui/primitives/document-flow/add-signers'; -import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types'; +import type { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types'; import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/add-subject'; -import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types'; +import type { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types'; import { DocumentFlowFormContainer, DocumentFlowFormContainerHeader, } from '@documenso/ui/primitives/document-flow/document-flow-root'; -import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; +import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -117,14 +117,16 @@ export const EditDocumentForm = ({ }; const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => { - const { subject, message } = data.email; + const { subject, message, timezone, dateFormat } = data.meta; try { await completeDocument({ documentId: document.id, - email: { + meta: { subject, message, + timezone, + dateFormat, }, }); diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx index 9963d072a..5ae0ad938 100644 --- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx @@ -23,6 +23,7 @@ export type UploadDocumentProps = { export const UploadDocument = ({ className }: UploadDocumentProps) => { const router = useRouter(); + const { data: session } = useSession(); const { toast } = useToast(); diff --git a/apps/web/src/app/(signing)/sign/[token]/date-field.tsx b/apps/web/src/app/(signing)/sign/[token]/date-field.tsx index 9cff29c64..40d0f945a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/date-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/date-field.tsx @@ -6,8 +6,13 @@ import { useRouter } from 'next/navigation'; import { Loader } from 'lucide-react'; -import { Recipient } from '@documenso/prisma/client'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import { + DEFAULT_DOCUMENT_DATE_FORMAT, + convertToLocalSystemFormat, +} from '@documenso/lib/constants/date-formats'; +import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; +import type { Recipient } from '@documenso/prisma/client'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -16,9 +21,16 @@ import { SigningFieldContainer } from './signing-field-container'; export type DateFieldProps = { field: FieldWithSignature; recipient: Recipient; + dateFormat?: string | null; + timezone?: string | null; }; -export const DateField = ({ field, recipient }: DateFieldProps) => { +export const DateField = ({ + field, + recipient, + dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT, + timezone = DEFAULT_DOCUMENT_TIME_ZONE, +}: DateFieldProps) => { const router = useRouter(); const { toast } = useToast(); @@ -35,12 +47,18 @@ export const DateField = ({ field, recipient }: DateFieldProps) => { const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; + const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone); + + const isDifferentTimeZone = field.inserted && localDateString !== field.customText; + + const tooltipText = `"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`; + const onSign = async () => { try { await signFieldWithToken({ token: recipient.token, fieldId: field.id, - value: '', + value: dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, }); startTransition(() => router.refresh()); @@ -75,7 +93,13 @@ export const DateField = ({ field, recipient }: DateFieldProps) => { }; return ( - + {isLoading && (
@@ -87,7 +111,7 @@ export const DateField = ({ field, recipient }: DateFieldProps) => { )} {field.inserted && ( -

{field.customText}

+

{localDateString}

)} ); diff --git a/apps/web/src/app/(signing)/sign/[token]/email-field.tsx b/apps/web/src/app/(signing)/sign/[token]/email-field.tsx index f6f790799..4d52ca50a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/email-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/email-field.tsx @@ -6,8 +6,8 @@ import { useRouter } from 'next/navigation'; import { Loader } from 'lucide-react'; -import { Recipient } from '@documenso/prisma/client'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { Recipient } from '@documenso/prisma/client'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -79,7 +79,7 @@ export const EmailField = ({ field, recipient }: EmailFieldProps) => { }; return ( - + {isLoading && (
diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index 5c6779c62..cce0e1b8e 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -9,7 +9,7 @@ import { useForm } from 'react-hook-form'; import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; -import { Document, Field, Recipient } from '@documenso/prisma/client'; +import type { Document, Field, Recipient } from '@documenso/prisma/client'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; @@ -32,6 +32,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = const { data: session } = useSession(); const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext(); + const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); const { @@ -81,7 +82,11 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = disabled={isSubmitting} className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')} > -
+

Sign Document

diff --git a/apps/web/src/app/(signing)/sign/[token]/name-field.tsx b/apps/web/src/app/(signing)/sign/[token]/name-field.tsx index bbe18fb8a..6e661e77a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/name-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/name-field.tsx @@ -6,8 +6,8 @@ import { useRouter } from 'next/navigation'; import { Loader } from 'lucide-react'; -import { Recipient } from '@documenso/prisma/client'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { Recipient } from '@documenso/prisma/client'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog'; @@ -98,7 +98,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => { }; return ( - + {isLoading && (

diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 67e679412..865f553d6 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -2,9 +2,12 @@ import { notFound, redirect } from 'next/navigation'; import { match } from 'ts-pattern'; +import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; +import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; +import { getDocumentMetaByDocumentId } from '@documenso/lib/server-only/document/get-document-meta-by-document-id'; import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; @@ -40,6 +43,8 @@ export default async function SigningPage({ params: { token } }: SigningPageProp viewedDocument({ token }).catch(() => null), ]); + const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null); + if (!document || !document.documentData || !recipient) { return notFound(); } @@ -97,7 +102,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp )) .with(FieldType.DATE, () => ( - + )) .with(FieldType.EMAIL, () => ( diff --git a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx index 01923bd6c..7ded3b698 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx @@ -6,8 +6,8 @@ import { useRouter } from 'next/navigation'; import { Loader } from 'lucide-react'; -import { Recipient } from '@documenso/prisma/client'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { Recipient } from '@documenso/prisma/client'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog'; @@ -121,7 +121,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => { }; return ( - + {isLoading && (
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 046e5b3df..b4805fa6b 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 @@ -2,8 +2,9 @@ import React from 'react'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { FieldRootContainer } from '@documenso/ui/components/field/field'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; export type SignatureFieldProps = { field: FieldWithSignature; @@ -11,6 +12,8 @@ export type SignatureFieldProps = { children: React.ReactNode; onSign?: () => Promise | void; onRemove?: () => Promise | void; + type?: 'Date' | 'Email' | 'Name' | 'Signature'; + tooltipText?: string | null; }; export const SigningFieldContainer = ({ @@ -19,6 +22,8 @@ export const SigningFieldContainer = ({ onSign, onRemove, children, + type, + tooltipText, }: SignatureFieldProps) => { const onSignFieldClick = async () => { if (field.inserted) { @@ -46,7 +51,22 @@ export const SigningFieldContainer = ({ /> )} - {field.inserted && !loading && ( + {type === 'Date' && field.inserted && !loading && ( + + + + + + {tooltipText && {tooltipText}} + + )} + + {type !== 'Date' && field.inserted && !loading && ( - + + - + + No value found. - - {allRoles.map((value: string, i: number) => ( - handleSelect(value)}> + + + {options.map((option, index) => ( + onOptionSelected(option)}> - {value} + + {option} ))} diff --git a/packages/ui/primitives/command.tsx b/packages/ui/primitives/command.tsx index 67cd3f487..cbc306c66 100644 --- a/packages/ui/primitives/command.tsx +++ b/packages/ui/primitives/command.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; -import { DialogProps } from '@radix-ui/react-dialog'; +import type { DialogProps } from '@radix-ui/react-dialog'; import { Command as CommandPrimitive } from 'cmdk'; import { Search } from 'lucide-react'; diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx index f662dca8b..0e17a9d35 100644 --- a/packages/ui/primitives/document-flow/add-fields.tsx +++ b/packages/ui/primitives/document-flow/add-fields.tsx @@ -11,7 +11,8 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { nanoid } from '@documenso/lib/universal/id'; -import { Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client'; +import type { Field, Recipient } from '@documenso/prisma/client'; +import { FieldType, SendStatus } from '@documenso/prisma/client'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Card, CardContent } from '@documenso/ui/primitives/card'; @@ -25,7 +26,7 @@ import { import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; -import { TAddFieldsFormSchema } from './add-fields.types'; +import type { TAddFieldsFormSchema } from './add-fields.types'; import { DocumentFlowFormContainerActions, DocumentFlowFormContainerContent, @@ -33,7 +34,8 @@ import { DocumentFlowFormContainerStep, } from './document-flow-root'; import { FieldItem } from './field-item'; -import { DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types'; +import type { DocumentFlowStep } from './types'; +import { FRIENDLY_FIELD_TYPE } from './types'; const fontCaveat = Caveat({ weight: ['500'], diff --git a/packages/ui/primitives/document-flow/add-signature.tsx b/packages/ui/primitives/document-flow/add-signature.tsx index aed252083..02e79ae19 100644 --- a/packages/ui/primitives/document-flow/add-signature.tsx +++ b/packages/ui/primitives/document-flow/add-signature.tsx @@ -7,21 +7,23 @@ import { DateTime } from 'luxon'; import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; +import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; -import { Field, FieldType } from '@documenso/prisma/client'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { Field } from '@documenso/prisma/client'; +import { FieldType } from '@documenso/prisma/client'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { cn } from '@documenso/ui/lib/utils'; import { Card, CardContent } from '@documenso/ui/primitives/card'; -import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types'; +import type { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types'; import { DocumentFlowFormContainerActions, DocumentFlowFormContainerContent, DocumentFlowFormContainerFooter, DocumentFlowFormContainerStep, } from '@documenso/ui/primitives/document-flow/document-flow-root'; -import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; +import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { Input } from '@documenso/ui/primitives/input'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; @@ -135,7 +137,7 @@ export const AddSignatureFormPartial = ({ return match(field.type) .with(FieldType.DATE, () => ({ ...field, - customText: DateTime.now().toFormat('yyyy-MM-dd hh:mm a'), + customText: DateTime.now().toFormat(DEFAULT_DOCUMENT_DATE_FORMAT), inserted: true, })) .with(FieldType.EMAIL, () => ({ diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 14d728f0a..0330700a5 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -9,21 +9,23 @@ import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { nanoid } from '@documenso/lib/universal/id'; -import { Field, Recipient, SendStatus } from '@documenso/prisma/client'; +import type { Field, Recipient } from '@documenso/prisma/client'; +import { SendStatus } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; import { useToast } from '@documenso/ui/primitives/use-toast'; -import { TAddSignersFormSchema, ZAddSignersFormSchema } from './add-signers.types'; +import type { TAddSignersFormSchema } from './add-signers.types'; +import { ZAddSignersFormSchema } from './add-signers.types'; import { DocumentFlowFormContainerActions, DocumentFlowFormContainerContent, DocumentFlowFormContainerFooter, DocumentFlowFormContainerStep, } from './document-flow-root'; -import { DocumentFlowStep } from './types'; +import type { DocumentFlowStep } from './types'; export type AddSignersFormProps = { documentFlow: DocumentFlowStep; diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx index 1bf3b2cb4..bbd5962da 100644 --- a/packages/ui/primitives/document-flow/add-subject.tsx +++ b/packages/ui/primitives/document-flow/add-subject.tsx @@ -1,22 +1,41 @@ 'use client'; -import { useForm } from 'react-hook-form'; +import { useEffect } from 'react'; -import { DocumentStatus, Field, Recipient } from '@documenso/prisma/client'; -import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; +import { Controller, useForm } from 'react-hook-form'; + +import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; +import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; +import type { Field, Recipient } from '@documenso/prisma/client'; +import { DocumentStatus, SendStatus } from '@documenso/prisma/client'; +import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@documenso/ui/primitives/accordion'; import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; import { Textarea } from '@documenso/ui/primitives/textarea'; -import { TAddSubjectFormSchema } from './add-subject.types'; +import { Combobox } from '../combobox'; +import type { TAddSubjectFormSchema } from './add-subject.types'; import { DocumentFlowFormContainerActions, DocumentFlowFormContainerContent, DocumentFlowFormContainerFooter, DocumentFlowFormContainerStep, } from './document-flow-root'; -import { DocumentFlowStep } from './types'; +import type { DocumentFlowStep } from './types'; export type AddSubjectFormProps = { documentFlow: DocumentFlowStep; @@ -29,27 +48,46 @@ export type AddSubjectFormProps = { export const AddSubjectFormPartial = ({ documentFlow, - recipients: _recipients, - fields: _fields, + recipients: recipients, + fields: fields, document, numberOfSteps, onSubmit, }: AddSubjectFormProps) => { const { + control, register, handleSubmit, - formState: { errors, isSubmitting }, + formState: { errors, isSubmitting, touchedFields }, + getValues, + setValue, } = useForm({ defaultValues: { - email: { + meta: { subject: document.documentMeta?.subject ?? '', message: document.documentMeta?.message ?? '', + timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, + dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, }, }, }); const onFormSubmit = handleSubmit(onSubmit); + const hasDateField = fields.find((field) => field.type === 'DATE'); + + const documentHasBeenSent = recipients.some( + (recipient) => recipient.sendStatus === SendStatus.SENT, + ); + + // We almost always want to set the timezone to the user's local timezone to avoid confusion + // when the document is signed. + useEffect(() => { + if (!touchedFields.meta?.timezone && !documentHasBeenSent) { + setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone); + } + }, [documentHasBeenSent, setValue, touchedFields.meta?.timezone]); + return ( <> @@ -65,10 +103,10 @@ export const AddSubjectFormPartial = ({ // placeholder="Subject" className="bg-background mt-2" disabled={isSubmitting} - {...register('email.subject')} + {...register('meta.subject')} /> - +
@@ -80,14 +118,12 @@ export const AddSubjectFormPartial = ({ id="message" className="bg-background mt-2 h-32 resize-none" disabled={isSubmitting} - {...register('email.message')} + {...register('meta.message')} />
@@ -117,6 +153,67 @@ export const AddSubjectFormPartial = ({
+ + + + + Advanced Options + + + + {hasDateField && ( +
+ + + ( + + )} + /> +
+ )} + + {hasDateField && ( +
+ + + ( + value && onChange(value)} + disabled={documentHasBeenSent} + /> + )} + /> +
+ )} +
+
+
diff --git a/packages/ui/primitives/document-flow/add-subject.types.ts b/packages/ui/primitives/document-flow/add-subject.types.ts index 33e2dedfb..ea14f4c0f 100644 --- a/packages/ui/primitives/document-flow/add-subject.types.ts +++ b/packages/ui/primitives/document-flow/add-subject.types.ts @@ -1,9 +1,14 @@ import { z } from 'zod'; +import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; +import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; + export const ZAddSubjectFormSchema = z.object({ - email: z.object({ + meta: z.object({ subject: z.string(), message: z.string(), + timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE), + dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT), }), }); diff --git a/packages/ui/primitives/multiselect-combobox.tsx b/packages/ui/primitives/multiselect-combobox.tsx new file mode 100644 index 000000000..bac87ce0b --- /dev/null +++ b/packages/ui/primitives/multiselect-combobox.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; + +import { Check, ChevronsUpDown } from 'lucide-react'; + +import { Role } from '@documenso/prisma/client'; +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from '@documenso/ui/primitives/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; + +type ComboboxProps = { + listValues: string[]; + onChange: (_values: string[]) => void; +}; + +const MultiSelectCombobox = ({ listValues, onChange }: ComboboxProps) => { + const [open, setOpen] = React.useState(false); + const [selectedValues, setSelectedValues] = React.useState([]); + const dbRoles = Object.values(Role); + + React.useEffect(() => { + setSelectedValues(listValues); + }, [listValues]); + + const allRoles = [...new Set([...dbRoles, ...selectedValues])]; + + const handleSelect = (currentValue: string) => { + let newSelectedValues; + if (selectedValues.includes(currentValue)) { + newSelectedValues = selectedValues.filter((value) => value !== currentValue); + } else { + newSelectedValues = [...selectedValues, currentValue]; + } + + setSelectedValues(newSelectedValues); + onChange(newSelectedValues); + setOpen(false); + }; + + return ( + + + + + + + + No value found. + + {allRoles.map((value: string, i: number) => ( + handleSelect(value)}> + + {value} + + ))} + + + + + ); +}; + +export { MultiSelectCombobox };