feat: migrate nextjs to rr7

This commit is contained in:
David Nguyen
2025-01-02 15:33:37 +11:00
committed by Mythie
parent 9183f668d3
commit 75d7336763
1021 changed files with 60930 additions and 40839 deletions

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';

View File

@ -1,5 +1,3 @@
'use client';
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
const AspectRatio = AspectRatioPrimitive.Root;

View File

@ -1,5 +1,3 @@
'use client';
import { useLayoutEffect, useRef } from 'react';
import { cn } from '../lib/utils';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';

View File

@ -1,5 +1,3 @@
'use client';
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
const Collapsible = CollapsiblePrimitive.Root;

View File

@ -1,7 +1,8 @@
import * as React from 'react';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Check, ChevronDown } from 'lucide-react';
import { cn } from '../lib/utils';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import type { DialogProps } from '@radix-ui/react-dialog';
@ -96,7 +94,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden border-b p-1 last:border-0 [&_[cmdk-group-heading]]:mt-2 [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-normal [&_[cmdk-group-heading]]:opacity-50 ',
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden border-b p-1 last:border-0 [&_[cmdk-group-heading]]:mt-2 [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-normal [&_[cmdk-group-heading]]:opacity-50',
className,
)}
{...props}

View File

@ -1,5 +0,0 @@
export const THEMES_TYPE = {
DARK: 'dark',
LIGHT: 'light',
SYSTEM: 'system'
};

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';

View File

@ -1,4 +1,4 @@
import { Plural, Trans } from '@lingui/macro';
import { Plural, Trans } from '@lingui/react/macro';
import type { Table } from '@tanstack/react-table';
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import { match } from 'ts-pattern';

View File

@ -1,8 +1,6 @@
'use client';
import React, { useMemo } from 'react';
import { Trans } from '@lingui/macro';
import { Trans } from '@lingui/react/macro';
import type {
ColumnDef,
PaginationState,

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';

View File

@ -1,13 +1,11 @@
'use client';
import Link from 'next/link';
import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { AlertTriangle, Plus } from 'lucide-react';
import { useDropzone } from 'react-dropzone';
import { Link } from 'react-router';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
@ -101,7 +99,6 @@ export const DocumentDropzone = ({
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
</motion.div>
<motion.div
className="group-hover:bg-destructive/5 border-muted-foreground/20 group-hover:border-destructive/50 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneDisabledCardCenterVariants}
@ -111,7 +108,6 @@ export const DocumentDropzone = ({
className="text-muted-foreground/20 group-hover:text-destructive h-12 w-12"
/>
</motion.div>
<motion.div
className="group-hover:bg-destructive/2 border-muted-foreground/20 group-hover:border-destructive/10 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneDisabledCardRightVariants}
@ -132,7 +128,6 @@ export const DocumentDropzone = ({
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
</motion.div>
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardCenterVariants}
@ -142,7 +137,6 @@ export const DocumentDropzone = ({
className="text-muted-foreground/20 group-hover:text-documenso h-12 w-12"
/>
</motion.div>
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardRightVariants}
@ -164,7 +158,7 @@ export const DocumentDropzone = ({
{disabled && IS_BILLING_ENABLED() && (
<Button className="hover:bg-warning/80 bg-warning mt-4 w-32" asChild>
<Link href="/settings/billing">
<Link to="/settings/billing">
<Trans>Upgrade</Trans>
</Link>
</Button>

View File

@ -1,12 +1,11 @@
'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Prisma } from '@prisma/client';
import type { Field, Recipient } from '@prisma/client';
import { FieldType, RecipientRole, SendStatus } from '@prisma/client';
import {
CalendarDays,
Check,
@ -41,8 +40,6 @@ import {
canRecipientBeModified,
canRecipientFieldsBeModified,
} from '@documenso/lib/utils/recipients';
import type { Field, Recipient } from '@documenso/prisma/client';
import { FieldType, RecipientRole, SendStatus } from '@documenso/prisma/client';
import { FieldToolTip } from '../../components/field/field-tooltip';
import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors';
@ -70,13 +67,6 @@ import { FieldAdvancedSettings } from './field-item-advanced-settings';
import { MissingSignatureFieldDialog } from './missing-signature-field-dialog';
import { type DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
const MIN_HEIGHT_PX = 12;
const MIN_WIDTH_PX = 36;
@ -508,7 +498,15 @@ export const AddFieldsFormPartial = ({
}, []);
useEffect(() => {
setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]);
const recipientsByRoleToDisplay = recipients.filter(
(recipient) =>
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
);
setSelectedSigner(
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
recipientsByRoleToDisplay[0],
);
}, [recipients]);
const recipientsByRole = useMemo(() => {
@ -517,6 +515,7 @@ export const AddFieldsFormPartial = ({
VIEWER: [],
SIGNER: [],
APPROVER: [],
ASSISTANT: [],
};
recipients.forEach((recipient) => {
@ -529,7 +528,12 @@ export const AddFieldsFormPartial = ({
const recipientsByRoleToDisplay = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][])
.filter(([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER)
.filter(
([role]) =>
role !== RecipientRole.CC &&
role !== RecipientRole.VIEWER &&
role !== RecipientRole.ASSISTANT,
)
.map(
([role, roleRecipients]) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@ -544,12 +548,6 @@ export const AddFieldsFormPartial = ({
);
}, [recipientsByRole]);
const isTypedSignatureEnabled = form.watch('typedSignatureEnabled');
const handleTypedSignatureChange = (value: boolean) => {
form.setValue('typedSignatureEnabled', value, { shouldDirty: true });
};
const handleAdvancedSettings = () => {
setShowAdvancedSettings((prev) => !prev);
};
@ -612,7 +610,7 @@ export const AddFieldsFormPartial = ({
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
'dark:text-black/60': isFieldWithinBounds,
},
selectedField === FieldType.SIGNATURE && fontCaveat.className,
// selectedField === FieldType.SIGNATURE && fontCaveat.className,
)}
style={{
top: coords.y,
@ -687,9 +685,7 @@ export const AddFieldsFormPartial = ({
)}
{!selectedSigner?.email && (
<span className="gradie flex-1 truncate text-left">
{selectedSigner?.email}
</span>
<span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
)}
<ChevronsUpDown className="ml-2 h-4 w-4" />
@ -834,8 +830,7 @@ export const AddFieldsFormPartial = ({
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-lg font-normal',
fontCaveat.className,
'text-muted-foreground group-data-[selected]:text-foreground font-signature flex items-center justify-center gap-x-1.5 text-lg font-normal',
)}
>
<Trans>Signature</Trans>

View File

@ -1,7 +1,7 @@
import { FieldType } from '@prisma/client';
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
export const ZAddFieldsFormSchema = z.object({
fields: z.array(

View File

@ -1,9 +1,9 @@
'use client';
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro';
import { Trans } from '@lingui/react/macro';
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@prisma/client';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
@ -13,8 +13,6 @@ import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import type { TDocument } from '@documenso/lib/types/document';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@documenso/prisma/client';
import {
DocumentGlobalAuthAccessSelect,
DocumentGlobalAuthAccessTooltip,

View File

@ -1,3 +1,4 @@
import { DocumentVisibility } from '@prisma/client';
import { z } from 'zod';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@ -8,7 +9,6 @@ import {
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { DocumentVisibility } from '@documenso/prisma/client';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaTimezoneSchema,

View File

@ -1,24 +1,23 @@
'use client';
import React, { useCallback, useId, useMemo, useRef, useState } from 'react';
import type { DropResult, SensorAPI } from '@hello-pangea/dnd';
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Field, Recipient } from '@prisma/client';
import { DocumentSigningOrder, RecipientRole, SendStatus } from '@prisma/client';
import { motion } from 'framer-motion';
import { GripVerticalIcon, Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useFieldArray, useForm } from 'react-hook-form';
import { prop, sortBy } from 'remeda';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id';
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
import type { Field, Recipient } from '@documenso/prisma/client';
import { DocumentSigningOrder, RecipientRole, SendStatus } from '@documenso/prisma/client';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/recipient-action-auth-select';
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';
@ -41,6 +40,7 @@ import {
DocumentFlowFormContainerStep,
} from './document-flow-root';
import { ShowFieldItem } from './show-field-item';
import { SigningOrderConfirmation } from './signing-order-confirmation';
import type { DocumentFlowStep } from './types';
export type AddSignersFormProps = {
@ -65,9 +65,7 @@ export const AddSignersFormPartial = ({
const { _ } = useLingui();
const { toast } = useToast();
const { remaining } = useLimits();
const { data: session } = useSession();
const user = session?.user;
const { user } = useSession();
const initialId = useId();
const $sensorApi = useRef<SensorAPI | null>(null);
@ -123,6 +121,7 @@ export const AddSignersFormPartial = ({
}, [recipients, form]);
const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings);
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
const {
setValue,
@ -134,6 +133,10 @@ export const AddSignersFormPartial = ({
const watchedSigners = watch('signers');
const isSigningOrderSequential = watch('signingOrder') === DocumentSigningOrder.SEQUENTIAL;
const hasAssistantRole = useMemo(() => {
return watchedSigners.some((signer) => signer.role === RecipientRole.ASSISTANT);
}, [watchedSigners]);
const normalizeSigningOrders = (signers: typeof watchedSigners) => {
return signers
.sort((a, b) => (a.signingOrder ?? 0) - (b.signingOrder ?? 0))
@ -198,10 +201,12 @@ export const AddSignersFormPartial = ({
return;
}
removeSigner(index);
const updatedSigners = signers.filter((_, idx) => idx !== index);
form.setValue('signers', normalizeSigningOrders(updatedSigners));
const formStateIndex = form.getValues('signers').findIndex((s) => s.formId === signer.formId);
if (formStateIndex !== -1) {
removeSigner(formStateIndex);
const updatedSigners = form.getValues('signers').filter((s) => s.formId !== signer.formId);
form.setValue('signers', normalizeSigningOrders(updatedSigners));
}
};
const onAddSelfSigner = () => {
@ -233,6 +238,7 @@ export const AddSignersFormPartial = ({
const items = Array.from(watchedSigners);
const [reorderedSigner] = items.splice(result.source.index, 1);
// Find next valid position
let insertIndex = result.destination.index;
while (insertIndex < items.length && !canRecipientBeModified(items[insertIndex].nativeId)) {
insertIndex++;
@ -240,126 +246,116 @@ export const AddSignersFormPartial = ({
items.splice(insertIndex, 0, reorderedSigner);
const updatedSigners = items.map((item, index) => ({
...item,
signingOrder: !canRecipientBeModified(item.nativeId) ? item.signingOrder : index + 1,
const updatedSigners = items.map((signer, index) => ({
...signer,
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : index + 1,
}));
updatedSigners.forEach((item, index) => {
const keys: (keyof typeof item)[] = [
'formId',
'nativeId',
'email',
'name',
'role',
'signingOrder',
'actionAuth',
];
keys.forEach((key) => {
form.setValue(`signers.${index}.${key}` as const, item[key]);
});
});
form.setValue('signers', updatedSigners);
const currentLength = form.getValues('signers').length;
if (currentLength > updatedSigners.length) {
for (let i = updatedSigners.length; i < currentLength; i++) {
form.unregister(`signers.${i}`);
}
const lastSigner = updatedSigners[updatedSigners.length - 1];
if (lastSigner.role === RecipientRole.ASSISTANT) {
toast({
title: _(msg`Warning: Assistant as last signer`),
description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
}
await form.trigger('signers');
},
[form, canRecipientBeModified, watchedSigners],
[form, canRecipientBeModified, watchedSigners, toast],
);
const triggerDragAndDrop = useCallback(
(fromIndex: number, toIndex: number) => {
if (!$sensorApi.current) {
const handleRoleChange = useCallback(
(index: number, role: RecipientRole) => {
const currentSigners = form.getValues('signers');
const signingOrder = form.getValues('signingOrder');
// Handle parallel to sequential conversion for assistants
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
toast({
title: _(msg`Signing order is enabled.`),
description: _(msg`You cannot add assistants when signing order is disabled.`),
variant: 'destructive',
});
return;
}
const draggableId = signers[fromIndex].id;
const updatedSigners = currentSigners.map((signer, idx) => ({
...signer,
role: idx === index ? role : signer.role,
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : idx + 1,
}));
const preDrag = $sensorApi.current.tryGetLock(draggableId);
form.setValue('signers', updatedSigners);
if (!preDrag) {
return;
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
toast({
title: _(msg`Warning: Assistant as last signer`),
description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
}
const drag = preDrag.snapLift();
setTimeout(() => {
// Move directly to the target index
if (fromIndex < toIndex) {
for (let i = fromIndex; i < toIndex; i++) {
drag.moveDown();
}
} else {
for (let i = fromIndex; i > toIndex; i--) {
drag.moveUp();
}
}
setTimeout(() => {
drag.drop();
}, 500);
}, 0);
},
[signers],
);
const updateSigningOrders = useCallback(
(newIndex: number, oldIndex: number) => {
const updatedSigners = form.getValues('signers').map((signer, index) => {
if (index === oldIndex) {
return { ...signer, signingOrder: newIndex + 1 };
} else if (index >= newIndex && index < oldIndex) {
return {
...signer,
signingOrder: !canRecipientBeModified(signer.nativeId)
? signer.signingOrder
: (signer.signingOrder ?? index + 1) + 1,
};
} else if (index <= newIndex && index > oldIndex) {
return {
...signer,
signingOrder: !canRecipientBeModified(signer.nativeId)
? signer.signingOrder
: Math.max(1, (signer.signingOrder ?? index + 1) - 1),
};
}
return signer;
});
updatedSigners.forEach((signer, index) => {
form.setValue(`signers.${index}.signingOrder`, signer.signingOrder);
});
},
[form, canRecipientBeModified],
[form, toast, canRecipientBeModified],
);
const handleSigningOrderChange = useCallback(
(index: number, newOrderString: string) => {
const newOrder = parseInt(newOrderString, 10);
if (!newOrderString.trim()) {
const trimmedOrderString = newOrderString.trim();
if (!trimmedOrderString) {
return;
}
if (Number.isNaN(newOrder)) {
form.setValue(`signers.${index}.signingOrder`, index + 1);
const newOrder = Number(trimmedOrderString);
if (!Number.isInteger(newOrder) || newOrder < 1) {
return;
}
const newIndex = newOrder - 1;
if (index !== newIndex) {
updateSigningOrders(newIndex, index);
triggerDragAndDrop(index, newIndex);
const currentSigners = form.getValues('signers');
const signer = currentSigners[index];
// Remove signer from current position and insert at new position
const remainingSigners = currentSigners.filter((_, idx) => idx !== index);
const newPosition = Math.min(Math.max(0, newOrder - 1), currentSigners.length - 1);
remainingSigners.splice(newPosition, 0, signer);
const updatedSigners = remainingSigners.map((s, idx) => ({
...s,
signingOrder: !canRecipientBeModified(s.nativeId) ? s.signingOrder : idx + 1,
}));
form.setValue('signers', updatedSigners);
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
toast({
title: _(msg`Warning: Assistant as last signer`),
description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
}
},
[form, triggerDragAndDrop, updateSigningOrders],
[form, canRecipientBeModified, toast],
);
const handleSigningOrderDisable = useCallback(() => {
setShowSigningOrderConfirmation(false);
const currentSigners = form.getValues('signers');
const updatedSigners = currentSigners.map((signer) => ({
...signer,
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
}));
form.setValue('signers', updatedSigners);
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
}, [form]);
return (
<>
<DocumentFlowFormContainerHeader
@ -384,11 +380,16 @@ export const AddSignersFormPartial = ({
{...field}
id="signingOrder"
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
onCheckedChange={(checked) =>
onCheckedChange={(checked) => {
if (!checked && hasAssistantRole) {
setShowSigningOrderConfirmation(true);
return;
}
field.onChange(
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
)
}
);
}}
disabled={isSubmitting || hasDocumentBeenSent}
/>
</FormControl>
@ -613,7 +614,11 @@ export const AddSignersFormPartial = ({
<FormControl>
<RecipientRoleSelect
{...field}
onValueChange={field.onChange}
isAssistantEnabled={isSigningOrderSequential}
onValueChange={(value) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
handleRoleChange(index, value as RecipientRole)
}
disabled={
snapshot.isDragging ||
isSubmitting ||
@ -710,6 +715,12 @@ export const AddSignersFormPartial = ({
)}
</Form>
</AnimateGenericFadeInOut>
<SigningOrderConfirmation
open={showSigningOrderConfirmation}
onOpenChange={setShowSigningOrderConfirmation}
onConfirm={handleSigningOrderDisable}
/>
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter>

View File

@ -1,10 +1,10 @@
import { msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
import { DocumentSigningOrder, RecipientRole } from '.prisma/client';
export const ZAddSignersFormSchema = z
.object({

View File

@ -1,8 +1,9 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Field, Recipient } from '@prisma/client';
import { DocumentDistributionMethod, DocumentStatus, RecipientRole } from '@prisma/client';
import { AnimatePresence, motion } from 'framer-motion';
import { useForm } from 'react-hook-form';
@ -10,12 +11,6 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
import type { TDocument } from '@documenso/lib/types/document';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import type { Field, Recipient } from '@documenso/prisma/client';
import {
DocumentDistributionMethod,
DocumentStatus,
RecipientRole,
} from '@documenso/prisma/client';
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
@ -84,11 +79,13 @@ export const AddSubjectFormPartial = ({
? msg`Resend`
: msg`Send`,
[DocumentStatus.COMPLETED]: msg`Update`,
[DocumentStatus.REJECTED]: msg`Update`,
},
[DocumentDistributionMethod.NONE]: {
[DocumentStatus.DRAFT]: msg`Generate Links`,
[DocumentStatus.PENDING]: msg`View Document`,
[DocumentStatus.COMPLETED]: msg`View Document`,
[DocumentStatus.REJECTED]: msg`View Document`,
},
};

View File

@ -1,9 +1,8 @@
import { DocumentDistributionMethod } from '@prisma/client';
import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { DocumentDistributionMethod } from '.prisma/client';
export const ZAddSubjectFormSchema = z.object({
meta: z.object({
subject: z.string(),

View File

@ -20,15 +20,13 @@ export const CheckboxField = ({ field }: CheckboxFieldProps) => {
}
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
return (
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} signerEmail={field.signerEmail} />
);
return <FieldIcon fieldMeta={field.fieldMeta} type={field.type} />;
}
return (
<div className="flex flex-col gap-y-1">
{!parsedFieldMeta?.values ? (
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} signerEmail={field.signerEmail} />
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
) : (
parsedFieldMeta.values.map((item: { value: string; checked: boolean }, index: number) => (
<div key={index} className="flex items-center gap-x-1.5">

View File

@ -20,15 +20,13 @@ export const RadioField = ({ field }: RadioFieldProps) => {
}
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
return (
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} signerEmail={field.signerEmail} />
);
return <FieldIcon fieldMeta={field.fieldMeta} type={field.type} />;
}
return (
<div className="flex flex-col gap-y-2">
{!parsedFieldMeta?.values ? (
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} signerEmail={field.signerEmail} />
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
) : (
<RadioGroup className="gap-y-1">
{parsedFieldMeta.values?.map((item, index) => (

View File

@ -1,11 +1,10 @@
'use client';
import type { HTMLAttributes } from 'react';
import React from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { cn } from '../../lib/utils';

View File

@ -1,4 +1,5 @@
import { Trans } from '@lingui/macro';
import { Trans } from '@lingui/react/macro';
import { FieldType } from '@prisma/client';
import {
CalendarDays,
CheckSquare,
@ -12,15 +13,12 @@ import {
} from 'lucide-react';
import type { TFieldMetaSchema as FieldMetaType } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
import { cn } from '../../lib/utils';
type FieldIconProps = {
fieldMeta: FieldMetaType;
type: FieldType;
signerEmail?: string;
fontCaveatClassName?: string;
};
const fieldIcons = {
@ -35,18 +33,12 @@ const fieldIcons = {
[FieldType.DROPDOWN]: { icon: ChevronDown, label: 'Select' },
};
export const FieldIcon = ({
fieldMeta,
type,
signerEmail,
fontCaveatClassName,
}: FieldIconProps) => {
export const FieldIcon = ({ fieldMeta, type }: FieldIconProps) => {
if (type === 'SIGNATURE' || type === 'FREE_SIGNATURE') {
return (
<div
className={cn(
'text-field-card-foreground flex items-center justify-center gap-x-1 text-[clamp(0.575rem,25cqw,1.2rem)]',
fontCaveatClassName,
'text-field-card-foreground font-signature flex items-center justify-center gap-x-1 text-[clamp(0.575rem,25cqw,1.2rem)]',
)}
>
<Trans>Signature</Trans>
@ -59,10 +51,10 @@ export const FieldIcon = ({
if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) {
if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) {
label =
fieldMeta.text.length > 10 ? fieldMeta.text.substring(0, 10) + '...' : fieldMeta.text;
fieldMeta.text.length > 20 ? fieldMeta.text.substring(0, 20) + '...' : fieldMeta.text;
} else if (fieldMeta.label) {
label =
fieldMeta.label.length > 10 ? fieldMeta.label.substring(0, 10) + '...' : fieldMeta.label;
fieldMeta.label.length > 20 ? fieldMeta.label.substring(0, 20) + '...' : fieldMeta.label;
} else {
label = fieldIcons[type]?.label;
}

View File

@ -1,10 +1,9 @@
'use client';
import { forwardRef, useEffect, useState } from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { FieldType } from '@prisma/client';
import { match } from 'ts-pattern';
import {
@ -21,7 +20,6 @@ import {
type TTextFieldMeta as TextFieldMeta,
ZFieldMetaSchema,
} from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
import { useToast } from '@documenso/ui/primitives/use-toast';
import type { FieldFormType } from './add-fields';
@ -71,21 +69,25 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
return {
type: 'initials',
fontSize: 14,
textAlign: 'left',
};
case FieldType.NAME:
return {
type: 'name',
fontSize: 14,
textAlign: 'left',
};
case FieldType.EMAIL:
return {
type: 'email',
fontSize: 14,
textAlign: 'left',
};
case FieldType.DATE:
return {
type: 'date',
fontSize: 14,
textAlign: 'left',
};
case FieldType.TEXT:
return {
@ -97,6 +99,7 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
fontSize: 14,
required: false,
readOnly: false,
textAlign: 'left',
};
case FieldType.NUMBER:
return {
@ -110,6 +113,7 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
required: false,
readOnly: false,
fontSize: 14,
textAlign: 'left',
};
case FieldType.RADIO:
return {

View File

@ -1,9 +1,6 @@
'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
import { FieldType } from '@prisma/client';
import { CopyPlus, Settings2, Trash } from 'lucide-react';
import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd';
@ -22,13 +19,6 @@ import type { TDocumentFlowFormSchema } from './types';
type Field = TDocumentFlowFormSchema['fields'][0];
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
export type FieldItemProps = {
field: Field;
passive?: boolean;
@ -185,11 +175,35 @@ export const FieldItem = ({
() => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta),
[field.fieldMeta],
);
const radioHasValues = useMemo(
() => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta),
[field.fieldMeta],
);
const hasCheckedValues = (fieldMeta: TFieldMetaSchema, type: FieldType) => {
if (!fieldMeta || (type !== FieldType.RADIO && type !== FieldType.CHECKBOX)) {
return false;
}
if (type === FieldType.RADIO) {
const parsed = ZRadioFieldMeta.parse(fieldMeta);
return parsed.values?.some((value) => value.checked) ?? false;
}
if (type === FieldType.CHECKBOX) {
const parsed = ZCheckboxFieldMeta.parse(fieldMeta);
return parsed.values?.some((value) => value.checked) ?? false;
}
return false;
};
const fieldHasCheckedValues = useMemo(
() => hasCheckedValues(field.fieldMeta, field.type),
[field.fieldMeta, field.type],
);
const fixedSize = checkBoxHasValues || radioHasValues;
return createPortal(
@ -229,6 +243,21 @@ export const FieldItem = ({
onMove?.(d.node);
}}
>
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
field.fieldMeta?.label && (
<div
className={cn(
'absolute -top-16 left-0 right-0 rounded-md p-2 text-center text-xs text-gray-700',
{
'bg-foreground/5 border-primary border': !fieldHasCheckedValues,
'bg-documenso-200 border-primary border': fieldHasCheckedValues,
},
)}
>
{field.fieldMeta.label}
</div>
)}
<div
className={cn(
'relative flex h-full w-full items-center justify-center bg-white',
@ -254,12 +283,7 @@ export const FieldItem = ({
.with('CHECKBOX', () => <CheckboxField field={field} />)
.with('RADIO', () => <RadioField field={field} />)
.otherwise(() => (
<FieldIcon
fieldMeta={field.fieldMeta}
type={field.type}
signerEmail={field.signerEmail}
fontCaveatClassName={fontCaveat.className}
/>
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
))}
{!hideRecipients && (

View File

@ -1,9 +1,8 @@
'use client';
import { useEffect, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
@ -126,6 +125,18 @@ export const CheckboxFieldAdvancedSettings = ({
return (
<div className="flex flex-col gap-4">
<div className="mb-2">
<Label>
<Trans>Label</Trans>
</Label>
<Input
id="label"
className="bg-background mt-2"
placeholder={_(msg`Field label`)}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
/>
</div>
<div className="flex flex-row items-center gap-x-4">
<div className="flex w-2/3 flex-col">
<Label>

View File

@ -1,10 +1,18 @@
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { validateFields as validateDateFields } from '@documenso/lib/advanced-fields-validation/validate-fields';
import { type TDateFieldMeta as DateFieldMeta } from '@documenso/lib/types/field-meta';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
type DateFieldAdvancedSettingsProps = {
fieldState: DateFieldMeta;
@ -66,6 +74,27 @@ export const DateFieldAdvancedSettings = ({
max={96}
/>
</div>
<div>
<Label>
<Trans>Text Align</Trans>
</Label>
<Select
value={fieldState.textAlign}
onValueChange={(value) => handleInput('textAlign', value)}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="center">Center</SelectItem>
<SelectItem value="right">Right</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
};

View File

@ -1,9 +1,8 @@
'use client';
import { useEffect, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
@ -105,8 +104,12 @@ export const DropdownFieldAdvancedSettings = ({
<Trans>Select default option</Trans>
</Label>
<Select
defaultValue={defaultValue}
value={defaultValue}
onValueChange={(val) => {
if (!val) {
return;
}
setDefaultValue(val);
handleFieldChange('defaultValue', val);
}}
@ -172,7 +175,7 @@ export const DropdownFieldAdvancedSettings = ({
/>
<button
type="button"
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => removeValue(index)}
>
<Trash className="h-5 w-5" />

View File

@ -1,10 +1,18 @@
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { validateFields as validateEmailFields } from '@documenso/lib/advanced-fields-validation/validate-fields';
import { type TEmailFieldMeta as EmailFieldMeta } from '@documenso/lib/types/field-meta';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
type EmailFieldAdvancedSettingsProps = {
fieldState: EmailFieldMeta;
@ -48,6 +56,27 @@ export const EmailFieldAdvancedSettings = ({
max={96}
/>
</div>
<div>
<Label>
<Trans>Text Align</Trans>
</Label>
<Select
value={fieldState.textAlign}
onValueChange={(value) => handleInput('textAlign', value)}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="center">Center</SelectItem>
<SelectItem value="right">Right</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
};

View File

@ -1,11 +1,14 @@
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { validateFields as validateInitialsFields } from '@documenso/lib/advanced-fields-validation/validate-fields';
import { type TInitialsFieldMeta as InitialsFieldMeta } from '@documenso/lib/types/field-meta';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../select';
type InitialsFieldAdvancedSettingsProps = {
fieldState: InitialsFieldMeta;
handleFieldChange: (key: keyof InitialsFieldMeta, value: string | boolean) => void;
@ -48,6 +51,27 @@ export const InitialsFieldAdvancedSettings = ({
max={96}
/>
</div>
<div>
<Label>
<Trans>Text Align</Trans>
</Label>
<Select
value={fieldState.textAlign}
onValueChange={(value) => handleInput('textAlign', value)}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="center">Center</SelectItem>
<SelectItem value="right">Right</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
};

View File

@ -1,10 +1,18 @@
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { validateFields as validateNameFields } from '@documenso/lib/advanced-fields-validation/validate-fields';
import { type TNameFieldMeta as NameFieldMeta } from '@documenso/lib/types/field-meta';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
type NameFieldAdvancedSettingsProps = {
fieldState: NameFieldMeta;
@ -48,6 +56,27 @@ export const NameFieldAdvancedSettings = ({
max={96}
/>
</div>
<div>
<Label>
<Trans>Text Align</Trans>
</Label>
<Select
value={fieldState.textAlign}
onValueChange={(value) => handleInput('textAlign', value)}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="center">Center</SelectItem>
<SelectItem value="right">Right</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
};

View File

@ -1,9 +1,8 @@
'use client';
import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
@ -38,12 +37,12 @@ export const NumberFieldAdvancedSettings = ({
const [showValidation, setShowValidation] = useState(false);
const handleInput = (field: keyof NumberFieldMeta, value: string | boolean) => {
const userValue = field === 'value' ? value : fieldState.value ?? 0;
const userValue = field === 'value' ? value : (fieldState.value ?? 0);
const userMinValue = field === 'minValue' ? Number(value) : Number(fieldState.minValue ?? 0);
const userMaxValue = field === 'maxValue' ? Number(value) : Number(fieldState.maxValue ?? 0);
const readOnly = field === 'readOnly' ? Boolean(value) : Boolean(fieldState.readOnly);
const required = field === 'required' ? Boolean(value) : Boolean(fieldState.required);
const numberFormat = field === 'numberFormat' ? String(value) : fieldState.numberFormat ?? '';
const numberFormat = field === 'numberFormat' ? String(value) : (fieldState.numberFormat ?? '');
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
const valueErrors = validateNumberField(String(userValue), {
@ -135,6 +134,27 @@ export const NumberFieldAdvancedSettings = ({
/>
</div>
<div>
<Label>
<Trans>Text Align</Trans>
</Label>
<Select
value={fieldState.textAlign}
onValueChange={(value) => handleInput('textAlign', value)}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="center">Center</SelectItem>
<SelectItem value="right">Right</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-2 flex flex-col gap-4">
<div className="flex flex-row items-center gap-2">
<Switch

View File

@ -1,8 +1,8 @@
'use client';
import { useEffect, useState } from 'react';
import { Trans } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
@ -27,6 +27,8 @@ export const RadioFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: RadioFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const [showValidation, setShowValidation] = useState(false);
const [values, setValues] = useState(
fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }],
@ -102,6 +104,18 @@ export const RadioFieldAdvancedSettings = ({
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div>
<Label>
<Trans>Label</Trans>
</Label>
<Input
id="label"
className="bg-background mt-2"
placeholder={_(msg`Field label`)}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
/>
</div>
<div className="flex flex-row items-center gap-2">
<Switch
className="bg-background"

View File

@ -1,10 +1,18 @@
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import { type TTextFieldMeta as TextFieldMeta } from '@documenso/lib/types/field-meta';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { Switch } from '@documenso/ui/primitives/switch';
import { Textarea } from '@documenso/ui/primitives/textarea';
@ -22,7 +30,7 @@ export const TextFieldAdvancedSettings = ({
const { _ } = useLingui();
const handleInput = (field: keyof TextFieldMeta, value: string | boolean) => {
const text = field === 'text' ? String(value) : fieldState.text ?? '';
const text = field === 'text' ? String(value) : (fieldState.text ?? '');
const limit =
field === 'characterLimit' ? Number(value) : Number(fieldState.characterLimit ?? 0);
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
@ -112,6 +120,33 @@ export const TextFieldAdvancedSettings = ({
/>
</div>
<div>
<Label>
<Trans>Text Align</Trans>
</Label>
<Select
value={fieldState.textAlign}
onValueChange={(value) => {
if (!value) {
return;
}
handleInput('textAlign', value);
}}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="center">Center</SelectItem>
<SelectItem value="right">Right</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-4 flex flex-col gap-4">
<div className="flex flex-row items-center gap-2">
<Switch

View File

@ -1,6 +1,4 @@
'use client';
import { Trans } from '@lingui/macro';
import { Trans } from '@lingui/react/macro';
import { DialogClose } from '@radix-ui/react-dialog';
import { Button } from '@documenso/ui/primitives/button';

View File

@ -1,6 +1,6 @@
import { useState } from 'react';
import { Trans } from '@lingui/macro';
import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import type { ButtonProps } from '../button';

View File

@ -1,39 +1,24 @@
'use client';
import { Caveat } from 'next/font/google';
import { useLingui } from '@lingui/react';
import type { Prisma } from '@prisma/client';
import { FieldType, type Prisma } from '@prisma/client';
import { createPortal } from 'react-dom';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { FieldType } from '@documenso/prisma/client';
import { cn } from '../../lib/utils';
import { Card, CardContent } from '../card';
import { FRIENDLY_FIELD_TYPE } from './types';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
export type ShowFieldItemProps = {
field: Prisma.FieldGetPayload<null>;
recipients: Prisma.RecipientGetPayload<null>[];
};
export const ShowFieldItem = ({ field, recipients }: ShowFieldItemProps) => {
export const ShowFieldItem = ({ field }: ShowFieldItemProps) => {
const { _ } = useLingui();
const coords = useFieldPageCoords(field);
const signerEmail =
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '';
return createPortal(
<div
className={cn('pointer-events-none absolute z-10 opacity-75')}
@ -48,7 +33,7 @@ export const ShowFieldItem = ({ field, recipients }: ShowFieldItemProps) => {
<CardContent
className={cn(
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-0 text-[clamp(0.575rem,1.8cqw,1.2rem)] leading-none',
field.type === FieldType.SIGNATURE && fontCaveat.className,
field.type === FieldType.SIGNATURE && 'font-signature',
)}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}

View File

@ -0,0 +1,40 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@documenso/ui/primitives/alert-dialog';
export type SigningOrderConfirmationProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
};
export function SigningOrderConfirmation({
open,
onOpenChange,
onConfirm,
}: SigningOrderConfirmationProps) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Warning</AlertDialogTitle>
<AlertDialogDescription>
You have an assistant role on the signers list, removing the signing order will change
the assistant role to signer.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>Proceed</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@ -1,9 +1,9 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { FieldType } from '@prisma/client';
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
export const ZDocumentFlowFormSchema = z.object({
title: z.string().min(1),

View File

@ -1,106 +0,0 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button } from './button';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './dialog';
import { Form, FormControl, FormField, FormItem, FormMessage } from './form/form';
import { Input } from './input';
const ZPasswordDialogFormSchema = z.object({
password: z.string(),
});
type TPasswordDialogFormSchema = z.infer<typeof ZPasswordDialogFormSchema>;
type PasswordDialogProps = {
open: boolean;
onOpenChange: (_open: boolean) => void;
defaultPassword?: string;
onPasswordSubmit?: (password: string) => void;
isError?: boolean;
};
export const PasswordDialog = ({
open,
onOpenChange,
defaultPassword,
onPasswordSubmit,
isError,
}: PasswordDialogProps) => {
const { _ } = useLingui();
const form = useForm<TPasswordDialogFormSchema>({
defaultValues: {
password: defaultPassword ?? '',
},
resolver: zodResolver(ZPasswordDialogFormSchema),
});
const onFormSubmit = ({ password }: TPasswordDialogFormSchema) => {
onPasswordSubmit?.(password);
};
useEffect(() => {
if (isError) {
form.setError('password', {
type: 'manual',
message: _(msg`The password you have entered is incorrect. Please try again.`),
});
}
}, [form, isError]);
return (
<Dialog open={open}>
<DialogContent className="w-full max-w-md">
<DialogHeader>
<DialogTitle>
<Trans>Password Required</Trans>
</DialogTitle>
<DialogDescription className="text-muted-foreground">
<Trans>
This document is password protected. Please enter the password to view the document.
</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset className="flex flex-wrap items-start justify-between gap-4">
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem className="relative flex-1">
<FormControl>
<Input
type="password"
className="bg-background"
placeholder={_(msg`Enter password`)}
autoComplete="off"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div>
<Button>
<Trans>Submit</Trans>
</Button>
</div>
</fieldset>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';

View File

@ -1,5 +1,3 @@
'use client';
import React, { useEffect, useState } from 'react';
export type ElementVisibleProps = {

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';

View File

@ -1,9 +1,8 @@
'use client';
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { VariantProps, cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';

View File

@ -1,26 +0,0 @@
'use client';
import dynamic from 'next/dynamic';
import { Trans } from '@lingui/macro';
import { Loader } from 'lucide-react';
export const LazyPDFViewer = dynamic(async () => import('./pdf-viewer'), {
ssr: false,
loading: () => (
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
<Loader className="text-documenso h-12 w-12 animate-spin" />
<p className="text-muted-foreground mt-4">
<Trans>Loading document...</Trans>
</p>
</div>
),
});
/**
* LazyPDFViewer variant with no loader.
*/
export const LazyPDFViewerNoLoader = dynamic(async () => import('./pdf-viewer'), {
ssr: false,
});

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as MenubarPrimitive from '@radix-ui/react-menubar';

View File

@ -1,10 +1,8 @@
'use client';
import * as React from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { AnimatePresence } from 'framer-motion';
import { Check, ChevronsUpDown, Loader, XIcon } from 'lucide-react';
@ -24,7 +22,7 @@ type ComboBoxOption<T = OptionValue> = {
};
type MultiSelectComboboxProps<T = OptionValue> = {
emptySelectionPlaceholder?: React.ReactNode | string;
emptySelectionPlaceholder?: React.ReactElement | string;
enableClearAllButton?: boolean;
loading?: boolean;
inputPlaceholder?: MessageDescriptor;

View File

@ -4,7 +4,8 @@ import { Eye, EyeOff } from 'lucide-react';
import { cn } from '../lib/utils';
import { Button } from './button';
import { Input, InputProps } from './input';
import type { InputProps } from './input';
import { Input } from './input';
const PasswordInput = React.forwardRef<HTMLInputElement, Omit<InputProps, 'type'>>(
({ className, ...props }, ref) => {

View File

@ -1,22 +1,19 @@
'use client';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentData } from '@prisma/client';
import { Loader } from 'lucide-react';
import { type PDFDocumentProxy, PasswordResponses } from 'pdfjs-dist';
import { type PDFDocumentProxy } from 'pdfjs-dist';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import { match } from 'ts-pattern';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import type { DocumentData } from '@documenso/prisma/client';
import { cn } from '../lib/utils';
import { PasswordDialog } from './document-password-dialog';
import { useToast } from './use-toast';
export type LoadedPDFDocument = PDFDocumentProxy;
@ -24,7 +21,10 @@ export type LoadedPDFDocument = PDFDocumentProxy;
/**
* This imports the worker from the `pdfjs-dist` package.
*/
pdfjs.GlobalWorkerOptions.workerSrc = `/pdf.worker.min.js`;
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
export type OnPDFViewerPageClick = (_event: {
pageNumber: number;
@ -49,8 +49,6 @@ const PDFLoader = () => (
export type PDFViewerProps = {
className?: string;
documentData: DocumentData;
password?: string | null;
onPasswordSubmit?: (password: string) => void | Promise<void>;
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
onPageClick?: OnPDFViewerPageClick;
[key: string]: unknown;
@ -59,8 +57,6 @@ export type PDFViewerProps = {
export const PDFViewer = ({
className,
documentData,
password: defaultPassword,
onPasswordSubmit,
onDocumentLoad,
onPageClick,
...props
@ -70,11 +66,7 @@ export const PDFViewer = ({
const $el = useRef<HTMLDivElement>(null);
const passwordCallbackRef = useRef<((password: string | null) => void) | null>(null);
const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false);
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
const [isPasswordError, setIsPasswordError] = useState(false);
const [documentBytes, setDocumentBytes] = useState<Uint8Array | null>(null);
const [width, setWidth] = useState(0);
@ -190,21 +182,6 @@ export const PDFViewer = ({
className={cn('w-full overflow-hidden rounded', {
'h-[80vh] max-h-[60rem]': numPages === 0,
})}
onPassword={(callback, reason) => {
// If the document already has a password, we don't need to ask for it again.
if (defaultPassword && reason !== PasswordResponses.INCORRECT_PASSWORD) {
callback(defaultPassword);
return;
}
setIsPasswordModalOpen(true);
passwordCallbackRef.current = callback;
match(reason)
.with(PasswordResponses.NEED_PASSWORD, () => setIsPasswordError(false))
.with(PasswordResponses.INCORRECT_PASSWORD, () => setIsPasswordError(true));
}}
onLoadSuccess={(d) => onDocumentLoaded(d)}
// Uploading a invalid document causes an error which doesn't appear to be handled by the `error` prop.
// Therefore we add some additional custom error handling.
@ -263,19 +240,6 @@ export const PDFViewer = ({
</div>
))}
</PDFDocument>
<PasswordDialog
open={isPasswordModalOpen}
onOpenChange={setIsPasswordModalOpen}
onPasswordSubmit={(password) => {
passwordCallbackRef.current?.(password);
setIsPasswordModalOpen(false);
void onPasswordSubmit?.(password);
}}
isError={isPasswordError}
/>
</>
)}
</div>

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import { OTPInput, OTPInputContext } from 'input-otp';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as ProgressPrimitive from '@radix-ui/react-progress';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
@ -19,18 +17,18 @@ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, children: _children, ...props }, ref) => {
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'border-input ring-offset-background focus:ring-ring h-4 w-4 rounded-full border focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'border-primary text-primary focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border shadow focus:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="fill-primary text-primary h-2.5 w-2.5" />
<Circle className="fill-primary h-2.5 w-2.5" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);

View File

@ -1,10 +1,10 @@
import { BadgeCheck, Copy, Eye, PencilLine } from 'lucide-react';
import type { RecipientRole } from '.prisma/client';
import type { RecipientRole } from '@prisma/client';
import { BadgeCheck, Copy, Eye, PencilLine, User } from 'lucide-react';
export const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
SIGNER: <PencilLine className="h-4 w-4" />,
APPROVER: <BadgeCheck className="h-4 w-4" />,
CC: <Copy className="h-4 w-4" />,
VIEWER: <Eye className="h-4 w-4" />,
ASSISTANT: <User className="h-4 w-4" />,
};

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';

View File

@ -1,4 +1,4 @@
import {
import type {
MouseEvent as ReactMouseEvent,
PointerEvent as ReactPointerEvent,
TouchEvent as ReactTouchEvent,

View File

@ -1,11 +1,7 @@
'use client';
import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
import { Trans } from '@lingui/macro';
import { Trans } from '@lingui/react/macro';
import { Undo2, Upload } from 'lucide-react';
import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand';
@ -24,13 +20,6 @@ import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper';
import { Point } from './point';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
const DPI = 2;
const isBase64Image = (value: string) => value.startsWith('data:image/png;base64,');
@ -282,7 +271,11 @@ export const SignaturePad = ({
event.preventDefault();
}
onMouseUp(event, false);
if (isPressed) {
onMouseUp(event, true);
} else {
onMouseUp(event, false);
}
};
const onClearClick = () => {
@ -312,7 +305,7 @@ export const SignaturePad = ({
if (ctx) {
const canvasWidth = $el.current.width;
const canvasHeight = $el.current.height;
const fontFamily = String(fontCaveat.style.fontFamily);
const fontFamily = 'Caveat';
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.textAlign = 'center';
@ -479,6 +472,7 @@ export const SignaturePad = ({
})}
>
<canvas
data-testid="signature-pad"
ref={$el}
className={cn(
'relative block',

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';

View File

@ -1,11 +1,10 @@
'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Field, Recipient } from '@prisma/client';
import { FieldType, RecipientRole, SendStatus } from '@prisma/client';
import {
CalendarDays,
CheckSquare,
@ -31,8 +30,6 @@ import {
} from '@documenso/lib/types/field-meta';
import { nanoid } from '@documenso/lib/universal/id';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import type { Field, Recipient } from '@documenso/prisma/client';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -64,13 +61,6 @@ import { Form, FormControl, FormField, FormItem, FormLabel } from '../form/form'
import { useStep } from '../stepper';
import type { TAddTemplateFieldsFormSchema } from './add-template-fields.types';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
const MIN_HEIGHT_PX = 12;
const MIN_WIDTH_PX = 36;
@ -438,6 +428,7 @@ export const AddTemplateFieldsFormPartial = ({
VIEWER: [],
SIGNER: [],
APPROVER: [],
ASSISTANT: [],
};
recipients.forEach((recipient) => {
@ -447,10 +438,25 @@ export const AddTemplateFieldsFormPartial = ({
return recipientsByRole;
}, [recipients]);
useEffect(() => {
const recipientsByRoleToDisplay = recipients.filter(
(recipient) =>
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
);
setSelectedSigner(
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
recipientsByRoleToDisplay[0],
);
}, [recipients]);
const recipientsByRoleToDisplay = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER,
([role]) =>
role !== RecipientRole.CC &&
role !== RecipientRole.VIEWER &&
role !== RecipientRole.ASSISTANT,
);
}, [recipientsByRole]);
@ -699,8 +705,7 @@ export const AddTemplateFieldsFormPartial = ({
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-lg font-normal',
fontCaveat.className,
'text-muted-foreground group-data-[selected]:text-foreground font-signature flex items-center justify-center gap-x-1.5 text-lg font-normal',
)}
>
<Trans>Signature</Trans>

View File

@ -1,7 +1,7 @@
import { FieldType } from '@prisma/client';
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
export const ZAddTemplateFieldsFormSchema = z.object({
fields: z.array(

View File

@ -1,27 +1,21 @@
'use client';
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import type { DropResult, SensorAPI } from '@hello-pangea/dnd';
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TemplateDirectLink } from '@prisma/client';
import { DocumentSigningOrder, type Field, type Recipient, RecipientRole } from '@prisma/client';
import { motion } from 'framer-motion';
import { GripVerticalIcon, Link2Icon, Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id';
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
import type { TemplateDirectLink } from '@documenso/prisma/client';
import {
DocumentSigningOrder,
type Field,
type Recipient,
RecipientRole,
} from '@documenso/prisma/client';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/recipient-action-auth-select';
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';
@ -29,6 +23,7 @@ import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input';
import { toast } from '@documenso/ui/primitives/use-toast';
import { Checkbox } from '../checkbox';
import {
@ -39,6 +34,7 @@ import {
DocumentFlowFormContainerStep,
} from '../document-flow/document-flow-root';
import { ShowFieldItem } from '../document-flow/show-field-item';
import { SigningOrderConfirmation } from '../document-flow/signing-order-confirmation';
import type { DocumentFlowStep } from '../document-flow/types';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
import { useStep } from '../stepper';
@ -71,9 +67,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const $sensorApi = useRef<SensorAPI | null>(null);
const { _ } = useLingui();
const { data: session } = useSession();
const user = session?.user;
const { user } = useSession();
const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(() =>
recipients.length > 1 ? recipients.length + 1 : 2,
@ -174,8 +168,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const onAddPlaceholderSelfRecipient = () => {
appendSigner({
formId: nanoid(12),
name: user?.name ?? '',
email: user?.email ?? '',
name: user.name ?? '',
email: user.email ?? '',
role: RecipientRole.SIGNER,
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
});
@ -213,41 +207,30 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const items = Array.from(watchedSigners);
const [reorderedSigner] = items.splice(result.source.index, 1);
const insertIndex = result.destination.index;
items.splice(insertIndex, 0, reorderedSigner);
const updatedSigners = items.map((item, index) => ({
...item,
const updatedSigners = items.map((signer, index) => ({
...signer,
signingOrder: index + 1,
}));
updatedSigners.forEach((item, index) => {
const keys: (keyof typeof item)[] = [
'formId',
'nativeId',
'email',
'name',
'role',
'signingOrder',
'actionAuth',
];
keys.forEach((key) => {
form.setValue(`signers.${index}.${key}` as const, item[key]);
});
});
form.setValue('signers', updatedSigners);
const currentLength = form.getValues('signers').length;
if (currentLength > updatedSigners.length) {
for (let i = updatedSigners.length; i < currentLength; i++) {
form.unregister(`signers.${i}`);
}
const lastSigner = updatedSigners[updatedSigners.length - 1];
if (lastSigner.role === RecipientRole.ASSISTANT) {
toast({
title: _(msg`Warning: Assistant as last signer`),
description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
}
await form.trigger('signers');
},
[form, watchedSigners],
[form, watchedSigners, toast],
);
const triggerDragAndDrop = useCallback(
@ -308,26 +291,94 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const handleSigningOrderChange = useCallback(
(index: number, newOrderString: string) => {
const newOrder = parseInt(newOrderString, 10);
if (!newOrderString.trim()) {
const trimmedOrderString = newOrderString.trim();
if (!trimmedOrderString) {
return;
}
if (Number.isNaN(newOrder)) {
form.setValue(`signers.${index}.signingOrder`, index + 1);
const newOrder = Number(trimmedOrderString);
if (!Number.isInteger(newOrder) || newOrder < 1) {
return;
}
const newIndex = newOrder - 1;
if (index !== newIndex) {
updateSigningOrders(newIndex, index);
triggerDragAndDrop(index, newIndex);
const currentSigners = form.getValues('signers');
const signer = currentSigners[index];
// Remove signer from current position and insert at new position
const remainingSigners = currentSigners.filter((_, idx) => idx !== index);
const newPosition = Math.min(Math.max(0, newOrder - 1), currentSigners.length - 1);
remainingSigners.splice(newPosition, 0, signer);
const updatedSigners = remainingSigners.map((s, idx) => ({
...s,
signingOrder: idx + 1,
}));
form.setValue('signers', updatedSigners);
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
toast({
title: _(msg`Warning: Assistant as last signer`),
description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
}
},
[form, triggerDragAndDrop, updateSigningOrders],
[form, toast],
);
const handleRoleChange = useCallback(
(index: number, role: RecipientRole) => {
const currentSigners = form.getValues('signers');
const signingOrder = form.getValues('signingOrder');
// Handle parallel to sequential conversion for assistants
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
toast({
title: _(msg`Signing order is enabled.`),
description: _(msg`You cannot add assistants when signing order is disabled.`),
variant: 'destructive',
});
return;
}
const updatedSigners = currentSigners.map((signer, idx) => ({
...signer,
role: idx === index ? role : signer.role,
signingOrder: idx + 1,
}));
form.setValue('signers', updatedSigners);
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
toast({
title: _(msg`Warning: Assistant as last signer`),
description: _(
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
),
});
}
},
[form, toast],
);
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
const handleSigningOrderDisable = useCallback(() => {
setShowSigningOrderConfirmation(false);
const currentSigners = form.getValues('signers');
const updatedSigners = currentSigners.map((signer) => ({
...signer,
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
}));
form.setValue('signers', updatedSigners);
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
}, [form]);
return (
<>
<DocumentFlowFormContainerHeader
@ -353,11 +404,19 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
{...field}
id="signingOrder"
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
onCheckedChange={(checked) =>
onCheckedChange={(checked) => {
if (
!checked &&
watchedSigners.some((s) => s.role === RecipientRole.ASSISTANT)
) {
setShowSigningOrderConfirmation(true);
return;
}
field.onChange(
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
)
}
);
}}
disabled={isSubmitting}
/>
</FormControl>
@ -556,7 +615,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<FormControl>
<RecipientRoleSelect
{...field}
onValueChange={field.onChange}
onValueChange={(value) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
handleRoleChange(index, value as RecipientRole)
}
disabled={isSubmitting}
hideCCRecipients={isSignerDirectRecipient(signer)}
/>
@ -677,6 +739,12 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
onGoNextClick={() => void onFormSubmit()}
/>
</DocumentFlowFormContainerFooter>
<SigningOrderConfirmation
open={showSigningOrderConfirmation}
onOpenChange={setShowSigningOrderConfirmation}
onConfirm={handleSigningOrderDisable}
/>
</>
);
};

View File

@ -1,7 +1,7 @@
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/client';
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';

View File

@ -1,10 +1,10 @@
'use client';
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentDistributionMethod, type Field, type Recipient } from '@prisma/client';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
@ -16,8 +16,6 @@ import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TTemplate } from '@documenso/lib/types/template';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import { DocumentDistributionMethod, type Field, type Recipient } from '@documenso/prisma/client';
import type { TDocumentMetaDateFormat } from '@documenso/trpc/server/document-router/schema';
import {
DocumentGlobalAuthAccessSelect,

View File

@ -1,3 +1,5 @@
import { DocumentDistributionMethod } from '@prisma/client';
import { DocumentVisibility } from '@prisma/client';
import { z } from 'zod';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@ -9,14 +11,12 @@ import {
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { DocumentVisibility } from '@documenso/prisma/client';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaTimezoneSchema,
} from '@documenso/trpc/server/document-router/schema';
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
import { DocumentDistributionMethod } from '.prisma/client';
export const ZAddTemplateSettingsFormSchema = z.object({
title: z.string().trim().min(1, { message: "Title can't be empty" }),

View File

@ -1,22 +1,20 @@
import { motion } from 'framer-motion';
import { Monitor, MoonStar, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Theme, useTheme } from 'remix-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 [theme, setTheme] = useTheme();
const isMounted = useIsMounted();
return (
<div className="bg-muted flex items-center gap-x-1 rounded-full p-1">
<button
className="text-muted-foreground relative z-10 flex h-8 w-8 items-center justify-center rounded-full"
onClick={() => setTheme(THEMES_TYPE.LIGHT)}
onClick={() => setTheme(Theme.LIGHT)}
>
{isMounted && theme === THEMES_TYPE.LIGHT && (
{isMounted && theme === Theme.LIGHT && (
<motion.div
className="bg-background absolute inset-0 rounded-full mix-blend-color-burn"
layoutId="selected-theme"
@ -27,9 +25,9 @@ export const ThemeSwitcher = () => {
<button
className="text-muted-foreground relative z-10 flex h-8 w-8 items-center justify-center rounded-full"
onClick={() => setTheme(THEMES_TYPE.DARK)}
onClick={() => setTheme(Theme.DARK)}
>
{isMounted && theme === THEMES_TYPE.DARK && (
{isMounted && theme === Theme.DARK && (
<motion.div
className="bg-background absolute inset-0 rounded-full mix-blend-exclusion"
layoutId="selected-theme"
@ -41,9 +39,9 @@ export const ThemeSwitcher = () => {
<button
className="text-muted-foreground relative z-10 flex h-8 w-8 items-center justify-center rounded-full"
onClick={() => setTheme(THEMES_TYPE.SYSTEM)}
onClick={() => setTheme(null)}
>
{isMounted && theme === THEMES_TYPE.SYSTEM && (
{isMounted && theme === null && (
<motion.div
className="bg-background absolute inset-0 rounded-full mix-blend-exclusion"
layoutId="selected-theme"

View File

@ -1,5 +1,3 @@
'use client';
import {
Toast,
ToastClose,

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as TogglePrimitive from '@radix-ui/react-toggle';

View File

@ -1,5 +1,3 @@
'use client';
import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';