feat: add envelope editor

This commit is contained in:
David Nguyen
2025-10-12 23:35:54 +11:00
parent bf89bc781b
commit 0da8e7dbc6
307 changed files with 24657 additions and 3681 deletions

View File

@ -1,27 +1,25 @@
import * as React from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
import { cn } from '../lib/utils';
export type CardProps = React.HTMLAttributes<HTMLDivElement> & {
spotlight?: boolean;
gradient?: boolean;
degrees?: number;
/**
* Not sure if this is needed, but added a toggle so it defaults to true since that was how it was before.
*
* This is required to be set false if you want drag drop to work within this element.
*/
backdropBlur?: boolean;
};
const Card = React.forwardRef<HTMLDivElement, CardProps>(
({ className, children, gradient = false, spotlight = false, degrees = 120, ...props }, ref) => {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top);
};
(
{ className, children, gradient = false, degrees = 120, backdropBlur = true, ...props },
ref,
) => {
return (
<div
ref={ref}
@ -32,8 +30,9 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
} as React.CSSProperties
}
className={cn(
'bg-background text-foreground group relative rounded-lg border-2 backdrop-blur-[2px]',
'bg-background text-foreground group relative rounded-lg border-2',
{
'backdrop-blur-[2px]': backdropBlur,
'gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.primary.DEFAULT/50%)_5%,theme(colors.border/80%)_30%)]':
gradient,
'dark:gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.primary.DEFAULT/70%)_5%,theme(colors.border/80%)_30%)]':
@ -44,23 +43,8 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
},
className,
)}
onMouseMove={handleMouseMove}
{...props}
>
{spotlight && (
<motion.div
className="pointer-events-none absolute -inset-[2px] rounded-lg opacity-0 transition duration-300 group-hover:opacity-100"
style={{
background: useMotionTemplate`
radial-gradient(
300px circle at ${mouseX}px ${mouseY}px,
hsl(var(--primary) / 7%),
transparent 80%
)
`,
}}
/>
)}
{children}
</div>
);

View File

@ -26,9 +26,11 @@ import { Card, CardContent } from './card';
export type DocumentDropzoneProps = {
className?: string;
allowMultiple?: boolean;
disabled?: boolean;
disabledHeading?: MessageDescriptor;
disabledMessage?: MessageDescriptor;
onDrop?: (_file: File) => void | Promise<void>;
onDrop?: (_file: File[]) => void | Promise<void>;
onDropRejected?: () => void | Promise<void>;
type?: 'document' | 'template';
[key: string]: unknown;
@ -36,9 +38,11 @@ export type DocumentDropzoneProps = {
export const DocumentDropzone = ({
className,
allowMultiple,
onDrop,
onDropRejected,
disabled,
disabledHeading,
disabledMessage = msg`You cannot upload documents at this time.`,
type = 'document',
...props
@ -51,11 +55,11 @@ export const DocumentDropzone = ({
accept: {
'application/pdf': ['.pdf'],
},
multiple: false,
multiple: allowMultiple,
disabled,
onDrop: ([acceptedFile]) => {
if (acceptedFile && onDrop) {
void onDrop(acceptedFile);
onDrop: (acceptedFiles) => {
if (acceptedFiles.length > 0 && onDrop) {
void onDrop(acceptedFiles);
}
},
onDropRejected: () => {
@ -67,7 +71,9 @@ export const DocumentDropzone = ({
});
const heading = {
document: disabled ? msg`You have reached your document limit.` : msg`Add a document`,
document: disabled
? disabledHeading || msg`You have reached your document limit.`
: msg`Add a document`,
template: msg`Upload Template Document`,
};
@ -153,7 +159,7 @@ export const DocumentDropzone = ({
<input {...getInputProps()} />
<p className="text-foreground mt-8 font-medium">{_(heading[type])}</p>
<p className="text-foreground mt-6 font-medium">{_(heading[type])}</p>
<p className="text-muted-foreground/80 mt-1 text-center text-sm">
{_(disabled ? disabledMessage : msg`Drag & drop your PDF here.`)}

View File

@ -19,7 +19,6 @@ import {
} from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { prop, sortBy } from 'remeda';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
@ -551,29 +550,6 @@ export const AddFieldsFormPartial = ({
return recipientsByRole;
}, [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 !== RecipientRole.ASSISTANT,
)
.map(
([role, roleRecipients]) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
[
role,
sortBy(
roleRecipients,
[(r) => r.signingOrder || Number.MAX_SAFE_INTEGER, 'asc'],
[prop('id'), 'asc'],
),
] as [RecipientRole, Recipient[]],
);
}, [recipientsByRole]);
const handleAdvancedSettings = () => {
setShowAdvancedSettings((prev) => !prev);
};

View File

@ -10,11 +10,11 @@ import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaTimezoneSchema,
} from '@documenso/trpc/server/document-router/schema';
} from '@documenso/lib/types/document-meta';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
export const ZAddSettingsFormSchema = z.object({
title: z

View File

@ -2,14 +2,17 @@ export const numberFormatValues = [
{
label: '123,456,789.00',
value: '123,456,789.00',
regex: /^(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d{1,2})?$/,
},
{
label: '123.456.789,00',
value: '123.456.789,00',
regex: /^(?:\d{1,3}(?:\.\d{3})*|\d+)(?:,\d{1,2})?$/,
},
{
label: '123456,789.00',
value: '123456,789.00',
regex: /^(?:\d+)(?:,\d{1,3}(?:\.\d{1,2})?)?$/,
},
];

View File

@ -102,7 +102,7 @@ export const NumberFieldAdvancedSettings = ({
<Trans>Number format</Trans>
</Label>
<Select
value={fieldState.numberFormat}
value={fieldState.numberFormat ?? ''}
onValueChange={(val) => handleInput('numberFormat', val)}
>
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
@ -199,7 +199,7 @@ export const NumberFieldAdvancedSettings = ({
id="minValue"
className="bg-background mt-2"
placeholder="E.g. 0"
value={fieldState.minValue}
value={fieldState.minValue ?? ''}
onChange={(e) => handleInput('minValue', e.target.value)}
/>
</div>
@ -211,7 +211,7 @@ export const NumberFieldAdvancedSettings = ({
id="maxValue"
className="bg-background mt-2"
placeholder="E.g. 100"
value={fieldState.maxValue}
value={fieldState.maxValue ?? ''}
onChange={(e) => handleInput('maxValue', e.target.value)}
/>
</div>

View File

@ -20,9 +20,9 @@ export type DocumentDropzoneProps = {
disabled?: boolean;
loading?: boolean;
disabledMessage?: MessageDescriptor;
onDrop?: (_file: File) => void | Promise<void>;
onDrop?: (_files: File[]) => void | Promise<void>;
onDropRejected?: () => void | Promise<void>;
type?: 'document' | 'template';
type?: 'document' | 'template' | 'envelope';
[key: string]: unknown;
};
@ -48,11 +48,12 @@ export const DocumentDropzone = ({
accept: {
'application/pdf': ['.pdf'],
},
multiple: false,
multiple: type === 'envelope',
disabled,
onDrop: ([acceptedFile]) => {
if (acceptedFile && onDrop) {
void onDrop(acceptedFile);
maxFiles: 10, // Todo: Envelopes - Use claims. And also update other places where this is used.
onDrop: (acceptedFiles) => {
if (acceptedFiles.length > 0 && onDrop) {
void onDrop(acceptedFiles);
}
},
onDropRejected: () => {
@ -66,6 +67,7 @@ export const DocumentDropzone = ({
const heading = {
document: msg`Upload Document`,
template: msg`Upload Template Document`,
envelope: msg`Envelope (beta)`,
};
if (disabled && IS_BILLING_ENABLED()) {

View File

@ -48,7 +48,7 @@ const PDFLoader = () => (
export type PDFViewerProps = {
className?: string;
documentData: DocumentData;
documentData: Pick<DocumentData, 'type' | 'data'>;
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
onPageClick?: OnPDFViewerPageClick;
[key: string]: unknown;

View File

@ -21,6 +21,7 @@ export interface RecipientSelectorProps {
selectedRecipient: Recipient | null;
onSelectedRecipientChange: (recipient: Recipient) => void;
recipients: Recipient[];
align?: 'center' | 'end' | 'start';
}
export const RecipientSelector = ({
@ -28,6 +29,7 @@ export const RecipientSelector = ({
selectedRecipient,
onSelectedRecipientChange,
recipients,
align = 'start',
}: RecipientSelectorProps) => {
const { _ } = useLingui();
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
@ -83,7 +85,7 @@ export const RecipientSelector = ({
recipients.findIndex((r) => r.id === selectedRecipient?.id),
0,
),
).base,
).comboxBoxTrigger,
className,
)}
>
@ -101,8 +103,8 @@ export const RecipientSelector = ({
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command value={selectedRecipient?.email}>
<PopoverContent className="p-0" align={align}>
<Command value={selectedRecipient ? selectedRecipient.id.toString() : undefined}>
<CommandInput />
<CommandEmpty>
@ -149,7 +151,7 @@ export const RecipientSelector = ({
>
<span
className={cn('text-foreground/70 truncate', {
'text-foreground/80': recipient === selectedRecipient,
'text-foreground/80': recipient.id === selectedRecipient?.id,
})}
>
{recipient.name && (
@ -164,10 +166,10 @@ export const RecipientSelector = ({
<div className="ml-auto flex items-center justify-center">
{recipient.sendStatus !== SendStatus.SENT ? (
<Check
aria-hidden={recipient !== selectedRecipient}
aria-hidden={recipient.id !== selectedRecipient?.id}
className={cn('h-4 w-4 flex-shrink-0', {
'opacity-0': recipient !== selectedRecipient,
'opacity-100': recipient === selectedRecipient,
'opacity-0': recipient.id !== selectedRecipient?.id,
'opacity-100': recipient.id === selectedRecipient?.id,
})}
/>
) : (

View File

@ -19,11 +19,11 @@ import {
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TDocumentMetaDateFormat } from '@documenso/lib/types/document-meta';
import type { TTemplate } from '@documenso/lib/types/template';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import type { TDocumentMetaDateFormat } from '@documenso/trpc/server/document-router/schema';
import {
DocumentGlobalAuthAccessSelect,
DocumentGlobalAuthAccessTooltip,
@ -216,7 +216,12 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} maxLength={255} onBlur={handleAutoSave} />
<Input
className="bg-background"
{...field}
maxLength={255}
onBlur={handleAutoSave}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -624,7 +629,12 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} maxLength={255} onBlur={handleAutoSave} />
<Input
className="bg-background"
{...field}
maxLength={255}
onBlur={handleAutoSave}
/>
</FormControl>
<FormMessage />
@ -715,7 +725,12 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} maxLength={255} onBlur={handleAutoSave} />
<Input
className="bg-background"
{...field}
maxLength={255}
onBlur={handleAutoSave}
/>
</FormControl>
<FormMessage />

View File

@ -12,11 +12,11 @@ import {
ZDocumentActionAuthTypesSchema,
} 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 {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaTimezoneSchema,
} from '@documenso/trpc/server/document-router/schema';
} from '@documenso/lib/types/document-meta';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
export const ZAddTemplateSettingsFormSchema = z.object({
title: z.string().trim().min(1, { message: "Title can't be empty" }),