From 8d1b960aa87e0e0358489b2b4facfc11515f546d Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Mon, 25 Dec 2023 23:16:56 +0000 Subject: [PATCH 01/14] feat: add templates to command menu --- .../components/(dashboard)/common/command-menu.tsx | 14 ++++++++++++++ packages/lib/constants/keyboard-shortcuts.ts | 1 + 2 files changed, 15 insertions(+) diff --git a/apps/web/src/components/(dashboard)/common/command-menu.tsx b/apps/web/src/components/(dashboard)/common/command-menu.tsx index 19a35874e..5be89343b 100644 --- a/apps/web/src/components/(dashboard)/common/command-menu.tsx +++ b/apps/web/src/components/(dashboard)/common/command-menu.tsx @@ -11,6 +11,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { DOCUMENTS_PAGE_SHORTCUT, SETTINGS_PAGE_SHORTCUT, + TEMPLATES_PAGE_SHORTCUT, } from '@documenso/lib/constants/keyboard-shortcuts'; import { trpc as trpcReact } from '@documenso/trpc/react'; import { @@ -38,6 +39,14 @@ const DOCUMENTS_PAGES = [ { label: 'Inbox documents', path: '/documents?status=INBOX' }, ]; +const TEMPLATES_PAGES = [ + { + label: 'All templates', + path: '/templates', + shortcut: TEMPLATES_PAGE_SHORTCUT.replace('+', ''), + }, +]; + const SETTINGS_PAGES = [ { label: 'Settings', @@ -124,10 +133,12 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { const goToSettings = useCallback(() => push(SETTINGS_PAGES[0].path), [push]); const goToDocuments = useCallback(() => push(DOCUMENTS_PAGES[0].path), [push]); + const goToTemplates = useCallback(() => push(TEMPLATES_PAGES[0].path), [push]); useHotkeys(['ctrl+k', 'meta+k'], toggleOpen); useHotkeys(SETTINGS_PAGE_SHORTCUT, goToSettings); useHotkeys(DOCUMENTS_PAGE_SHORTCUT, goToDocuments); + useHotkeys(TEMPLATES_PAGE_SHORTCUT, goToTemplates); const handleKeyDown = (e: React.KeyboardEvent) => { // Escape goes to previous page @@ -174,6 +185,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { + + + diff --git a/packages/lib/constants/keyboard-shortcuts.ts b/packages/lib/constants/keyboard-shortcuts.ts index 896b4abf5..34d3a02e6 100644 --- a/packages/lib/constants/keyboard-shortcuts.ts +++ b/packages/lib/constants/keyboard-shortcuts.ts @@ -1,2 +1,3 @@ export const SETTINGS_PAGE_SHORTCUT = 'N+S'; export const DOCUMENTS_PAGE_SHORTCUT = 'N+D'; +export const TEMPLATES_PAGE_SHORTCUT = 'N+T'; From 5a32b5cafd3b5ebe6f2a4fdff0ce85c9a4849dae Mon Sep 17 00:00:00 2001 From: Apoorv Taneja Date: Tue, 26 Dec 2023 05:19:27 +0530 Subject: [PATCH 02/14] fix: added constants for theme variables (#777) fixes: #776 --- .../components/(dashboard)/common/command-menu.tsx | 7 ++++--- packages/ui/primitives/constants.ts | 5 +++++ packages/ui/primitives/theme-switcher.tsx | 14 ++++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 packages/ui/primitives/constants.ts diff --git a/apps/web/src/components/(dashboard)/common/command-menu.tsx b/apps/web/src/components/(dashboard)/common/command-menu.tsx index 19a35874e..fe690329b 100644 --- a/apps/web/src/components/(dashboard)/common/command-menu.tsx +++ b/apps/web/src/components/(dashboard)/common/command-menu.tsx @@ -22,6 +22,7 @@ import { CommandList, CommandShortcut, } from '@documenso/ui/primitives/command'; +import { THEMES_TYPE } from '@documenso/ui/primitives/constants'; const DOCUMENTS_PAGES = [ { @@ -215,9 +216,9 @@ const Commands = ({ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) => { const THEMES = useMemo( () => [ - { label: 'Light Mode', theme: 'light', icon: Sun }, - { label: 'Dark Mode', theme: 'dark', icon: Moon }, - { label: 'System Theme', theme: 'system', icon: Monitor }, + { label: 'Light Mode', theme: THEMES_TYPE.LIGHT, icon: Sun }, + { label: 'Dark Mode', theme: THEMES_TYPE.DARK, icon: Moon }, + { label: 'System Theme', theme: THEMES_TYPE.SYSTEM, icon: Monitor }, ], [], ); 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/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 = () => {
+ + {lines.length > 0 && ( +
+ +
+ )} ); }; From 32633f96d292056a22e855986ba795bfba3e4b5c 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 08/14] feat: dateformat and timezone customization (#506) --- .../src/pages/api/stripe/webhook/index.ts | 5 +- .../app/(dashboard)/admin/users/[id]/page.tsx | 4 +- .../documents/[id]/edit-document.tsx | 8 +- .../(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 | 7 +- .../app/(signing)/sign/[token]/name-field.tsx | 6 +- .../src/app/(signing)/sign/[token]/page.tsx | 13 +- .../sign/[token]/signature-field.tsx | 2 +- .../sign/[token]/signing-field-container.tsx | 24 +++- .../components/formatter/document-status.tsx | 4 +- .../src/components/formatter/locale-date.tsx | 6 +- package-lock.json | 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/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 ++ .../server-only/document/update-document.ts | 2 +- .../field/sign-field-with-token.ts | 13 +- packages/lib/utils/recipient-formatter.ts | 2 +- .../migration.sql | 3 + .../migration.sql | 8 ++ 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 +- .../trpc/server/document-router/router.ts | 10 +- .../trpc/server/document-router/schema.ts | 4 +- packages/trpc/server/trpc.ts | 2 +- packages/ui/lib/utils.ts | 3 +- packages/ui/primitives/combobox.tsx | 71 +++++------ packages/ui/primitives/command.tsx | 2 +- .../document-flow/add-signature.tsx | 5 +- .../primitives/document-flow/add-subject.tsx | 119 ++++++++++++++++-- .../document-flow/add-subject.types.ts | 7 +- .../ui/primitives/multiselect-combobox.tsx | 82 ++++++++++++ 40 files changed, 517 insertions(+), 94 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/prisma/migrations/20231214081915_remove_template_token_column/migration.sql create mode 100644 packages/ui/primitives/multiselect-combobox.tsx 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..04ba990d5 100644 --- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx @@ -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(); 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 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..18b81696e 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'; @@ -42,6 +45,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(); } @@ -111,7 +116,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 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/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/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 }; From eb84d7ff3cb32269b383f471e485538bab28bccd Mon Sep 17 00:00:00 2001 From: Mythie Date: Wed, 27 Dec 2023 11:58:02 +1100 Subject: [PATCH 09/14] fix: remove invalid migration --- .../migration.sql | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql diff --git a/packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql b/packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql deleted file mode 100644 index 8514a14b7..000000000 --- a/packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `templateToken` on the `Recipient` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "Recipient" DROP COLUMN "templateToken"; From d8eff192febf8358e2dbb3069236368ccecdd1b8 Mon Sep 17 00:00:00 2001 From: Mythie Date: Wed, 27 Dec 2023 14:04:22 +1100 Subject: [PATCH 10/14] fix: update date format and add missing default --- .../app/(signing)/sign/[token]/date-field.tsx | 4 ++-- packages/lib/constants/date-formats.ts | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) 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 40d0f945a..ce34a55fd 100644 --- a/apps/web/src/app/(signing)/sign/[token]/date-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/date-field.tsx @@ -49,7 +49,7 @@ export const DateField = ({ const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone); - const isDifferentTimeZone = field.inserted && localDateString !== field.customText; + const isDifferentTime = field.inserted && localDateString !== field.customText; const tooltipText = `"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`; @@ -98,7 +98,7 @@ export const DateField = ({ onSign={onSign} onRemove={onRemove} type="Date" - tooltipText={isDifferentTimeZone ? tooltipText : undefined} + tooltipText={isDifferentTime ? tooltipText : undefined} > {isLoading && (
diff --git a/packages/lib/constants/date-formats.ts b/packages/lib/constants/date-formats.ts index 8c9ebe9e6..5b36cefdf 100644 --- a/packages/lib/constants/date-formats.ts +++ b/packages/lib/constants/date-formats.ts @@ -5,10 +5,15 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from './time-zones'; export const DEFAULT_DOCUMENT_DATE_FORMAT = 'yyyy-MM-dd hh:mm a'; export const DATE_FORMATS = [ + { + key: 'yyyy-MM-dd_hh:mm_a', + label: 'YYYY-MM-DD HH:mm a', + value: DEFAULT_DOCUMENT_DATE_FORMAT, + }, { key: 'YYYYMMDD', label: 'YYYY-MM-DD', - value: DEFAULT_DOCUMENT_DATE_FORMAT, + value: 'YYYY-MM-DD', }, { key: 'DDMMYYYY', @@ -57,15 +62,18 @@ export const convertToLocalSystemFormat = ( dateFormat: string | null = DEFAULT_DOCUMENT_DATE_FORMAT, timeZone: string | null = DEFAULT_DOCUMENT_TIME_ZONE, ): string => { - const parsedDate = DateTime.fromFormat(customText, dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, { - zone: timeZone ?? DEFAULT_DOCUMENT_TIME_ZONE, + const coalescedDateFormat = dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT; + const coalescedTimeZone = timeZone ?? DEFAULT_DOCUMENT_TIME_ZONE; + + const parsedDate = DateTime.fromFormat(customText, coalescedDateFormat, { + zone: coalescedTimeZone, }); if (!parsedDate.isValid) { return 'Invalid date'; } - const formattedDate = parsedDate.toLocal().toFormat(dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT); + const formattedDate = parsedDate.toLocal().toFormat(coalescedDateFormat); return formattedDate; }; From c4800f74b90687f9f0d10e96bc2510f4cbbb5d44 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 28 Dec 2023 20:07:29 +1100 Subject: [PATCH 11/14] feat: update disabled dropzone text (#787) Update the dropzone so it will display the relevant disabled text based on the reason it is disabled. --- .../app/(dashboard)/documents/upload-document.tsx | 13 ++++++++++++- packages/ui/primitives/document-dropzone.tsx | 6 +++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx index 04ba990d5..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'; @@ -36,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); @@ -91,6 +101,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => { diff --git a/packages/ui/primitives/document-dropzone.tsx b/packages/ui/primitives/document-dropzone.tsx index 8ba22109a..21337956d 100644 --- a/packages/ui/primitives/document-dropzone.tsx +++ b/packages/ui/primitives/document-dropzone.tsx @@ -87,6 +87,7 @@ const DocumentDescription = { export type DocumentDropzoneProps = { className?: string; disabled?: boolean; + disabledMessage?: string; onDrop?: (_file: File) => 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) => { @@ -172,7 +174,9 @@ export const DocumentDropzone = ({ {DocumentDescription[type].headline}

-

Drag & drop your document here.

+

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

From 3f89f8725bc407248275e81a363ace551111b486 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 28 Dec 2023 20:08:19 +1100 Subject: [PATCH 12/14] fix: resolve conflicting z-index values (#788) ## Description Currently there are various z-index values that are causing: - Toasts to be placed behind dialog blur background - Menu being cropped off by header ## Changes Made - Revert `z-[1000]` back to `z-50` for the header (not exactly sure why it was bumped) - Refactor z-indexes over 9000 to start from 1000 - Ensure z-index of toast is higher than dialog --- apps/web/src/components/(dashboard)/layout/header.tsx | 2 +- packages/ui/primitives/dialog.tsx | 2 +- packages/ui/primitives/select.tsx | 2 +- packages/ui/primitives/toast.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/(dashboard)/layout/header.tsx b/apps/web/src/components/(dashboard)/layout/header.tsx index cf8873a1a..bdae6c511 100644 --- a/apps/web/src/components/(dashboard)/layout/header.tsx +++ b/apps/web/src/components/(dashboard)/layout/header.tsx @@ -33,7 +33,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => { return (
5 && 'border-b-border', className, )} 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' }) => (
Date: Thu, 28 Dec 2023 15:06:46 +0530 Subject: [PATCH 13/14] fix: fixed the title box overlapping issue (#785) The issue is fixed. Now the box is no more overlapping Screenshot 2023-12-26 at 10 20 32 AM --- packages/ui/primitives/document-flow/add-title.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = ({ From 341481d6dba8f91782e8e6787b7ddf3521064e19 Mon Sep 17 00:00:00 2001 From: Mohith Gadireddy <88539464+Mohith234@users.noreply.github.com> Date: Fri, 29 Dec 2023 15:48:19 +0530 Subject: [PATCH 14/14] fix: trimmed long file names for better UX (#760) Fixes #755 ### Notes for Reviewers - The max length of the title is set to be `16` - If the length of the title is <16 it returns the original one. - Or else the title will be the first 8 characters (start) and last 8 characters (end) - The truncated file name will look like `start...end` ### Screenshot for reference ![image](https://github.com/documenso/documenso/assets/88539464/565e4868-7bb1-4b46-9cb0-886d542b8a01) --------- Co-authored-by: Catalin Pit <25515812+catalinpit@users.noreply.github.com> --- .../src/app/(signing)/sign/[token]/complete/page.tsx | 6 +++++- apps/web/src/app/(signing)/sign/[token]/page.tsx | 6 +++++- .../web/src/app/(signing)/sign/[token]/sign-dialog.tsx | 8 +++++--- apps/web/src/helpers/truncate-title.ts | 10 ++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/helpers/truncate-title.ts 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]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 18b81696e..efd0b266c 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -17,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'; @@ -51,6 +53,8 @@ export default async function SigningPage({ params: { token } }: SigningPageProp return notFound(); } + const truncatedTitle = truncateTitle(document.title); + const { documentData } = document; const { user } = await getServerComponentSession(); @@ -82,7 +86,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp >

- {document.title} + {truncatedTitle}

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/helpers/truncate-title.ts b/apps/web/src/helpers/truncate-title.ts new file mode 100644 index 000000000..2ad25c39a --- /dev/null +++ b/apps/web/src/helpers/truncate-title.ts @@ -0,0 +1,10 @@ +export const truncateTitle = (title: string, maxLength: number = 16) => { + if (title.length <= maxLength) { + return title; + } + + const start = title.slice(0, maxLength / 2); + const end = title.slice(-maxLength / 2); + + return `${start}.....${end}`; +};