diff --git a/apps/marketing/src/app/(marketing)/open/monthly-new-users-chart.tsx b/apps/marketing/src/app/(marketing)/open/monthly-new-users-chart.tsx index 5aae8eeb5..b96bbf50d 100644 --- a/apps/marketing/src/app/(marketing)/open/monthly-new-users-chart.tsx +++ b/apps/marketing/src/app/(marketing)/open/monthly-new-users-chart.tsx @@ -3,7 +3,7 @@ import { DateTime } from 'luxon'; import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; -import { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth'; +import type { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth'; import { cn } from '@documenso/ui/lib/utils'; export type MonthlyNewUsersChartProps = { @@ -22,7 +22,7 @@ export const MonthlyNewUsersChart = ({ className, data }: MonthlyNewUsersChartPr return (
-

Monthly New Users

+

New Users

diff --git a/apps/marketing/src/app/(marketing)/open/monthly-total-users-chart.tsx b/apps/marketing/src/app/(marketing)/open/monthly-total-users-chart.tsx index 3c3f4476a..e31bb9def 100644 --- a/apps/marketing/src/app/(marketing)/open/monthly-total-users-chart.tsx +++ b/apps/marketing/src/app/(marketing)/open/monthly-total-users-chart.tsx @@ -3,7 +3,7 @@ import { DateTime } from 'luxon'; import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; -import { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth'; +import type { GetUserMonthlyGrowthResult } from '@documenso/lib/server-only/user/get-user-monthly-growth'; import { cn } from '@documenso/ui/lib/utils'; export type MonthlyTotalUsersChartProps = { @@ -22,7 +22,7 @@ export const MonthlyTotalUsersChart = ({ className, data }: MonthlyTotalUsersCha return (
-

Monthly Total Users

+

Total Users

diff --git a/apps/marketing/src/app/(marketing)/open/tooltip.tsx b/apps/marketing/src/app/(marketing)/open/tooltip.tsx index 0ae92d535..d077e7d35 100644 --- a/apps/marketing/src/app/(marketing)/open/tooltip.tsx +++ b/apps/marketing/src/app/(marketing)/open/tooltip.tsx @@ -29,10 +29,7 @@ export function OpenPageTooltip() { -

- August and earlier: Active subscribers. September and beyond: Numbers of active - subscriptions. -

+

Active Subscriptions.

diff --git a/apps/marketing/src/pages/api/stripe/webhook/index.ts b/apps/marketing/src/pages/api/stripe/webhook/index.ts index 2bdcdeb50..a19cffda9 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 { buffer } from 'micro'; @@ -6,7 +6,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 ffce3bd6c..a5dc9e23e 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -4,8 +4,8 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; -import { DocumentStatus } from '@documenso/prisma/client'; import type { DocumentData, Field, Recipient, User } from '@documenso/prisma/client'; +import { DocumentStatus } from '@documenso/prisma/client'; import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; @@ -145,14 +145,16 @@ export const EditDocumentForm = ({ }; const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => { - const { subject, message } = data.email; + const { subject, message, timezone, dateFormat } = data.meta; try { await sendDocument({ 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 5e93495e3..65b95f9ec 100644 --- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; @@ -25,6 +25,7 @@ export type UploadDocumentProps = { export const UploadDocument = ({ className }: UploadDocumentProps) => { const router = useRouter(); const analytics = useAnalytics(); + const { data: session } = useSession(); const { toast } = useToast(); @@ -35,6 +36,16 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => { const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation(); + const disabledMessage = useMemo(() => { + if (remaining.documents === 0) { + return 'You have reached your document limit.'; + } + + if (!session?.user.emailVerified) { + return 'Verify your email to upload documents.'; + } + }, [remaining.documents, session?.user.emailVerified]); + const onFileDrop = async (file: File) => { try { setIsLoading(true); @@ -90,6 +101,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => { diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx index 54757667a..4b1aed265 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx @@ -15,6 +15,8 @@ import { DocumentDownloadButton } from '@documenso/ui/components/document/docume import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { SigningCard3D } from '@documenso/ui/components/signing-card'; +import { truncateTitle } from '~/helpers/truncate-title'; + export type CompletedSigningPageProps = { params: { token?: string; @@ -36,6 +38,8 @@ export default async function CompletedSigningPage({ return notFound(); } + const truncatedTitle = truncateTitle(document.title); + const { documentData } = document; const [fields, recipient] = await Promise.all([ @@ -89,7 +93,7 @@ export default async function CompletedSigningPage({

You have signed - "{document.title}" + "{truncatedTitle}"

{match({ status: document.status, deletedAt: document.deletedAt }) 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..ce34a55fd 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 isDifferentTime = 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 29cd77995..4f20a8199 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -34,6 +34,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = const { data: session } = useSession(); const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext(); + const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); const { mutateAsync: completeDocumentWithToken } = @@ -92,7 +93,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 97babb82f..efd0b266c 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'; @@ -14,6 +17,8 @@ import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; +import { truncateTitle } from '~/helpers/truncate-title'; + import { DateField } from './date-field'; import { EmailField } from './email-field'; import { SigningForm } from './form'; @@ -42,10 +47,14 @@ 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(); } + const truncatedTitle = truncateTitle(document.title); + const { documentData } = document; const { user } = await getServerComponentSession(); @@ -77,7 +86,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp >

- {document.title} + {truncatedTitle}

@@ -111,7 +120,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]/sign-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx index 0ce750a39..faecf5d7e 100644 --- a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { Document, Field } from '@documenso/prisma/client'; +import type { Document, Field } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, @@ -9,6 +9,8 @@ import { DialogTrigger, } from '@documenso/ui/primitives/dialog'; +import { truncateTitle } from '~/helpers/truncate-title'; + export type SignDialogProps = { isSubmitting: boolean; document: Document; @@ -23,7 +25,7 @@ export const SignDialog = ({ onSignatureComplete, }: SignDialogProps) => { const [showDialog, setShowDialog] = useState(false); - + const truncatedTitle = truncateTitle(document.title); const isComplete = fields.every((field) => field.inserted); return ( @@ -43,7 +45,7 @@ export const SignDialog = ({
Sign Document
- You are about to finish signing "{document.title}". Are you sure? + You are about to finish signing "{truncatedTitle}". Are you sure?
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 ec3e45fe5..220d3084a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx @@ -127,7 +127,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/constants.ts b/packages/ui/primitives/constants.ts new file mode 100644 index 000000000..9771eb35a --- /dev/null +++ b/packages/ui/primitives/constants.ts @@ -0,0 +1,5 @@ +export const THEMES_TYPE = { + DARK: 'dark', + LIGHT: 'light', + SYSTEM: 'system' +}; \ No newline at end of file diff --git a/packages/ui/primitives/dialog.tsx b/packages/ui/primitives/dialog.tsx index 8e5ed20e5..47982ab09 100644 --- a/packages/ui/primitives/dialog.tsx +++ b/packages/ui/primitives/dialog.tsx @@ -20,7 +20,7 @@ const DialogPortal = ({ }: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' | 'center' }) => (
void | Promise; type?: 'document' | 'template'; [key: string]: unknown; @@ -96,6 +97,7 @@ export const DocumentDropzone = ({ className, onDrop, disabled, + disabledMessage = 'You cannot upload documents at this time.', type = 'document', ...props }: DocumentDropzoneProps) => { @@ -115,11 +117,12 @@ export const DocumentDropzone = ({ return ( */}
@@ -147,7 +150,7 @@ export const DocumentDropzone = ({
@@ -171,7 +174,9 @@ export const DocumentDropzone = ({ {DocumentDescription[type].headline}

-

Drag & drop your document here.

+

+ {disabled ? disabledMessage : 'Drag & drop your document here.'} +

diff --git a/packages/ui/primitives/document-flow/add-signature.tsx b/packages/ui/primitives/document-flow/add-signature.tsx index e4e5d9253..5accdca16 100644 --- a/packages/ui/primitives/document-flow/add-signature.tsx +++ b/packages/ui/primitives/document-flow/add-signature.tsx @@ -7,11 +7,13 @@ 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 type { Field } from '@documenso/prisma/client'; import { FieldType } from '@documenso/prisma/client'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import { FieldToolTip } from '../../components/field/field-tooltip'; import { cn } from '../../lib/utils'; @@ -34,7 +36,6 @@ import { SinglePlayerModeCustomTextField, SinglePlayerModeSignatureField, } from './single-player-mode-fields'; -import type { DocumentFlowStep } from './types'; export type AddSignatureFormProps = { defaultValues?: TAddSignatureFormSchema; @@ -140,7 +141,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-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx index 881d59c74..d73019732 100644 --- a/packages/ui/primitives/document-flow/add-subject.tsx +++ b/packages/ui/primitives/document-flow/add-subject.tsx @@ -1,11 +1,30 @@ 'use client'; -import { useForm } from 'react-hook-form'; +import { useEffect } from 'react'; +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 } from '@documenso/prisma/client'; +import { 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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; +import { Combobox } from '../combobox'; import { FormErrorMessage } from '../form/form-error-message'; import { Input } from '../input'; import { Label } from '../label'; @@ -31,20 +50,25 @@ export type AddSubjectFormProps = { export const AddSubjectFormPartial = ({ documentFlow, - recipients: _recipients, - fields: _fields, + recipients: recipients, + fields: fields, document, 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, }, }, }); @@ -52,6 +76,20 @@ export const AddSubjectFormPartial = ({ const onFormSubmit = handleSubmit(onSubmit); const { currentStep, totalSteps, previousStep } = useStep(); + 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 ( <> - +
@@ -86,14 +124,12 @@ export const AddSubjectFormPartial = ({ id="message" className="bg-background mt-2 h-32 resize-none" disabled={isSubmitting} - {...register('email.message')} + {...register('meta.message')} />
@@ -123,6 +159,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/document-flow/add-title.tsx b/packages/ui/primitives/document-flow/add-title.tsx index 8c2a9dc7a..afce0d9e0 100644 --- a/packages/ui/primitives/document-flow/add-title.tsx +++ b/packages/ui/primitives/document-flow/add-title.tsx @@ -64,7 +64,7 @@ export const AddTitleFormPartial = ({ diff --git a/packages/ui/primitives/document-flow/document-flow-root.tsx b/packages/ui/primitives/document-flow/document-flow-root.tsx index 42b70c58a..74a232e1d 100644 --- a/packages/ui/primitives/document-flow/document-flow-root.tsx +++ b/packages/ui/primitives/document-flow/document-flow-root.tsx @@ -22,12 +22,12 @@ export const DocumentFlowFormContainer = ({
-
{children}
+
{children}
); }; @@ -63,10 +63,7 @@ export const DocumentFlowFormContainerContent = ({ }: DocumentFlowFormContainerContentProps) => { return (
{children}
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 }; diff --git a/packages/ui/primitives/select.tsx b/packages/ui/primitives/select.tsx index 0d4789550..fba05f7ef 100644 --- a/packages/ui/primitives/select.tsx +++ b/packages/ui/primitives/select.tsx @@ -42,7 +42,7 @@ const SelectContent = React.forwardRef< (null); const [isPressed, setIsPressed] = useState(false); - const [points, setPoints] = useState([]); + const [lines, setLines] = useState([]); + const [currentLine, setCurrentLine] = useState([]); const perfectFreehandOptions = useMemo(() => { const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10; @@ -52,26 +54,7 @@ export const SignaturePad = ({ const point = Point.fromEvent(event, DPI, $el.current); - const newPoints = [...points, point]; - - setPoints(newPoints); - - if ($el.current) { - const ctx = $el.current.getContext('2d'); - - if (ctx) { - ctx.save(); - - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; - - const pathData = new Path2D( - getSvgPathFromStroke(getStroke(newPoints, perfectFreehandOptions)), - ); - - ctx.fill(pathData); - } - } + setCurrentLine([point]); }; const onMouseMove = (event: MouseEvent | PointerEvent | TouchEvent) => { @@ -85,31 +68,36 @@ export const SignaturePad = ({ const point = Point.fromEvent(event, DPI, $el.current); - if (point.distanceTo(points[points.length - 1]) > 5) { - const newPoints = [...points, point]; - - setPoints(newPoints); + if (point.distanceTo(currentLine[currentLine.length - 1]) > 5) { + setCurrentLine([...currentLine, point]); + // Update the canvas here to draw the lines if ($el.current) { const ctx = $el.current.getContext('2d'); if (ctx) { ctx.restore(); - ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; - const pathData = new Path2D( - getSvgPathFromStroke(getStroke(points, perfectFreehandOptions)), - ); + lines.forEach((line) => { + const pathData = new Path2D( + getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)), + ); + ctx.fill(pathData); + }); + + const pathData = new Path2D( + getSvgPathFromStroke(getStroke([...currentLine, point], perfectFreehandOptions)), + ); ctx.fill(pathData); } } } }; - const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addPoint = true) => { + const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addLine = true) => { if (event.cancelable) { event.preventDefault(); } @@ -118,15 +106,16 @@ export const SignaturePad = ({ const point = Point.fromEvent(event, DPI, $el.current); - const newPoints = [...points]; + const newLines = [...lines]; - if (addPoint) { - newPoints.push(point); - - setPoints(newPoints); + if (addLine && currentLine.length > 0) { + newLines.push([...currentLine, point]); + setCurrentLine([]); } - if ($el.current && newPoints.length > 0) { + setLines(newLines); + + if ($el.current && newLines.length > 0) { const ctx = $el.current.getContext('2d'); if (ctx) { @@ -135,19 +124,18 @@ export const SignaturePad = ({ ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; - const pathData = new Path2D( - getSvgPathFromStroke(getStroke(newPoints, perfectFreehandOptions)), - ); + newLines.forEach((line) => { + const pathData = new Path2D( + getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)), + ); + ctx.fill(pathData); + }); - ctx.fill(pathData); + onChange?.($el.current.toDataURL()); ctx.save(); } - - onChange?.($el.current.toDataURL()); } - - setPoints([]); }; const onMouseEnter = (event: MouseEvent | PointerEvent | TouchEvent) => { @@ -177,7 +165,29 @@ export const SignaturePad = ({ onChange?.(null); - setPoints([]); + setLines([]); + setCurrentLine([]); + }; + + const onUndoClick = () => { + if (lines.length === 0) { + return; + } + + const newLines = [...lines]; + newLines.pop(); // Remove the last line + setLines(newLines); + + // Clear the canvas + if ($el.current) { + const ctx = $el.current.getContext('2d'); + ctx?.clearRect(0, 0, $el.current.width, $el.current.height); + + newLines.forEach((line) => { + const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions))); + ctx?.fill(pathData); + }); + } }; useEffect(() => { @@ -217,15 +227,29 @@ export const SignaturePad = ({ {...props} /> -
+
+ + {lines.length > 0 && ( +
+ +
+ )}
); }; diff --git a/packages/ui/primitives/theme-switcher.tsx b/packages/ui/primitives/theme-switcher.tsx index 7aa570749..fcc789404 100644 --- a/packages/ui/primitives/theme-switcher.tsx +++ b/packages/ui/primitives/theme-switcher.tsx @@ -4,6 +4,8 @@ import { useTheme } from 'next-themes'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; +import { THEMES_TYPE } from './constants'; + export const ThemeSwitcher = () => { const { theme, setTheme } = useTheme(); const isMounted = useIsMounted(); @@ -12,9 +14,9 @@ export const ThemeSwitcher = () => {