feat: dateformat and timezone customization (#506)
This commit is contained in:
@@ -8,18 +8,19 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { base64 } from '@documenso/lib/universal/base64';
|
import { base64 } from '@documenso/lib/universal/base64';
|
||||||
import { putFile } from '@documenso/lib/universal/upload/put-file';
|
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 { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||||
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
|
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 { 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 {
|
import {
|
||||||
DocumentFlowFormContainer,
|
DocumentFlowFormContainer,
|
||||||
DocumentFlowFormContainerHeader,
|
DocumentFlowFormContainerHeader,
|
||||||
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
} 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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { z } from 'zod';
|
|||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import { render } from '@documenso/email/render';
|
import { render } from '@documenso/email/render';
|
||||||
import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed';
|
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 { 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 { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf';
|
||||||
import { alphaid } from '@documenso/lib/universal/id';
|
import { alphaid } from '@documenso/lib/universal/id';
|
||||||
@@ -215,7 +216,7 @@ const mapField = (
|
|||||||
signer: TCreateSinglePlayerDocumentSchema['signer'],
|
signer: TCreateSinglePlayerDocumentSchema['signer'],
|
||||||
) => {
|
) => {
|
||||||
const customText = match(field.type)
|
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.EMAIL, () => signer.email)
|
||||||
.with(FieldType.NAME, () => signer.name)
|
.with(FieldType.NAME, () => signer.name)
|
||||||
.otherwise(() => '');
|
.otherwise(() => '');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { readFileSync } from 'fs';
|
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 { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf';
|
||||||
import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf';
|
import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf';
|
||||||
import { redis } from '@documenso/lib/server-only/redis';
|
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 { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||||
import { updateFile } from '@documenso/lib/universal/upload/update-file';
|
import { updateFile } from '@documenso/lib/universal/upload/update-file';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type { z } from 'zod';
|
|||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema';
|
import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Combobox } from '@documenso/ui/primitives/combobox';
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { MultiSelectCombobox } from '@documenso/ui/primitives/multiselect-combobox';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
const ZUserFormSchema = ZUpdateProfileMutationByAdminSchema.omit({ id: true });
|
const ZUserFormSchema = ZUpdateProfileMutationByAdminSchema.omit({ id: true });
|
||||||
@@ -117,7 +117,7 @@ export default function UserPage({ params }: { params: { id: number } }) {
|
|||||||
<fieldset className="flex flex-col gap-2">
|
<fieldset className="flex flex-col gap-2">
|
||||||
<FormLabel className="text-muted-foreground">Roles</FormLabel>
|
<FormLabel className="text-muted-foreground">Roles</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Combobox
|
<MultiSelectCombobox
|
||||||
listValues={roles}
|
listValues={roles}
|
||||||
onChange={(values: string[]) => onChange(values)}
|
onChange={(values: string[]) => onChange(values)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { DocumentData, Field, Recipient, User } from '@documenso/prisma/client';
|
import type { DocumentData, Field, Recipient, User } from '@documenso/prisma/client';
|
||||||
import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
|
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 { 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 { 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 {
|
import {
|
||||||
DocumentFlowFormContainer,
|
DocumentFlowFormContainer,
|
||||||
DocumentFlowFormContainerHeader,
|
DocumentFlowFormContainerHeader,
|
||||||
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
} 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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@@ -117,14 +117,16 @@ export const EditDocumentForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
||||||
const { subject, message } = data.email;
|
const { subject, message, timezone, dateFormat } = data.meta;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await completeDocument({
|
await completeDocument({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
email: {
|
meta: {
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
|
timezone,
|
||||||
|
dateFormat,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type UploadDocumentProps = {
|
|||||||
|
|
||||||
export const UploadDocument = ({ className }: UploadDocumentProps) => {
|
export const UploadDocument = ({ className }: UploadDocumentProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ import { useRouter } from 'next/navigation';
|
|||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
|
||||||
import { Recipient } from '@documenso/prisma/client';
|
import {
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
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 { trpc } from '@documenso/trpc/react';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@@ -16,9 +21,16 @@ import { SigningFieldContainer } from './signing-field-container';
|
|||||||
export type DateFieldProps = {
|
export type DateFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
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 router = useRouter();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -35,12 +47,18 @@ export const DateField = ({ field, recipient }: DateFieldProps) => {
|
|||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
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 () => {
|
const onSign = async () => {
|
||||||
try {
|
try {
|
||||||
await signFieldWithToken({
|
await signFieldWithToken({
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
value: '',
|
value: dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
});
|
});
|
||||||
|
|
||||||
startTransition(() => router.refresh());
|
startTransition(() => router.refresh());
|
||||||
@@ -75,7 +93,13 @@ export const DateField = ({ field, recipient }: DateFieldProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
<SigningFieldContainer
|
||||||
|
field={field}
|
||||||
|
onSign={onSign}
|
||||||
|
onRemove={onRemove}
|
||||||
|
type="Date"
|
||||||
|
tooltipText={isDifferentTimeZone ? tooltipText : undefined}
|
||||||
|
>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
@@ -87,7 +111,7 @@ export const DateField = ({ field, recipient }: DateFieldProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground text-sm duration-200">{field.customText}</p>
|
<p className="text-muted-foreground text-sm duration-200">{localDateString}</p>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</SigningFieldContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useRouter } from 'next/navigation';
|
|||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
|
||||||
import { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ export const EmailField = ({ field, recipient }: EmailFieldProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Email">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
|
|
||||||
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
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 { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@@ -32,6 +32,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
||||||
|
|
||||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -81,7 +82,11 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}
|
className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}
|
||||||
>
|
>
|
||||||
<div className={cn('flex flex-1 flex-col')}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
|
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useRouter } from 'next/navigation';
|
|||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
|
||||||
import { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
|
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
|
||||||
@@ -98,7 +98,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Name">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { notFound, redirect } from 'next/navigation';
|
|||||||
|
|
||||||
import { match } from 'ts-pattern';
|
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 { 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 { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
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 { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-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),
|
viewedDocument({ token }).catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null);
|
||||||
|
|
||||||
if (!document || !document.documentData || !recipient) {
|
if (!document || !document.documentData || !recipient) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
@@ -97,7 +102,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
<NameField key={field.id} field={field} recipient={recipient} />
|
<NameField key={field.id} field={field} recipient={recipient} />
|
||||||
))
|
))
|
||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
<DateField key={field.id} field={field} recipient={recipient} />
|
<DateField
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
|
recipient={recipient}
|
||||||
|
dateFormat={documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||||
|
timezone={documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.EMAIL, () => (
|
.with(FieldType.EMAIL, () => (
|
||||||
<EmailField key={field.id} field={field} recipient={recipient} />
|
<EmailField key={field.id} field={field} recipient={recipient} />
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useRouter } from 'next/navigation';
|
|||||||
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
|
|
||||||
import { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
|
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
|
||||||
@@ -121,7 +121,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
|
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Signature">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||||
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import React from 'react';
|
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 { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
export type SignatureFieldProps = {
|
export type SignatureFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
@@ -11,6 +12,8 @@ export type SignatureFieldProps = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onSign?: () => Promise<void> | void;
|
onSign?: () => Promise<void> | void;
|
||||||
onRemove?: () => Promise<void> | void;
|
onRemove?: () => Promise<void> | void;
|
||||||
|
type?: 'Date' | 'Email' | 'Name' | 'Signature';
|
||||||
|
tooltipText?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningFieldContainer = ({
|
export const SigningFieldContainer = ({
|
||||||
@@ -19,6 +22,8 @@ export const SigningFieldContainer = ({
|
|||||||
onSign,
|
onSign,
|
||||||
onRemove,
|
onRemove,
|
||||||
children,
|
children,
|
||||||
|
type,
|
||||||
|
tooltipText,
|
||||||
}: SignatureFieldProps) => {
|
}: SignatureFieldProps) => {
|
||||||
const onSignFieldClick = async () => {
|
const onSignFieldClick = async () => {
|
||||||
if (field.inserted) {
|
if (field.inserted) {
|
||||||
@@ -46,7 +51,22 @@ export const SigningFieldContainer = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && !loading && (
|
{type === 'Date' && field.inserted && !loading && (
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="text-destructive bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 backdrop-blur-sm duration-200 group-hover:opacity-100"
|
||||||
|
onClick={onRemoveSignedFieldClick}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
{tooltipText && <TooltipContent className="max-w-xs">{tooltipText}</TooltipContent>}
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type !== 'Date' && field.inserted && !loading && (
|
||||||
<button
|
<button
|
||||||
className="text-destructive bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 backdrop-blur-sm duration-200 group-hover:opacity-100"
|
className="text-destructive bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 backdrop-blur-sm duration-200 group-hover:opacity-100"
|
||||||
onClick={onRemoveSignedFieldClick}
|
onClick={onRemoveSignedFieldClick}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import { CheckCircle2, Clock, File } from 'lucide-react';
|
import { CheckCircle2, Clock, File } from 'lucide-react';
|
||||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||||
|
|
||||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { HTMLAttributes, useEffect, useState } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { DateTime, DateTimeFormatOptions } from 'luxon';
|
import type { DateTimeFormatOptions } from 'luxon';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { useLocale } from '@documenso/lib/client-only/providers/locale';
|
import { useLocale } from '@documenso/lib/client-only/providers/locale';
|
||||||
|
|
||||||
|
|||||||
@@ -9,16 +9,18 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
|||||||
documentId: number;
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
|
export const completeDocument = async ({ documentId, meta }: CompleteDocumentActionInput) => {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
if (email.message || email.subject) {
|
if (meta.message || meta.subject || meta.dateFormat || meta.timezone) {
|
||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId,
|
documentId,
|
||||||
subject: email.subject,
|
subject: meta.subject,
|
||||||
message: email.message,
|
message: meta.message,
|
||||||
|
timezone: meta.timezone,
|
||||||
|
dateFormat: meta.dateFormat,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -6983,6 +6983,11 @@
|
|||||||
"isomorphic-fetch": "^3.0.0"
|
"isomorphic-fetch": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vvo/tzdb": {
|
||||||
|
"version": "6.117.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.117.0.tgz",
|
||||||
|
"integrity": "sha512-vZkfoag1kHqItK/zebxT0Fkt3R/zscjgD+Ib7kaAdum0Sz9psXDfVHPW1Benv91d02zPWlLIvZtjBmzX4a+6fw=="
|
||||||
|
},
|
||||||
"node_modules/abbrev": {
|
"node_modules/abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
@@ -20189,6 +20194,7 @@
|
|||||||
"@scure/base": "^1.1.3",
|
"@scure/base": "^1.1.3",
|
||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@upstash/redis": "^1.20.6",
|
"@upstash/redis": "^1.20.6",
|
||||||
|
"@vvo/tzdb": "^6.117.0",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ import {
|
|||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
import {
|
import type { TemplateDocumentInviteProps } from '../template-components/template-document-invite';
|
||||||
TemplateDocumentInvite,
|
import { TemplateDocumentInvite } from '../template-components/template-document-invite';
|
||||||
TemplateDocumentInviteProps,
|
|
||||||
} from '../template-components/template-document-invite';
|
|
||||||
import { TemplateFooter } from '../template-components/template-footer';
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
|
|
||||||
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ReadStatus, Recipient, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
|
import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const getRecipientType = (recipient: Recipient) => {
|
export const getRecipientType = (recipient: Recipient) => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
71
packages/lib/constants/date-formats.ts
Normal file
71
packages/lib/constants/date-formats.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
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: 'YYYYMMDD',
|
||||||
|
label: 'YYYY-MM-DD',
|
||||||
|
value: DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'DDMMYYYY',
|
||||||
|
label: 'DD/MM/YYYY',
|
||||||
|
value: 'dd/MM/yyyy hh:mm a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'MMDDYYYY',
|
||||||
|
label: 'MM/DD/YYYY',
|
||||||
|
value: 'MM/dd/yyyy hh:mm a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'YYYYMMDDHHmm',
|
||||||
|
label: 'YYYY-MM-DD HH:mm',
|
||||||
|
value: 'yyyy-MM-dd HH:mm',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'YYMMDD',
|
||||||
|
label: 'YY-MM-DD',
|
||||||
|
value: 'yy-MM-dd hh:mm a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'YYYYMMDDhhmmss',
|
||||||
|
label: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
value: 'yyyy-MM-dd HH:mm:ss',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'MonthDateYear',
|
||||||
|
label: 'Month Date, Year',
|
||||||
|
value: 'MMMM dd, yyyy hh:mm a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'DayMonthYear',
|
||||||
|
label: 'Day, Month Year',
|
||||||
|
value: 'EEEE, MMMM dd, yyyy hh:mm a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ISO8601',
|
||||||
|
label: 'ISO 8601',
|
||||||
|
value: "yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const convertToLocalSystemFormat = (
|
||||||
|
customText: string,
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parsedDate.isValid) {
|
||||||
|
return 'Invalid date';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDate = parsedDate.toLocal().toFormat(dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT);
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
};
|
||||||
44
packages/lib/constants/time-zones.ts
Normal file
44
packages/lib/constants/time-zones.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { rawTimeZones, timeZonesNames } from '@vvo/tzdb';
|
||||||
|
|
||||||
|
export const TIME_ZONE_DATA = rawTimeZones;
|
||||||
|
|
||||||
|
export const DEFAULT_DOCUMENT_TIME_ZONE = 'Etc/UTC';
|
||||||
|
|
||||||
|
export type TimeZone = {
|
||||||
|
name: string;
|
||||||
|
rawOffsetInMinutes: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const minutesToHours = (minutes: number): string => {
|
||||||
|
const hours = Math.abs(Math.floor(minutes / 60));
|
||||||
|
const min = Math.abs(minutes % 60);
|
||||||
|
const sign = minutes >= 0 ? '+' : '-';
|
||||||
|
|
||||||
|
return `${sign}${String(hours).padStart(2, '0')}:${String(min).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGMTOffsets = (timezones: TimeZone[]): string[] => {
|
||||||
|
const gmtOffsets: string[] = [];
|
||||||
|
|
||||||
|
for (const timezone of timezones) {
|
||||||
|
const offsetValue = minutesToHours(timezone.rawOffsetInMinutes);
|
||||||
|
const gmtText = `(${offsetValue})`;
|
||||||
|
|
||||||
|
gmtOffsets.push(`${timezone.name} ${gmtText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gmtOffsets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const splitTimeZone = (input: string | null): string => {
|
||||||
|
if (input === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const [timeZone] = input.split('(');
|
||||||
|
|
||||||
|
return timeZone.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIME_ZONES_FULL = getGMTOffsets(TIME_ZONE_DATA);
|
||||||
|
|
||||||
|
export const TIME_ZONES = ['Etc/UTC', ...timeZonesNames];
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
||||||
import { compare } from 'bcrypt';
|
import { compare } from 'bcrypt';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { AuthOptions, Session, User } from 'next-auth';
|
import type { AuthOptions, Session, User } from 'next-auth';
|
||||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||||
import GoogleProvider, { GoogleProfile } from 'next-auth/providers/google';
|
import type { GoogleProfile } from 'next-auth/providers/google';
|
||||||
|
import GoogleProvider from 'next-auth/providers/google';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"@scure/base": "^1.1.3",
|
"@scure/base": "^1.1.3",
|
||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@upstash/redis": "^1.20.6",
|
"@upstash/redis": "^1.20.6",
|
||||||
|
"@vvo/tzdb": "^6.117.0",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ export type CreateDocumentMetaOptions = {
|
|||||||
documentId: number;
|
documentId: number;
|
||||||
subject: string;
|
subject: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
timezone: string;
|
||||||
|
dateFormat: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upsertDocumentMeta = async ({
|
export const upsertDocumentMeta = async ({
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
|
timezone,
|
||||||
|
dateFormat,
|
||||||
documentId,
|
documentId,
|
||||||
}: CreateDocumentMetaOptions) => {
|
}: CreateDocumentMetaOptions) => {
|
||||||
return await prisma.documentMeta.upsert({
|
return await prisma.documentMeta.upsert({
|
||||||
@@ -20,11 +24,15 @@ export const upsertDocumentMeta = async ({
|
|||||||
create: {
|
create: {
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
|
dateFormat,
|
||||||
|
timezone,
|
||||||
documentId,
|
documentId,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
|
dateFormat,
|
||||||
|
timezone,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export const duplicateDocumentById = async ({ id, userId }: DuplicateDocumentByI
|
|||||||
select: {
|
select: {
|
||||||
message: true,
|
message: true,
|
||||||
subject: true,
|
subject: true,
|
||||||
|
dateFormat: true,
|
||||||
|
timezone: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export interface GetDocumentMetaByDocumentIdOptions {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDocumentMetaByDocumentId = async ({ id }: GetDocumentMetaByDocumentIdOptions) => {
|
||||||
|
return await prisma.documentMeta.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
documentId: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { SigningStatus, User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
|
import { SigningStatus } from '@documenso/prisma/client';
|
||||||
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { Prisma } from '@prisma/client';
|
import type { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { DateTime } from 'luxon';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
||||||
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
|
||||||
|
|
||||||
export type SignFieldWithTokenOptions = {
|
export type SignFieldWithTokenOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
fieldId: number;
|
fieldId: number;
|
||||||
@@ -50,6 +53,12 @@ export const signFieldWithToken = async ({
|
|||||||
throw new Error(`Field ${fieldId} has no recipientId`);
|
throw new Error(`Field ${fieldId} has no recipientId`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const documentMeta = await prisma.documentMeta.findFirst({
|
||||||
|
where: {
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const isSignatureField =
|
const isSignatureField =
|
||||||
field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE;
|
field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE;
|
||||||
|
|
||||||
@@ -58,7 +67,9 @@ export const signFieldWithToken = async ({
|
|||||||
const typedSignature = isSignatureField && !isBase64 ? value : undefined;
|
const typedSignature = isSignatureField && !isBase64 ? value : undefined;
|
||||||
|
|
||||||
if (field.type === FieldType.DATE) {
|
if (field.type === FieldType.DATE) {
|
||||||
customText = DateTime.now().toFormat('yyyy-MM-dd hh:mm a');
|
customText = DateTime.now()
|
||||||
|
.setZone(documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE)
|
||||||
|
.toFormat(documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.field.update({
|
await prisma.field.update({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const recipientInitials = (text: string) =>
|
export const recipientInitials = (text: string) =>
|
||||||
text
|
text
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DocumentMeta" ADD COLUMN "dateFormat" TEXT DEFAULT 'yyyy-MM-dd hh:mm a',
|
||||||
|
ADD COLUMN "timezone" TEXT DEFAULT 'Etc/UTC';
|
||||||
@@ -159,6 +159,8 @@ model DocumentMeta {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
subject String?
|
subject String?
|
||||||
message String?
|
message String?
|
||||||
|
timezone String? @db.Text @default("Etc/UTC")
|
||||||
|
dateFormat String? @db.Text @default("yyyy-MM-dd hh:mm a")
|
||||||
documentId Int @unique
|
documentId Int @unique
|
||||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Document, DocumentData, DocumentMeta } from '@documenso/prisma/client';
|
import type { Document, DocumentData, DocumentMeta } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type DocumentWithData = Document & {
|
export type DocumentWithData = Document & {
|
||||||
documentData?: DocumentData | null;
|
documentData?: DocumentData | null;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Document, DocumentData, Recipient } from '@documenso/prisma/client';
|
import type { Document, DocumentData, Recipient } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type DocumentWithRecipients = Document & {
|
export type DocumentWithRecipients = Document & {
|
||||||
Recipient: Recipient[];
|
Recipient: Recipient[];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Field, Signature } from '@documenso/prisma/client';
|
import type { Field, Signature } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type FieldWithSignature = Field & {
|
export type FieldWithSignature = Field & {
|
||||||
Signature?: Signature | null;
|
Signature?: Signature | null;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import SuperJSON from 'superjson';
|
|||||||
|
|
||||||
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||||
|
|
||||||
import { TrpcContext } from './context';
|
import type { TrpcContext } from './context';
|
||||||
|
|
||||||
const t = initTRPC.context<TrpcContext>().create({
|
const t = initTRPC.context<TrpcContext>().create({
|
||||||
transformer: SuperJSON,
|
transformer: SuperJSON,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ClassValue, clsx } from 'clsx';
|
import type { ClassValue } from 'clsx';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { Check, ChevronsUpDown } from 'lucide-react';
|
import { Check, ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
import { Role } from '@documenso/prisma/client';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@@ -15,34 +14,31 @@ import {
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||||
|
|
||||||
type ComboboxProps = {
|
type ComboboxProps = {
|
||||||
listValues: string[];
|
className?: string;
|
||||||
onChange: (_values: string[]) => void;
|
options: string[];
|
||||||
|
value: string | null;
|
||||||
|
onChange: (_value: string | null) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Combobox = ({ listValues, onChange }: ComboboxProps) => {
|
const Combobox = ({
|
||||||
|
className,
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
placeholder,
|
||||||
|
}: ComboboxProps) => {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [selectedValues, setSelectedValues] = React.useState<string[]>([]);
|
|
||||||
const dbRoles = Object.values(Role);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
const onOptionSelected = (newValue: string) => {
|
||||||
setSelectedValues(listValues);
|
onChange(newValue === value ? null : newValue);
|
||||||
}, [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);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const placeholderValue = placeholder ?? 'Select an option';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@@ -50,26 +46,28 @@ const Combobox = ({ listValues, onChange }: ComboboxProps) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="w-[200px] justify-between"
|
className={cn('my-2 w-full justify-between', className)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{selectedValues.length > 0 ? selectedValues.join(', ') : 'Select values...'}
|
{value ? value : placeholderValue}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[200px] p-0">
|
|
||||||
|
<PopoverContent className="p-0" side="bottom" align="start">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder={selectedValues.join(', ')} />
|
<CommandInput placeholder={value || placeholderValue} />
|
||||||
|
|
||||||
<CommandEmpty>No value found.</CommandEmpty>
|
<CommandEmpty>No value found.</CommandEmpty>
|
||||||
<CommandGroup>
|
|
||||||
{allRoles.map((value: string, i: number) => (
|
<CommandGroup className="max-h-[250px] overflow-y-auto">
|
||||||
<CommandItem key={i} onSelect={() => handleSelect(value)}>
|
{options.map((option, index) => (
|
||||||
|
<CommandItem key={index} onSelect={() => onOptionSelected(option)}>
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn('mr-2 h-4 w-4', option === value ? 'opacity-100' : 'opacity-0')}
|
||||||
'mr-2 h-4 w-4',
|
|
||||||
selectedValues.includes(value) ? 'opacity-100' : 'opacity-0',
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{value}
|
|
||||||
|
{option}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
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 { Command as CommandPrimitive } from 'cmdk';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
|
|||||||
@@ -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 { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
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 { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@@ -25,7 +26,7 @@ import {
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
import { TAddFieldsFormSchema } from './add-fields.types';
|
import type { TAddFieldsFormSchema } from './add-fields.types';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
@@ -33,7 +34,8 @@ import {
|
|||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './document-flow-root';
|
} from './document-flow-root';
|
||||||
import { FieldItem } from './field-item';
|
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({
|
const fontCaveat = Caveat({
|
||||||
weight: ['500'],
|
weight: ['500'],
|
||||||
|
|||||||
@@ -7,21 +7,23 @@ import { DateTime } from 'luxon';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { match } from 'ts-pattern';
|
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 { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import { Field, FieldType } from '@documenso/prisma/client';
|
import type { Field } from '@documenso/prisma/client';
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
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 { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
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 {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
DocumentFlowFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
} 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 { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
@@ -135,7 +137,7 @@ export const AddSignatureFormPartial = ({
|
|||||||
return match(field.type)
|
return match(field.type)
|
||||||
.with(FieldType.DATE, () => ({
|
.with(FieldType.DATE, () => ({
|
||||||
...field,
|
...field,
|
||||||
customText: DateTime.now().toFormat('yyyy-MM-dd hh:mm a'),
|
customText: DateTime.now().toFormat(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||||
inserted: true,
|
inserted: true,
|
||||||
}))
|
}))
|
||||||
.with(FieldType.EMAIL, () => ({
|
.with(FieldType.EMAIL, () => ({
|
||||||
|
|||||||
@@ -9,21 +9,23 @@ import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
|||||||
|
|
||||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
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 { Button } from '@documenso/ui/primitives/button';
|
||||||
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
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 {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
DocumentFlowFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './document-flow-root';
|
} from './document-flow-root';
|
||||||
import { DocumentFlowStep } from './types';
|
import type { DocumentFlowStep } from './types';
|
||||||
|
|
||||||
export type AddSignersFormProps = {
|
export type AddSignersFormProps = {
|
||||||
documentFlow: DocumentFlowStep;
|
documentFlow: DocumentFlowStep;
|
||||||
|
|||||||
@@ -1,22 +1,41 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { DocumentStatus, Field, Recipient } from '@documenso/prisma/client';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
|
||||||
|
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 { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
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 { Textarea } from '@documenso/ui/primitives/textarea';
|
||||||
|
|
||||||
import { TAddSubjectFormSchema } from './add-subject.types';
|
import { Combobox } from '../combobox';
|
||||||
|
import type { TAddSubjectFormSchema } from './add-subject.types';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
DocumentFlowFormContainerContent,
|
DocumentFlowFormContainerContent,
|
||||||
DocumentFlowFormContainerFooter,
|
DocumentFlowFormContainerFooter,
|
||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './document-flow-root';
|
} from './document-flow-root';
|
||||||
import { DocumentFlowStep } from './types';
|
import type { DocumentFlowStep } from './types';
|
||||||
|
|
||||||
export type AddSubjectFormProps = {
|
export type AddSubjectFormProps = {
|
||||||
documentFlow: DocumentFlowStep;
|
documentFlow: DocumentFlowStep;
|
||||||
@@ -29,27 +48,46 @@ export type AddSubjectFormProps = {
|
|||||||
|
|
||||||
export const AddSubjectFormPartial = ({
|
export const AddSubjectFormPartial = ({
|
||||||
documentFlow,
|
documentFlow,
|
||||||
recipients: _recipients,
|
recipients: recipients,
|
||||||
fields: _fields,
|
fields: fields,
|
||||||
document,
|
document,
|
||||||
numberOfSteps,
|
numberOfSteps,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: AddSubjectFormProps) => {
|
}: AddSubjectFormProps) => {
|
||||||
const {
|
const {
|
||||||
|
control,
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting, touchedFields },
|
||||||
|
getValues,
|
||||||
|
setValue,
|
||||||
} = useForm<TAddSubjectFormSchema>({
|
} = useForm<TAddSubjectFormSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: {
|
meta: {
|
||||||
subject: document.documentMeta?.subject ?? '',
|
subject: document.documentMeta?.subject ?? '',
|
||||||
message: document.documentMeta?.message ?? '',
|
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 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
@@ -65,10 +103,10 @@ export const AddSubjectFormPartial = ({
|
|||||||
// placeholder="Subject"
|
// placeholder="Subject"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...register('email.subject')}
|
{...register('meta.subject')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormErrorMessage className="mt-2" error={errors.email?.subject} />
|
<FormErrorMessage className="mt-2" error={errors.meta?.subject} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -80,14 +118,12 @@ export const AddSubjectFormPartial = ({
|
|||||||
id="message"
|
id="message"
|
||||||
className="bg-background mt-2 h-32 resize-none"
|
className="bg-background mt-2 h-32 resize-none"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...register('email.message')}
|
{...register('meta.message')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormErrorMessage
|
<FormErrorMessage
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
error={
|
error={typeof errors.meta?.message !== 'string' ? errors.meta?.message : undefined}
|
||||||
typeof errors.email?.message !== 'string' ? errors.email?.message : undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -117,6 +153,67 @@ export const AddSubjectFormPartial = ({
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Accordion type="multiple" className="mt-8 border-none">
|
||||||
|
<AccordionItem value="advanced-options" className="border-none">
|
||||||
|
<AccordionTrigger className="mb-2 border-b text-left hover:no-underline">
|
||||||
|
Advanced Options
|
||||||
|
</AccordionTrigger>
|
||||||
|
|
||||||
|
<AccordionContent className="text-muted-foreground -mx-1 flex max-w-prose flex-col px-1 text-sm leading-relaxed">
|
||||||
|
{hasDateField && (
|
||||||
|
<div className="mt-2 flex flex-col">
|
||||||
|
<Label htmlFor="date-format">
|
||||||
|
Date Format <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`meta.dateFormat`}
|
||||||
|
disabled={documentHasBeenSent}
|
||||||
|
render={({ field: { value, onChange, disabled } }) => (
|
||||||
|
<Select value={value} onValueChange={onChange} disabled={disabled}>
|
||||||
|
<SelectTrigger className="bg-background mt-2">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{DATE_FORMATS.map((format) => (
|
||||||
|
<SelectItem key={format.key} value={format.value}>
|
||||||
|
{format.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasDateField && (
|
||||||
|
<div className="mt-4 flex flex-col">
|
||||||
|
<Label htmlFor="time-zone">
|
||||||
|
Time Zone <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`meta.timezone`}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Combobox
|
||||||
|
className="bg-background"
|
||||||
|
options={TIME_ZONES}
|
||||||
|
value={value}
|
||||||
|
onChange={(value) => value && onChange(value)}
|
||||||
|
disabled={documentHasBeenSent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { z } from 'zod';
|
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({
|
export const ZAddSubjectFormSchema = z.object({
|
||||||
email: z.object({
|
meta: z.object({
|
||||||
subject: z.string(),
|
subject: z.string(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||||
|
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
82
packages/ui/primitives/multiselect-combobox.tsx
Normal file
82
packages/ui/primitives/multiselect-combobox.tsx
Normal file
@@ -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<string[]>([]);
|
||||||
|
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 (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className="w-[200px] justify-between"
|
||||||
|
>
|
||||||
|
{selectedValues.length > 0 ? selectedValues.join(', ') : 'Select values...'}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder={selectedValues.join(', ')} />
|
||||||
|
<CommandEmpty>No value found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{allRoles.map((value: string, i: number) => (
|
||||||
|
<CommandItem key={i} onSelect={() => handleSelect(value)}>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
'mr-2 h-4 w-4',
|
||||||
|
selectedValues.includes(value) ? 'opacity-100' : 'opacity-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{value}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { MultiSelectCombobox };
|
||||||
Reference in New Issue
Block a user