mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 03:01:59 +10:00
feat: migrate nextjs to rr7
This commit is contained in:
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
type AnimateGenericFadeInOutProps = {
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Button } from '../primitives/button';
|
||||
import { Card, CardContent } from '../primitives/card';
|
||||
|
||||
type CallToActionProps = {
|
||||
className?: string;
|
||||
utmSource?: string;
|
||||
};
|
||||
|
||||
export const CallToAction = ({ className, utmSource = 'generic-cta' }: CallToActionProps) => {
|
||||
return (
|
||||
<Card spotlight className={className}>
|
||||
<CardContent className="flex flex-col items-center justify-center p-12">
|
||||
<h2 className="text-center text-2xl font-bold">Looking for the managed solution?</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center leading-normal">
|
||||
You can get started with Documenso in minutes. We handle the infrastructure, so you can
|
||||
focus on signing documents.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
className="focus-visible:ring-ring ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90text-sm mt-8 inline-flex items-center justify-center rounded-full border font-medium no-underline transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
variant="default"
|
||||
size="lg"
|
||||
asChild
|
||||
>
|
||||
<Link href={`https://app.documenso.com/signup?utm_source=${utmSource}`} target="_blank">
|
||||
Get started
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
11
packages/ui/components/client-only.tsx
Normal file
11
packages/ui/components/client-only.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const ClientOnly = async ({ children }: { children: React.ReactNode }) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
return mounted ? children : null;
|
||||
};
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { msg } from '@lingui/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import {
|
||||
@ -25,8 +24,16 @@ export const LanguageSwitcherDialog = ({ open, setOpen }: LanguageSwitcherDialog
|
||||
const setLanguage = async (lang: string) => {
|
||||
setOpen(false);
|
||||
|
||||
await dynamicActivate(i18n, lang);
|
||||
await switchI18NLanguage(lang);
|
||||
await dynamicActivate(lang);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('lang', lang);
|
||||
|
||||
await fetch('/api/locale', {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -1,24 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { DocumentData } from '@prisma/client';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import type { DocumentData } from '@documenso/prisma/client';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Dialog, DialogOverlay, DialogPortal } from '../../primitives/dialog';
|
||||
import { LazyPDFViewerNoLoader } from '../../primitives/lazy-pdf-viewer';
|
||||
import { Dialog, DialogOverlay, DialogPortal, DialogTrigger } from '../../primitives/dialog';
|
||||
import PDFViewer from '../../primitives/pdf-viewer';
|
||||
|
||||
export type DocumentDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
documentData: DocumentData;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
/**
|
||||
* A dialog which renders the provided document.
|
||||
*/
|
||||
export default function DocumentDialog({ documentData, ...props }: DocumentDialogProps) {
|
||||
export default function DocumentDialog({ trigger, documentData, ...props }: DocumentDialogProps) {
|
||||
const [documentLoaded, setDocumentLoaded] = useState(false);
|
||||
|
||||
const onDocumentLoad = () => {
|
||||
@ -30,6 +28,12 @@ export default function DocumentDialog({ documentData, ...props }: DocumentDialo
|
||||
<DialogPortal>
|
||||
<DialogOverlay className="bg-black/80" />
|
||||
|
||||
{trigger && (
|
||||
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
|
||||
{trigger}
|
||||
</DialogTrigger>
|
||||
)}
|
||||
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
'animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 pointer-events-none fixed z-50 h-screen w-screen overflow-y-auto px-2 py-14 opacity-0 transition-opacity lg:py-32',
|
||||
@ -39,7 +43,7 @@ export default function DocumentDialog({ documentData, ...props }: DocumentDialo
|
||||
)}
|
||||
onClick={() => props.onOpenChange?.(false)}
|
||||
>
|
||||
<LazyPDFViewerNoLoader
|
||||
<PDFViewer
|
||||
className="mx-auto w-full max-w-3xl xl:max-w-5xl"
|
||||
documentData={documentData}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentData } from '@prisma/client';
|
||||
import { Download } from 'lucide-react';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import type { DocumentData } from '@documenso/prisma/client';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { Button } from '../../primitives/button';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import type { TDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import React, { forwardRef } 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 { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import React, { forwardRef } 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 { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
export const DocumentSendEmailMessageHelper = () => {
|
||||
return (
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import React, { 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 { Copy, Sparkles } from 'lucide-react';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { Field } from '@prisma/client';
|
||||
import { TooltipArrow } from '@radix-ui/react-tooltip';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
@ -12,7 +13,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '../..//primitives/tooltip';
|
||||
import type { Field } from '.prisma/client';
|
||||
|
||||
const tooltipVariants = cva('font-semibold', {
|
||||
variants: {
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { Field } from '@prisma/client';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Card, CardContent } from '../../primitives/card';
|
||||
@ -31,7 +29,8 @@ const getCardClassNames = (
|
||||
checkBoxOrRadio: boolean,
|
||||
cardClassName?: string,
|
||||
) => {
|
||||
const baseClasses = 'field-card-container relative z-20 h-full w-full transition-all';
|
||||
const baseClasses =
|
||||
'field--FieldRootContainer field-card-container relative z-20 h-full w-full transition-all';
|
||||
|
||||
const insertedClasses =
|
||||
'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none';
|
||||
@ -141,6 +140,7 @@ export function FieldRootContainer({ field, children, cardClassName }: FieldCont
|
||||
<Card
|
||||
id={`field-${field.id}`}
|
||||
ref={ref}
|
||||
data-field-type={field.type}
|
||||
data-inserted={field.inserted ? 'true' : 'false'}
|
||||
className={cardClassNames}
|
||||
>
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import 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 type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { ROLE_ICONS } from '@documenso/ui/primitives/recipient-role-icons';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export type RecipientRoleSelectProps = SelectProps & {
|
||||
hideCCRecipients?: boolean;
|
||||
isAssistantEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSelectProps>(
|
||||
({ hideCCRecipients, ...props }, ref) => (
|
||||
({ hideCCRecipients, isAssistantEnabled = true, ...props }, ref) => (
|
||||
<Select {...props}>
|
||||
<SelectTrigger ref={ref} className="bg-background w-[50px] p-2">
|
||||
{/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */}
|
||||
@ -110,6 +111,42 @@ export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSe
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
|
||||
<SelectItem
|
||||
value={RecipientRole.ASSISTANT}
|
||||
disabled={!isAssistantEnabled}
|
||||
className={cn(
|
||||
!isAssistantEnabled &&
|
||||
'cursor-not-allowed opacity-50 data-[disabled]:pointer-events-auto',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.ASSISTANT]}</span>
|
||||
<Trans>Can prepare</Trans>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||
<p>
|
||||
{isAssistantEnabled ? (
|
||||
<Trans>
|
||||
The recipient can prepare the document for later signers by pre-filling
|
||||
suggest values.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Assistant role is only available when the document is in sequential signing
|
||||
mode.
|
||||
</Trans>
|
||||
)}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
),
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { StaticImageData } from 'next/image';
|
||||
import Image from 'next/image';
|
||||
|
||||
import type { Signature } from '@prisma/client';
|
||||
import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import type { Signature } from '@documenso/prisma/client';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
import { Card, CardContent } from '../primitives/card';
|
||||
|
||||
@ -17,7 +11,7 @@ export type SigningCardProps = {
|
||||
className?: string;
|
||||
name: string;
|
||||
signature?: Signature;
|
||||
signingCelebrationImage?: StaticImageData;
|
||||
signingCelebrationImage?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -213,7 +207,7 @@ const SigningCardContent = ({ className, name, signature }: SigningCardContentPr
|
||||
};
|
||||
|
||||
type SigningCardImageProps = {
|
||||
signingCelebrationImage: StaticImageData;
|
||||
signingCelebrationImage: string;
|
||||
};
|
||||
|
||||
const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) => {
|
||||
@ -233,7 +227,7 @@ const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) =>
|
||||
duration: 0.5,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
<img
|
||||
src={signingCelebrationImage}
|
||||
alt="background pattern"
|
||||
className="w-full dark:brightness-150 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
@ -241,7 +235,6 @@ const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) =>
|
||||
mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
|
||||
WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
|
||||
}}
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@ -17,8 +17,8 @@ export const VerifiedIcon: LucideIcon = forwardRef(
|
||||
<g id="badge, verified, award">
|
||||
<path
|
||||
id="Icon"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.5457 2.89094C11.5779 1.70302 13.4223 1.70302 14.4545 2.89094L15.2585 3.81628C15.3917 3.96967 15.5947 4.04354 15.7954 4.0117L17.0061 3.81965C18.5603 3.57309 19.9732 4.75869 20.0003 6.33214L20.0214 7.55778C20.0249 7.76096 20.1329 7.94799 20.3071 8.05261L21.358 8.6837C22.7071 9.49389 23.0274 11.3103 22.0368 12.5331L21.2651 13.4855C21.1372 13.6434 21.0997 13.8561 21.1659 14.0482L21.5652 15.2072C22.0779 16.695 21.1557 18.2923 19.6109 18.5922L18.4075 18.8258C18.208 18.8646 18.0426 19.0034 17.9698 19.1931L17.5308 20.3376C16.9672 21.8069 15.234 22.4378 13.8578 21.6745L12.7858 21.08C12.6081 20.9814 12.3921 20.9814 12.2144 21.08L11.1424 21.6745C9.76623 22.4378 8.033 21.8069 7.4694 20.3376L7.03038 19.1931C6.9576 19.0034 6.79216 18.8646 6.59268 18.8258L5.38932 18.5922C3.84448 18.2923 2.92224 16.695 3.43495 15.2072L3.83431 14.0482C3.90052 13.8561 3.86302 13.6434 3.7351 13.4855L2.96343 12.5331C1.97279 11.3103 2.29307 9.49389 3.64218 8.6837L4.69306 8.05261C4.86728 7.94799 4.97526 7.76096 4.97875 7.55778L4.99985 6.33214C5.02694 4.75869 6.43987 3.57309 7.99413 3.81965L9.20481 4.0117C9.40551 4.04354 9.60845 3.96967 9.74173 3.81628L10.5457 2.89094ZM15.7072 11.2071C16.0977 10.8166 16.0977 10.1834 15.7072 9.79289C15.3167 9.40237 14.6835 9.40237 14.293 9.79289L11.5001 12.5858L10.7072 11.7929C10.3167 11.4024 9.68351 11.4024 9.29298 11.7929C8.90246 12.1834 8.90246 12.8166 9.29298 13.2071L10.4394 14.3536C11.0252 14.9393 11.975 14.9393 12.5608 14.3536L15.7072 11.2071Z"
|
||||
fill={color}
|
||||
/>
|
||||
|
||||
@ -29,8 +29,8 @@
|
||||
"@documenso/lib": "*",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@hookform/resolvers": "^3.3.0",
|
||||
"@lingui/macro": "^4.11.3",
|
||||
"@lingui/react": "^4.11.3",
|
||||
"@lingui/macro": "^5.2.0",
|
||||
"@lingui/react": "^5.2.0",
|
||||
"@radix-ui/react-accordion": "^1.1.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.3",
|
||||
"@radix-ui/react-aspect-ratio": "^1.0.2",
|
||||
@ -65,7 +65,7 @@
|
||||
"framer-motion": "^10.12.8",
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.2",
|
||||
"next": "14.2.6",
|
||||
"perfect-freehand": "^1.2.0",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"react": "^18",
|
||||
"react-colorful": "^5.6.1",
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root;
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
export const THEMES_TYPE = {
|
||||
DARK: 'dark',
|
||||
LIGHT: 'light',
|
||||
SYSTEM: 'system'
|
||||
};
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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])}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export type ElementVisibleProps = {
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as MenubarPrimitive from '@radix-ui/react-menubar';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { OTPInput, OTPInputContext } from 'input-otp';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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" />,
|
||||
};
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {
|
||||
import type {
|
||||
MouseEvent as ReactMouseEvent,
|
||||
PointerEvent as ReactPointerEvent,
|
||||
TouchEvent as ReactTouchEvent,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as SwitchPrimitives from '@radix-ui/react-switch';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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" }),
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as TogglePrimitive from '@radix-ui/react-toggle';
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
|
||||
Reference in New Issue
Block a user