mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 17:04:12 +10:00
Merge branch 'main' into feat/document-file-conversion
Resolved conflicts:
- create-document-data.ts: merged initialData + originalData/originalMimeType
- files.helpers.ts: kept both imports
- envelope-drop-zone-wrapper.tsx: adopted buildDropzoneRejectionDescription
- create-envelope-items.ts: adopted UNSAFE_createEnvelopeItems
- create-envelope.ts: integrated convertToPdfIfNeeded into createEnvelopeRouteCaller
Extended putPdfFileServerSide to accept { initialData, originalData,
originalMimeType } options; updated seal-document.handler call site.
This commit is contained in:
@@ -45,7 +45,7 @@ export const LanguageSwitcherDialog = ({ open, setOpen }: LanguageSwitcherDialog
|
||||
{Object.values(SUPPORTED_LANGUAGES).map((language) => (
|
||||
<CommandItem
|
||||
key={language.short}
|
||||
value={language.full}
|
||||
value={_(language.full)}
|
||||
onSelect={async () => setLanguage(language.short)}
|
||||
>
|
||||
<CheckIcon
|
||||
@@ -54,7 +54,7 @@ export const LanguageSwitcherDialog = ({ open, setOpen }: LanguageSwitcherDialog
|
||||
i18n.locale === language.short ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
{SUPPORTED_LANGUAGES[language.short].full}
|
||||
{_(language.full)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { CheckIcon, CopyIcon } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../../primitives/popover';
|
||||
|
||||
const HOVER_DELAY_MS = 500;
|
||||
|
||||
export type LocalTimeProps = {
|
||||
date: Date | string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const LocalTime = ({ date, className }: LocalTimeProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [copiedField, setCopiedField] = useState<string | null>(null);
|
||||
|
||||
const hoverTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const isMouseOver = useRef(false);
|
||||
|
||||
const dt = useMemo(() => DateTime.fromJSDate(new Date(date)), [date]);
|
||||
|
||||
const relative = dt.toRelative() ?? '';
|
||||
const local = dt.toFormat('yyyy-MM-dd HH:mm:ss ZZZZ');
|
||||
const utc = dt.toUTC().toFormat('yyyy-MM-dd HH:mm:ss') + ' UTC';
|
||||
const unix = Math.floor(dt.toSeconds()).toString();
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
isMouseOver.current = true;
|
||||
|
||||
hoverTimeout.current = setTimeout(() => {
|
||||
if (isMouseOver.current) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, HOVER_DELAY_MS);
|
||||
}, []);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
isMouseOver.current = false;
|
||||
|
||||
if (hoverTimeout.current) {
|
||||
clearTimeout(hoverTimeout.current);
|
||||
hoverTimeout.current = null;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (!isMouseOver.current) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
const onCopy = async (label: string, value: string) => {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopiedField(label);
|
||||
setTimeout(() => setCopiedField(null), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
'cursor-pointer underline decoration-muted-foreground/50 decoration-dotted underline-offset-4',
|
||||
className,
|
||||
)}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{relative}
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-auto p-3"
|
||||
align="start"
|
||||
onMouseEnter={() => {
|
||||
isMouseOver.current = true;
|
||||
}}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<div className="space-y-1.5 text-xs">
|
||||
<TimeRow
|
||||
label="Local"
|
||||
value={local}
|
||||
isCopied={copiedField === 'Local'}
|
||||
onCopy={() => void onCopy('Local', local)}
|
||||
/>
|
||||
<TimeRow
|
||||
label="UTC"
|
||||
value={utc}
|
||||
isCopied={copiedField === 'UTC'}
|
||||
onCopy={() => void onCopy('UTC', utc)}
|
||||
/>
|
||||
<TimeRow
|
||||
label="Unix"
|
||||
value={unix}
|
||||
isCopied={copiedField === 'Unix'}
|
||||
onCopy={() => void onCopy('Unix', unix)}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
type TimeRowProps = {
|
||||
label: string;
|
||||
value: string;
|
||||
isCopied: boolean;
|
||||
onCopy: () => void;
|
||||
};
|
||||
|
||||
const TimeRow = ({ label, value, isCopied, onCopy }: TimeRowProps) => (
|
||||
<div className="flex items-center justify-between gap-x-4">
|
||||
<div>
|
||||
<span className="text-muted-foreground">{label}: </span>
|
||||
<span className="font-mono">{value}</span>
|
||||
</div>
|
||||
|
||||
<button type="button" className="text-muted-foreground hover:text-foreground" onClick={onCopy}>
|
||||
{isCopied ? <CheckIcon className="h-3 w-3" /> : <CopyIcon className="h-3 w-3" />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -34,17 +34,17 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.RecipientSigned}
|
||||
>
|
||||
<Trans>Send recipient signed email</Trans>
|
||||
<Trans>Email the owner when a recipient signs</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Recipient signed email</Trans>
|
||||
@@ -72,17 +72,17 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.RecipientSigningRequest}
|
||||
>
|
||||
<Trans>Send recipient signing request email</Trans>
|
||||
<Trans>Email recipients with a signing request</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Recipient signing request email</Trans>
|
||||
@@ -110,17 +110,17 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.RecipientRemoved}
|
||||
>
|
||||
<Trans>Send recipient removed email</Trans>
|
||||
<Trans>Email recipients when they're removed from a pending document</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Recipient removed email</Trans>
|
||||
@@ -148,17 +148,17 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.DocumentPending}
|
||||
>
|
||||
<Trans>Send document pending email</Trans>
|
||||
<Trans>Email the signer if the document is still pending</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document pending email</Trans>
|
||||
@@ -187,17 +187,17 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.DocumentCompleted}
|
||||
>
|
||||
<Trans>Send document completed email</Trans>
|
||||
<Trans>Email recipients when the document is completed</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document completed email</Trans>
|
||||
@@ -225,17 +225,17 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.DocumentDeleted}
|
||||
>
|
||||
<Trans>Send document deleted email</Trans>
|
||||
<Trans>Email recipients when a pending document is deleted</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document deleted email</Trans>
|
||||
@@ -263,17 +263,17 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.OwnerDocumentCompleted}
|
||||
>
|
||||
<Trans>Send document completed email to the owner</Trans>
|
||||
<Trans>Email the owner when the document is completed</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document completed email</Trans>
|
||||
@@ -290,6 +290,84 @@ export const DocumentEmailCheckboxes = ({
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center">
|
||||
<Checkbox
|
||||
id={DocumentEmailEvents.OwnerDocumentCreated}
|
||||
className="h-5 w-5"
|
||||
checked={value.ownerDocumentCreated}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...value, [DocumentEmailEvents.OwnerDocumentCreated]: Boolean(checked) })
|
||||
}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.OwnerDocumentCreated}
|
||||
>
|
||||
<Trans>Email the owner when a document is created from a direct template</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document created from direct template email</Trans>
|
||||
</strong>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This email is sent to the document owner when a recipient creates a document via a
|
||||
direct template link.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center">
|
||||
<Checkbox
|
||||
id={DocumentEmailEvents.OwnerRecipientExpired}
|
||||
className="h-5 w-5"
|
||||
checked={value.ownerRecipientExpired}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...value, [DocumentEmailEvents.OwnerRecipientExpired]: Boolean(checked) })
|
||||
}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.OwnerRecipientExpired}
|
||||
>
|
||||
<Trans>Send recipient expired email to the owner</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Recipient expired email</Trans>
|
||||
</strong>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This will be sent to the document owner when a recipient's signing window has
|
||||
expired.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,13 @@ import { useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentMeta, Field, Recipient } from '@prisma/client';
|
||||
import type { DocumentMeta, Field } from '@prisma/client';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { Clock, EyeOffIcon } from 'lucide-react';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
import type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||
@@ -34,7 +35,7 @@ const getRecipientDisplayText = (recipient: { name: string; email: string }) =>
|
||||
};
|
||||
|
||||
export type DocumentField = Field & {
|
||||
recipient: Pick<Recipient, 'name' | 'email' | 'signingStatus'>;
|
||||
recipient: Pick<TRecipientLite, 'name' | 'email' | 'signingStatus'>;
|
||||
};
|
||||
|
||||
export type DocumentReadOnlyFieldsProps = {
|
||||
@@ -68,7 +69,7 @@ export type DocumentReadOnlyFieldsProps = {
|
||||
|
||||
export const mapFieldsWithRecipients = (
|
||||
fields: Field[],
|
||||
recipients: Recipient[],
|
||||
recipients: TRecipientLite[],
|
||||
): DocumentField[] => {
|
||||
return fields.map((field) => {
|
||||
const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || {
|
||||
@@ -98,10 +99,8 @@ export const DocumentReadOnlyFields = ({
|
||||
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
|
||||
};
|
||||
|
||||
const highestPageNumber = Math.max(...fields.map((field) => field.page));
|
||||
|
||||
return (
|
||||
<ElementVisible target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields.map(
|
||||
(field) =>
|
||||
!hiddenFieldIds[field.secondaryId] && (
|
||||
@@ -112,10 +111,7 @@ export const DocumentReadOnlyFields = ({
|
||||
color={
|
||||
showRecipientColors
|
||||
? getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipientIds.findIndex((id) => id === field.recipientId),
|
||||
0,
|
||||
),
|
||||
recipientIds.findIndex((id) => id === field.recipientId),
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
@@ -163,7 +159,7 @@ export const DocumentReadOnlyFields = ({
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-center text-xs">
|
||||
<p className="mt-1 text-center text-xs text-muted-foreground">
|
||||
{getRecipientDisplayText(field.recipient)}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { FaXTwitter } from 'react-icons/fa6';
|
||||
|
||||
import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
|
||||
@@ -60,7 +61,9 @@ export const DocumentShareButton = ({
|
||||
mutateAsync: createOrGetShareLink,
|
||||
data: shareLink,
|
||||
isPending: isCreatingOrGettingShareLink,
|
||||
} = trpc.document.share.useMutation();
|
||||
} = trpc.document.share.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
});
|
||||
|
||||
const isLoading = isCreatingOrGettingShareLink || isCopyingShareLink;
|
||||
|
||||
@@ -138,7 +141,7 @@ export const DocumentShareButton = ({
|
||||
|
||||
<DialogContent position="end">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<DialogTitle className="w-full max-w-full whitespace-pre-line break-words">
|
||||
<Trans>Share your signing experience!</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
@@ -166,7 +169,7 @@ export const DocumentShareButton = ({
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-muted/40 mt-4 aspect-[1200/630] overflow-hidden rounded-lg border',
|
||||
'mt-4 aspect-[1200/630] overflow-hidden rounded-lg border bg-muted/40',
|
||||
{
|
||||
'animate-pulse': !shareLink?.slug,
|
||||
},
|
||||
|
||||
@@ -186,10 +186,12 @@ export function EnvelopeRecipientFieldTooltip({
|
||||
)}
|
||||
|
||||
<p className="text-center font-semibold">
|
||||
<span>{t(FRIENDLY_FIELD_TYPE[field.type])} field</span>
|
||||
<span>
|
||||
<Trans>{t(FRIENDLY_FIELD_TYPE[field.type])} field</Trans>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-center text-xs">
|
||||
<p className="mt-1 text-center text-xs text-muted-foreground">
|
||||
{getRecipientDisplayText(field.recipient)}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Plural, Trans } from '@lingui/react/macro';
|
||||
|
||||
import type {
|
||||
TEnvelopeExpirationDurationPeriod,
|
||||
TEnvelopeExpirationPeriod,
|
||||
} from '@documenso/lib/constants/envelope-expiration';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
|
||||
type ExpirationMode = 'duration' | 'disabled' | 'inherit';
|
||||
|
||||
const getMode = (value: TEnvelopeExpirationPeriod | null | undefined): ExpirationMode => {
|
||||
if (!value) {
|
||||
return 'inherit';
|
||||
}
|
||||
|
||||
if ('disabled' in value) {
|
||||
return 'disabled';
|
||||
}
|
||||
|
||||
return 'duration';
|
||||
};
|
||||
|
||||
const getAmount = (value: TEnvelopeExpirationPeriod | null | undefined): number => {
|
||||
if (value && 'amount' in value) {
|
||||
return value.amount;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
const getUnit = (
|
||||
value: TEnvelopeExpirationPeriod | null | undefined,
|
||||
): TEnvelopeExpirationDurationPeriod['unit'] => {
|
||||
if (value && 'unit' in value) {
|
||||
return value.unit;
|
||||
}
|
||||
|
||||
return 'month';
|
||||
};
|
||||
|
||||
export type ExpirationPeriodPickerProps = {
|
||||
value: TEnvelopeExpirationPeriod | null | undefined;
|
||||
onChange: (value: TEnvelopeExpirationPeriod | null) => void;
|
||||
disabled?: boolean;
|
||||
inheritLabel?: string;
|
||||
};
|
||||
|
||||
export const ExpirationPeriodPicker = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
inheritLabel,
|
||||
}: ExpirationPeriodPickerProps) => {
|
||||
const mode = getMode(value);
|
||||
const amount = getAmount(value);
|
||||
const unit = getUnit(value);
|
||||
|
||||
const onModeChange = (newMode: string) => {
|
||||
if (newMode === 'inherit') {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMode === 'disabled') {
|
||||
onChange({ disabled: true });
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({ unit, amount });
|
||||
};
|
||||
|
||||
const onAmountChange = (newAmount: number) => {
|
||||
const clamped = Math.max(1, Math.floor(newAmount));
|
||||
|
||||
onChange({ unit, amount: clamped });
|
||||
};
|
||||
|
||||
const onUnitChange = (newUnit: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
onChange({ unit: newUnit as TEnvelopeExpirationDurationPeriod['unit'], amount });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select value={mode} onValueChange={onModeChange} disabled={disabled}>
|
||||
<SelectTrigger className="bg-background" data-testid="envelope-expiration-mode">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="duration">
|
||||
<Trans>Custom duration</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="disabled">
|
||||
<Trans>Never expires</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{inheritLabel !== undefined && <SelectItem value="inherit">{inheritLabel}</SelectItem>}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{mode === 'duration' && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
className="w-20 bg-background"
|
||||
data-testid="envelope-expiration-amount"
|
||||
value={amount}
|
||||
onChange={(e) => onAmountChange(Number(e.target.value))}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Select value={unit} onValueChange={onUnitChange} disabled={disabled}>
|
||||
<SelectTrigger className="flex-1 bg-background" data-testid="envelope-expiration-unit">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="day">
|
||||
<Plural value={amount} one="Day" other="Days" />
|
||||
</SelectItem>
|
||||
<SelectItem value="week">
|
||||
<Plural value={amount} one="Week" other="Weeks" />
|
||||
</SelectItem>
|
||||
<SelectItem value="month">
|
||||
<Plural value={amount} one="Month" other="Months" />
|
||||
</SelectItem>
|
||||
<SelectItem value="year">
|
||||
<Plural value={amount} one="Year" other="Years" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,266 @@
|
||||
import { Plural, Trans } from '@lingui/react/macro';
|
||||
|
||||
import type {
|
||||
TEnvelopeReminderDurationPeriod,
|
||||
TEnvelopeReminderPeriod,
|
||||
TEnvelopeReminderSettings,
|
||||
} from '@documenso/lib/constants/envelope-reminder';
|
||||
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 ReminderMode = 'enabled' | 'disabled' | 'inherit';
|
||||
|
||||
const getMode = (value: TEnvelopeReminderSettings | null | undefined): ReminderMode => {
|
||||
if (value === null || value === undefined) {
|
||||
return 'inherit';
|
||||
}
|
||||
|
||||
if ('disabled' in value.sendAfter) {
|
||||
return 'disabled';
|
||||
}
|
||||
|
||||
return 'enabled';
|
||||
};
|
||||
|
||||
const getPeriodAmount = (period: TEnvelopeReminderPeriod | undefined): number => {
|
||||
if (period && 'amount' in period) {
|
||||
return period.amount;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
const getPeriodUnit = (
|
||||
period: TEnvelopeReminderPeriod | undefined,
|
||||
): TEnvelopeReminderDurationPeriod['unit'] => {
|
||||
if (period && 'unit' in period) {
|
||||
return period.unit;
|
||||
}
|
||||
|
||||
return 'day';
|
||||
};
|
||||
|
||||
export type ReminderSettingsPickerProps = {
|
||||
value: TEnvelopeReminderSettings | null | undefined;
|
||||
onChange: (value: TEnvelopeReminderSettings | null) => void;
|
||||
disabled?: boolean;
|
||||
inheritLabel?: string;
|
||||
};
|
||||
|
||||
export const ReminderSettingsPicker = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
inheritLabel,
|
||||
}: ReminderSettingsPickerProps) => {
|
||||
const mode = getMode(value);
|
||||
|
||||
const sendAfterAmount = getPeriodAmount(value?.sendAfter);
|
||||
const sendAfterUnit = getPeriodUnit(value?.sendAfter);
|
||||
const repeatEveryAmount = getPeriodAmount(value?.repeatEvery);
|
||||
const repeatEveryUnit = getPeriodUnit(value?.repeatEvery);
|
||||
|
||||
const onModeChange = (newMode: string) => {
|
||||
if (newMode === 'inherit') {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMode === 'disabled') {
|
||||
onChange({
|
||||
sendAfter: { disabled: true },
|
||||
repeatEvery: { disabled: true },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({
|
||||
sendAfter: { unit: sendAfterUnit, amount: sendAfterAmount },
|
||||
repeatEvery: { unit: repeatEveryUnit, amount: repeatEveryAmount },
|
||||
});
|
||||
};
|
||||
|
||||
const updateSendAfter = (
|
||||
updates: Partial<{ amount: number; unit: TEnvelopeReminderDurationPeriod['unit'] }>,
|
||||
) => {
|
||||
const newAmount = Math.max(1, Math.floor(updates.amount ?? sendAfterAmount));
|
||||
const newUnit = updates.unit ?? sendAfterUnit;
|
||||
|
||||
onChange({
|
||||
sendAfter: { unit: newUnit, amount: newAmount },
|
||||
repeatEvery: value?.repeatEvery ?? { unit: repeatEveryUnit, amount: repeatEveryAmount },
|
||||
});
|
||||
};
|
||||
|
||||
const updateRepeatEvery = (
|
||||
updates: Partial<{ amount: number; unit: TEnvelopeReminderDurationPeriod['unit'] }>,
|
||||
) => {
|
||||
const newAmount = Math.max(1, Math.floor(updates.amount ?? repeatEveryAmount));
|
||||
const newUnit = updates.unit ?? repeatEveryUnit;
|
||||
|
||||
onChange({
|
||||
sendAfter: value?.sendAfter ?? { unit: sendAfterUnit, amount: sendAfterAmount },
|
||||
repeatEvery: { unit: newUnit, amount: newAmount },
|
||||
});
|
||||
};
|
||||
|
||||
const onRepeatModeChange = (newMode: string) => {
|
||||
if (newMode === 'disabled') {
|
||||
onChange({
|
||||
sendAfter: value?.sendAfter ?? { unit: sendAfterUnit, amount: sendAfterAmount },
|
||||
repeatEvery: { disabled: true },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({
|
||||
sendAfter: value?.sendAfter ?? { unit: sendAfterUnit, amount: sendAfterAmount },
|
||||
repeatEvery: { unit: repeatEveryUnit, amount: repeatEveryAmount },
|
||||
});
|
||||
};
|
||||
|
||||
const repeatMode = value?.repeatEvery && 'disabled' in value.repeatEvery ? 'disabled' : 'enabled';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4" data-testid="reminder-settings-picker">
|
||||
<Select value={mode} onValueChange={onModeChange} disabled={disabled}>
|
||||
<SelectTrigger className="bg-background" data-testid="reminder-mode-select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="enabled">
|
||||
<Trans>Enabled</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="disabled">
|
||||
<Trans>No reminders</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{inheritLabel !== undefined && <SelectItem value="inherit">{inheritLabel}</SelectItem>}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{mode === 'enabled' && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
<Trans>Send first reminder after</Trans>
|
||||
</Label>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
className="w-20 bg-background"
|
||||
value={sendAfterAmount}
|
||||
onChange={(e) => updateSendAfter({ amount: Number(e.target.value) })}
|
||||
disabled={disabled}
|
||||
data-testid="reminder-send-after-amount"
|
||||
/>
|
||||
|
||||
<UnitSelect
|
||||
value={sendAfterUnit}
|
||||
amount={sendAfterAmount}
|
||||
onChange={(unit) =>
|
||||
updateSendAfter({
|
||||
unit: unit as TEnvelopeReminderDurationPeriod['unit'],
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
testId="reminder-send-after-unit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
<Trans>Then repeat every</Trans>
|
||||
</Label>
|
||||
|
||||
<Select value={repeatMode} onValueChange={onRepeatModeChange} disabled={disabled}>
|
||||
<SelectTrigger className="bg-background" data-testid="reminder-repeat-mode-select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="enabled">
|
||||
<Trans>Custom interval</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="disabled">
|
||||
<Trans>Don't repeat</Trans>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{repeatMode === 'enabled' && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
className="w-20 bg-background"
|
||||
value={repeatEveryAmount}
|
||||
onChange={(e) => updateRepeatEvery({ amount: Number(e.target.value) })}
|
||||
disabled={disabled}
|
||||
data-testid="reminder-repeat-amount"
|
||||
/>
|
||||
|
||||
<UnitSelect
|
||||
value={repeatEveryUnit}
|
||||
amount={repeatEveryAmount}
|
||||
onChange={(unit) =>
|
||||
updateRepeatEvery({
|
||||
unit: unit as TEnvelopeReminderDurationPeriod['unit'],
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
testId="reminder-repeat-unit"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UnitSelect = ({
|
||||
value,
|
||||
amount,
|
||||
onChange,
|
||||
disabled,
|
||||
testId,
|
||||
}: {
|
||||
value: string;
|
||||
amount: number;
|
||||
onChange: (value: string) => void;
|
||||
disabled: boolean;
|
||||
testId: string;
|
||||
}) => (
|
||||
<Select value={value} onValueChange={onChange} disabled={disabled}>
|
||||
<SelectTrigger className="flex-1 bg-background" data-testid={testId}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="day">
|
||||
<Plural value={amount} one="Day" other="Days" />
|
||||
</SelectItem>
|
||||
<SelectItem value="week">
|
||||
<Plural value={amount} one="Week" other="Weeks" />
|
||||
</SelectItem>
|
||||
<SelectItem value="month">
|
||||
<Plural value={amount} one="Month" other="Months" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
@@ -124,7 +124,10 @@ export function EnvelopeFieldToolTip({
|
||||
<Tooltip delayDuration={0} open={!field.inserted || !field.fieldMeta}>
|
||||
<TooltipTrigger className="absolute inset-0 w-full"></TooltipTrigger>
|
||||
|
||||
<TooltipContent className={tooltipVariants({ color, className })} sideOffset={2}>
|
||||
<TooltipContent
|
||||
className={tooltipVariants({ color, className: cn(className, 'z-40') })}
|
||||
sideOffset={2}
|
||||
>
|
||||
{children}
|
||||
<TooltipArrow />
|
||||
</TooltipContent>
|
||||
|
||||
@@ -65,7 +65,7 @@ export function FieldToolTip({ children, color, className = '', field }: FieldTo
|
||||
<TooltipTrigger className="absolute inset-0 w-full"></TooltipTrigger>
|
||||
|
||||
<TooltipContent
|
||||
className={tooltipVariants({ color, className })}
|
||||
className={tooltipVariants({ color, className: cn(className, 'z-40') })}
|
||||
sideOffset={2}
|
||||
onClick={onTooltipContentClick}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,11 @@ import { createPortal } from 'react-dom';
|
||||
|
||||
import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds';
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { useIsPageInDom } from '@documenso/lib/client-only/hooks/use-is-page-in-dom';
|
||||
import {
|
||||
PDF_VIEWER_CONTENT_SELECTOR,
|
||||
PDF_VIEWER_PAGE_SELECTOR,
|
||||
} from '@documenso/lib/constants/pdf-viewer';
|
||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
|
||||
import type { RecipientColorStyles } from '../../lib/recipient-colors';
|
||||
@@ -81,6 +85,8 @@ export function FieldRootContainer({
|
||||
readonly,
|
||||
}: FieldRootContainerProps) {
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const isPageInDom = useIsPageInDom(field.page);
|
||||
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -88,6 +94,21 @@ export function FieldRootContainer({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the validation signal on the PDF viewer container. When a field
|
||||
// mounts after the virtual list scrolls to its page, the per-element
|
||||
// `data-validate` attribute will not have been set yet. The signal on the
|
||||
// `[data-pdf-content]` container bridges this gap so newly-rendered fields
|
||||
// pick up the validation state immediately.
|
||||
const pdfContent = document.querySelector(PDF_VIEWER_CONTENT_SELECTOR);
|
||||
|
||||
if (
|
||||
pdfContent?.getAttribute('data-validate-fields') === 'true' &&
|
||||
isFieldUnsignedAndRequired(field)
|
||||
) {
|
||||
ref.current.setAttribute('data-validate', 'true');
|
||||
setIsValidating(true);
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((_mutations) => {
|
||||
if (ref.current) {
|
||||
setIsValidating(ref.current.getAttribute('data-validate') === 'true');
|
||||
@@ -101,7 +122,11 @@ export function FieldRootContainer({
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [isPageInDom]);
|
||||
|
||||
if (!isPageInDom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldContainerPortal field={field}>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
|
||||
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
|
||||
import type { PdfViewerRendererMode } from './pdf-viewer-konva';
|
||||
|
||||
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||
|
||||
export type PDFViewerProps = {
|
||||
className?: string;
|
||||
onDocumentLoad?: () => void;
|
||||
renderer: PdfViewerRendererMode;
|
||||
[key: string]: unknown;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
|
||||
|
||||
const EnvelopePdfViewer = lazy(async () => import('./pdf-viewer-konva'));
|
||||
|
||||
export const PDFViewerKonvaLazy = (props: PDFViewerProps) => {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<EnvelopePdfViewer {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFViewerKonvaLazy;
|
||||
@@ -1,213 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import Konva from 'konva';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
||||
|
||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
|
||||
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||
|
||||
/**
|
||||
* This imports the worker from the `pdfjs-dist` package.
|
||||
*/
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
const pdfViewerOptions = {
|
||||
cMapUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/static/cmaps/`,
|
||||
};
|
||||
|
||||
const PDFLoader = () => (
|
||||
<>
|
||||
<Loader className="h-12 w-12 animate-spin text-documenso" />
|
||||
|
||||
<p className="mt-4 text-muted-foreground">
|
||||
<Trans>Loading document...</Trans>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
export type PdfViewerRendererMode = 'editor' | 'preview' | 'signing';
|
||||
|
||||
const RendererErrorMessages: Record<
|
||||
PdfViewerRendererMode,
|
||||
{ title: MessageDescriptor; description: MessageDescriptor }
|
||||
> = {
|
||||
editor: {
|
||||
title: msg`Configuration Error`,
|
||||
description: msg`There was an issue rendering some fields, please review the fields and try again.`,
|
||||
},
|
||||
preview: {
|
||||
title: msg`Configuration Error`,
|
||||
description: msg`Something went wrong while rendering the document, some fields may be missing or corrupted.`,
|
||||
},
|
||||
signing: {
|
||||
title: msg`Configuration Error`,
|
||||
description: msg`Something went wrong while rendering the document, some fields may be missing or corrupted.`,
|
||||
},
|
||||
};
|
||||
|
||||
export type PdfViewerKonvaProps = {
|
||||
className?: string;
|
||||
onDocumentLoad?: () => void;
|
||||
customPageRenderer?: React.FunctionComponent;
|
||||
renderer: PdfViewerRendererMode;
|
||||
[key: string]: unknown;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
|
||||
|
||||
export const PdfViewerKonva = ({
|
||||
className,
|
||||
onDocumentLoad,
|
||||
customPageRenderer,
|
||||
renderer,
|
||||
...props
|
||||
}: PdfViewerKonvaProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const $el = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { getPdfBuffer, currentEnvelopeItem, renderError } = useCurrentEnvelopeRender();
|
||||
|
||||
const [width, setWidth] = useState(0);
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [pdfError, setPdfError] = useState(false);
|
||||
|
||||
const envelopeItemFile = useMemo(() => {
|
||||
const data = getPdfBuffer(currentEnvelopeItem?.id || '');
|
||||
|
||||
if (!data || data.status !== 'loaded') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
data: new Uint8Array(data.file),
|
||||
};
|
||||
}, [currentEnvelopeItem?.id, getPdfBuffer]);
|
||||
|
||||
const onDocumentLoaded = useCallback(
|
||||
(doc: PDFDocumentProxy) => {
|
||||
setNumPages(doc.numPages);
|
||||
},
|
||||
[onDocumentLoad],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if ($el.current) {
|
||||
const $current = $el.current;
|
||||
|
||||
const { width } = $current.getBoundingClientRect();
|
||||
|
||||
setWidth(width);
|
||||
|
||||
const onResize = () => {
|
||||
const { width } = $current.getBoundingClientRect();
|
||||
setWidth(width);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={$el} className={cn('w-full max-w-[800px]', className)} {...props}>
|
||||
{renderError && (
|
||||
<Alert variant="destructive" className="mb-4 max-w-[800px]">
|
||||
<AlertTitle>{t(RendererErrorMessages[renderer].title)}</AlertTitle>
|
||||
<AlertDescription>{t(RendererErrorMessages[renderer].description)}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{envelopeItemFile && Konva ? (
|
||||
<PDFDocument
|
||||
file={envelopeItemFile}
|
||||
className={cn('w-full rounded', {
|
||||
'h-[80vh] max-h-[60rem]': numPages === 0,
|
||||
})}
|
||||
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.
|
||||
onSourceError={() => {
|
||||
setPdfError(true);
|
||||
}}
|
||||
externalLinkTarget="_blank"
|
||||
loading={
|
||||
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50 dark:bg-background">
|
||||
{pdfError ? (
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>
|
||||
<Trans>Something went wrong while loading the document.</Trans>
|
||||
</p>
|
||||
<p className="mt-1 text-sm">
|
||||
<Trans>Please try again or contact our support.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<PDFLoader />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
error={
|
||||
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50 dark:bg-background">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>
|
||||
<Trans>Something went wrong while loading the document.</Trans>
|
||||
</p>
|
||||
<p className="mt-1 text-sm">
|
||||
<Trans>Please try again or contact our support.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
options={pdfViewerOptions}
|
||||
>
|
||||
{Array(numPages)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<div key={i} className="last:-mb-2">
|
||||
<div className="rounded border border-border will-change-transform">
|
||||
<PDFPage
|
||||
pageNumber={i + 1}
|
||||
width={width}
|
||||
renderAnnotationLayer={false}
|
||||
renderTextLayer={false}
|
||||
loading={() => ''}
|
||||
renderMode={customPageRenderer ? 'custom' : 'canvas'}
|
||||
customRenderer={customPageRenderer}
|
||||
/>
|
||||
</div>
|
||||
<p className="my-2 text-center text-[11px] text-muted-foreground/80">
|
||||
<Trans>
|
||||
Page {i + 1} of {numPages}
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</PDFDocument>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-[80vh] max-h-[60rem] w-full flex-col items-center justify-center overflow-hidden rounded',
|
||||
)}
|
||||
>
|
||||
<PDFLoader />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PdfViewerKonva;
|
||||
@@ -12,14 +12,28 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export type RecipientRoleSelectProps = SelectProps & {
|
||||
hideCCRecipients?: boolean;
|
||||
hideAssistantRole?: boolean;
|
||||
hideCCerRole?: boolean;
|
||||
hideViewerRole?: boolean;
|
||||
hideApproverRole?: boolean;
|
||||
|
||||
isAssistantEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSelectProps>(
|
||||
({ hideCCRecipients, isAssistantEnabled = true, ...props }, ref) => (
|
||||
(
|
||||
{
|
||||
hideAssistantRole,
|
||||
hideCCerRole,
|
||||
hideViewerRole,
|
||||
hideApproverRole,
|
||||
isAssistantEnabled = true,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<Select {...props}>
|
||||
<SelectTrigger ref={ref} className="bg-background w-[50px] p-2">
|
||||
<SelectTrigger ref={ref} className="w-[50px] bg-background p-2" title={props.value}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */}
|
||||
{ROLE_ICONS[props.value as RecipientRole]}
|
||||
</SelectTrigger>
|
||||
@@ -35,7 +49,7 @@ export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSe
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||
<TooltipContent className="z-9999 max-w-md p-4 text-foreground">
|
||||
<p>
|
||||
<Trans>
|
||||
The recipient is required to sign the document for it to be completed.
|
||||
@@ -46,49 +60,53 @@ export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSe
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value={RecipientRole.APPROVER}>
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
|
||||
<Trans>Needs to approve</Trans>
|
||||
{!hideApproverRole && (
|
||||
<SelectItem value={RecipientRole.APPROVER}>
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
|
||||
<Trans>Needs to approve</Trans>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="z-9999 max-w-md p-4 text-foreground">
|
||||
<p>
|
||||
<Trans>
|
||||
The recipient is required to approve the document for it to be completed.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||
<p>
|
||||
<Trans>
|
||||
The recipient is required to approve the document for it to be completed.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectItem>
|
||||
)}
|
||||
|
||||
<SelectItem value={RecipientRole.VIEWER}>
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
|
||||
<Trans>Needs to view</Trans>
|
||||
{!hideViewerRole && (
|
||||
<SelectItem value={RecipientRole.VIEWER}>
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
|
||||
<Trans>Needs to view</Trans>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="z-9999 max-w-md p-4 text-foreground">
|
||||
<p>
|
||||
<Trans>
|
||||
The recipient is required to view the document for it to be completed.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||
<p>
|
||||
<Trans>
|
||||
The recipient is required to view the document for it to be completed.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectItem>
|
||||
)}
|
||||
|
||||
{!hideCCRecipients && (
|
||||
{!hideCCerRole && (
|
||||
<SelectItem value={RecipientRole.CC}>
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
@@ -99,7 +117,7 @@ export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSe
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||
<TooltipContent className="z-9999 max-w-md p-4 text-foreground">
|
||||
<p>
|
||||
<Trans>
|
||||
The recipient is not required to take any action and receives a copy of the
|
||||
@@ -112,41 +130,43 @@ export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSe
|
||||
</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>
|
||||
{!hideAssistantRole && (
|
||||
<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="z-9999 max-w-md p-4 text-foreground">
|
||||
<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>
|
||||
<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>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { TemplateType } from '@prisma/client';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
export type TemplateTypeSelectProps = SelectProps;
|
||||
|
||||
export const TemplateTypeSelect = forwardRef<HTMLButtonElement, TemplateTypeSelectProps>(
|
||||
({ ...props }, ref) => {
|
||||
useLingui();
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger ref={ref} className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value={TemplateType.PRIVATE}>{t`Private`}</SelectItem>
|
||||
<SelectItem value={TemplateType.PUBLIC}>{t`Public`}</SelectItem>
|
||||
<SelectItem value={TemplateType.ORGANISATION}>{t`Organisation`}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TemplateTypeSelect.displayName = 'TemplateTypeSelect';
|
||||
|
||||
export const TemplateTypeTooltip = ({
|
||||
organisationTeamCount,
|
||||
}: {
|
||||
organisationTeamCount: number;
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<p>
|
||||
<Trans>
|
||||
<strong>Private</strong> templates can only be used by your team.
|
||||
</Trans>
|
||||
</p>
|
||||
<p>
|
||||
<Trans>
|
||||
<strong>Public</strong> templates are linked to your public profile.
|
||||
</Trans>
|
||||
</p>
|
||||
{organisationTeamCount >= 2 && (
|
||||
<p>
|
||||
<Trans>
|
||||
<strong>Organisation</strong> templates are shared across all teams in your
|
||||
organisation but can only be edited by the owning team.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { ErrorCode, type FileRejection } from 'react-dropzone';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
|
||||
export const buildDropzoneRejectionDescription = (fileRejections: FileRejection[]) => {
|
||||
const errorCode = fileRejections[0]?.errors[0]?.code;
|
||||
|
||||
return match(errorCode)
|
||||
.with(
|
||||
ErrorCode.FileTooLarge,
|
||||
() => msg`File is larger than ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB`,
|
||||
)
|
||||
.with(ErrorCode.FileInvalidType, () => msg`Only PDF files are allowed`)
|
||||
.with(ErrorCode.FileTooSmall, () => msg`File is too small`)
|
||||
.with(ErrorCode.TooManyFiles, () => msg`Only one file can be uploaded at a time`)
|
||||
.otherwise(() => msg`Unknown error`);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
// !: We declare all of our classes here since TailwindCSS will remove any unused CSS classes,
|
||||
// !: therefore doing this at runtime is not possible without whitelisting a set of classnames.
|
||||
// !:
|
||||
// !: This will later be improved as we move to a CSS variable approach and rotate the lightness
|
||||
import { colord } from 'colord';
|
||||
import { once } from 'remeda';
|
||||
|
||||
export type RecipientColorMap = Record<number, RecipientColorStyles>;
|
||||
export type TRecipientColor = 'readOnly' | (typeof AVAILABLE_RECIPIENT_COLORS)[number];
|
||||
|
||||
export type RecipientColorStyles = {
|
||||
base: string;
|
||||
@@ -11,128 +9,89 @@ export type RecipientColorStyles = {
|
||||
baseRingHover: string;
|
||||
baseTextHover: string;
|
||||
fieldButton: string;
|
||||
fieldButtonText: string;
|
||||
fieldItem: string;
|
||||
fieldItemInitials: string;
|
||||
comboxBoxTrigger: string;
|
||||
comboxBoxItem: string;
|
||||
comboBoxTrigger: string;
|
||||
comboBoxItem: string;
|
||||
};
|
||||
|
||||
export const DEFAULT_RECT_BACKGROUND = 'rgba(255, 255, 255, 0.95)';
|
||||
|
||||
// !: values of the declared variable to do all the background, border and shadow styles.
|
||||
export const RECIPIENT_COLOR_STYLES = {
|
||||
readOnly: {
|
||||
const RECIPIENT_COLOR_STYLES: Record<TRecipientColor, () => RecipientColorStyles> = {
|
||||
readOnly: (): RecipientColorStyles => ({
|
||||
base: 'ring-neutral-400',
|
||||
baseRing: 'rgba(176, 176, 176, 1)',
|
||||
baseRingHover: 'rgba(176, 176, 176, 1)',
|
||||
baseTextHover: 'rgba(176, 176, 176, 1)',
|
||||
fieldButton: 'border-neutral-400 hover:border-neutral-400',
|
||||
fieldButtonText: '',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: '',
|
||||
comboxBoxTrigger:
|
||||
comboBoxTrigger:
|
||||
'ring-2 ring-recipient-green shadow-[0_0_0_5px_hsl(var(--recipient-green)/10%),0_0_0_2px_hsl(var(--recipient-green)/60%),0_0_0_0.5px_hsl(var(--recipient-green))]',
|
||||
comboxBoxItem: '',
|
||||
},
|
||||
|
||||
green: {
|
||||
base: 'ring-recipient-green hover:bg-recipient-green/30',
|
||||
baseRing: 'rgba(122, 195, 85, 1)',
|
||||
baseRingHover: 'rgba(122, 195, 85, 0.3)',
|
||||
baseTextHover: 'rgba(122, 195, 85, 1)',
|
||||
fieldButton: 'hover:border-recipient-green hover:bg-recipient-green/30 ',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-green',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-green hover:bg-recipient-green/15 active:bg-recipient-green/15 shadow-[0_0_0_5px_hsl(var(--recipient-green)/10%),0_0_0_2px_hsl(var(--recipient-green)/60%),0_0_0_0.5px_hsl(var(--recipient-green))]',
|
||||
comboxBoxItem: 'hover:bg-recipient-green/15 active:bg-recipient-green/15',
|
||||
},
|
||||
|
||||
blue: {
|
||||
base: 'ring-recipient-blue hover:bg-recipient-blue/30',
|
||||
baseRing: 'rgba(56, 123, 199, 1)',
|
||||
baseRingHover: 'rgba(56, 123, 199, 0.3)',
|
||||
baseTextHover: 'rgba(56, 123, 199, 1)',
|
||||
fieldButton: 'hover:border-recipient-blue hover:bg-recipient-blue/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-blue',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-blue hover:bg-recipient-blue/15 active:bg-recipient-blue/15 shadow-[0_0_0_5px_hsl(var(--recipient-blue)/10%),0_0_0_2px_hsl(var(--recipient-blue)/60%),0_0_0_0.5px_hsl(var(--recipient-blue))]',
|
||||
comboxBoxItem: 'ring-recipient-blue hover:bg-recipient-blue/15 active:bg-recipient-blue/15',
|
||||
},
|
||||
|
||||
purple: {
|
||||
base: 'ring-recipient-purple hover:bg-recipient-purple/30',
|
||||
baseRing: 'rgba(151, 71, 255, 1)',
|
||||
baseRingHover: 'rgba(151, 71, 255, 0.3)',
|
||||
baseTextHover: 'rgba(151, 71, 255, 1)',
|
||||
fieldButton: 'hover:border-recipient-purple hover:bg-recipient-purple/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-purple',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-purple hover:bg-recipient-purple/15 active:bg-recipient-purple/15 shadow-[0_0_0_5px_hsl(var(--recipient-purple)/10%),0_0_0_2px_hsl(var(--recipient-purple)/60%),0_0_0_0.5px_hsl(var(--recipient-purple))]',
|
||||
comboxBoxItem: 'hover:bg-recipient-purple/15 active:bg-recipient-purple/15',
|
||||
},
|
||||
|
||||
orange: {
|
||||
base: 'ring-recipient-orange hover:bg-recipient-orange/30',
|
||||
baseRing: 'rgba(246, 159, 30, 1)',
|
||||
baseRingHover: 'rgba(246, 159, 30, 0.3)',
|
||||
baseTextHover: 'rgba(246, 159, 30, 1)',
|
||||
fieldButton: 'hover:border-recipient-orange hover:bg-recipient-orange/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-orange',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-orange hover:bg-recipient-orange/15 active:bg-recipient-orange/15 shadow-[0_0_0_5px_hsl(var(--recipient-orange)/10%),0_0_0_2px_hsl(var(--recipient-orange)/60%),0_0_0_0.5px_hsl(var(--recipient-orange))]',
|
||||
comboxBoxItem: 'hover:bg-recipient-orange/15 active:bg-recipient-orange/15',
|
||||
},
|
||||
|
||||
yellow: {
|
||||
base: 'ring-recipient-yellow hover:bg-recipient-yellow/30',
|
||||
baseRing: 'rgba(219, 186, 0, 1)',
|
||||
baseRingHover: 'rgba(219, 186, 0, 0.3)',
|
||||
baseTextHover: 'rgba(219, 186, 0, 1)',
|
||||
fieldButton: 'hover:border-recipient-yellow hover:bg-recipient-yellow/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-yellow',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-yellow hover:bg-recipient-yellow/15 active:bg-recipient-yellow/15 shadow-[0_0_0_5px_hsl(var(--recipient-yellow)/10%),0_0_0_2px_hsl(var(--recipient-yellow)/60%),0_0_0_0.5px_hsl(var(--recipient-yellow))]',
|
||||
comboxBoxItem: 'hover:bg-recipient-yellow/15 active:bg-recipient-yellow/15',
|
||||
},
|
||||
|
||||
pink: {
|
||||
base: 'ring-recipient-pink hover:bg-recipient-pink/30',
|
||||
baseRing: 'rgba(217, 74, 186, 1)',
|
||||
baseRingHover: 'rgba(217, 74, 186, 0.3)',
|
||||
baseTextHover: 'rgba(217, 74, 186, 1)',
|
||||
fieldButton: 'hover:border-recipient-pink hover:bg-recipient-pink/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-pink',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-pink hover:bg-recipient-pink/15 active:bg-recipient-pink/15 shadow-[0_0_0_5px_hsl(var(--recipient-pink)/10%),0_0_0_2px_hsl(var(--recipient-pink)/60%),0_0_0_0.5px_hsl(var(--recipient-pink',
|
||||
comboxBoxItem: 'hover:bg-recipient-pink/15 active:bg-recipient-pink/15',
|
||||
},
|
||||
} satisfies Record<string, RecipientColorStyles>;
|
||||
|
||||
export type TRecipientColor = keyof typeof RECIPIENT_COLOR_STYLES;
|
||||
|
||||
export const AVAILABLE_RECIPIENT_COLORS = [
|
||||
'green',
|
||||
'blue',
|
||||
'purple',
|
||||
'orange',
|
||||
'yellow',
|
||||
'pink',
|
||||
] satisfies TRecipientColor[];
|
||||
|
||||
export const useRecipientColors = (index: number) => {
|
||||
const key = AVAILABLE_RECIPIENT_COLORS[index % AVAILABLE_RECIPIENT_COLORS.length];
|
||||
|
||||
return RECIPIENT_COLOR_STYLES[key];
|
||||
comboBoxItem: '',
|
||||
}),
|
||||
green: once(() => generateStyles('green')),
|
||||
blue: once(() => generateStyles('blue')),
|
||||
purple: once(() => generateStyles('purple')),
|
||||
orange: once(() => generateStyles('orange')),
|
||||
yellow: once(() => generateStyles('yellow')),
|
||||
pink: once(() => generateStyles('pink')),
|
||||
};
|
||||
|
||||
export const getRecipientColorStyles = (index: number) => {
|
||||
// Disabling the rule since the hook doesn't do anything special and can
|
||||
// be used universally.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useRecipientColors(index);
|
||||
const generateStyles = (recipientColor: TRecipientColor): RecipientColorStyles => {
|
||||
const { bg, border, ring, text } = CSS_PROPERTY;
|
||||
const { active, hover, groupHover, groupHoverFieldItem } = CSS_VARIANT;
|
||||
|
||||
const name = `recipient-${recipientColor}`;
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(`--${name}`);
|
||||
const color = colord(`hsl(${value})`);
|
||||
|
||||
return {
|
||||
base: `${ring}-${name} ${hover}:${bg}-${name}/30`,
|
||||
baseRing: color.toRgbString(),
|
||||
baseRingHover: color.alpha(0.3).toRgbString(),
|
||||
baseTextHover: color.toRgbString(),
|
||||
fieldButton: `${hover}:${border}-${name} ${hover}:${bg}-${name}/30`,
|
||||
fieldButtonText: `${groupHover}:${text}-${name}`,
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: `${groupHoverFieldItem}:${bg}-${name}`,
|
||||
comboBoxTrigger: `ring-2 ${ring}-${name} ${hover}:${bg}-${name}/15 ${active}:${bg}-${name}/15 shadow-[0_0_0_5px_hsl(var(--${name})/10%),0_0_0_2px_hsl(var(--${name})/60%),0_0_0_0.5px_hsl(var(--${name}))]`,
|
||||
comboBoxItem: `${hover}:${bg}-${name}/15 ${active}:${bg}-${name}/15`,
|
||||
};
|
||||
};
|
||||
|
||||
const CSS_PROPERTY = {
|
||||
bg: 'bg',
|
||||
border: 'border',
|
||||
ring: 'ring',
|
||||
text: 'text',
|
||||
};
|
||||
|
||||
const CSS_VARIANT = {
|
||||
active: 'active',
|
||||
groupHover: 'group-hover',
|
||||
groupHoverFieldItem: 'group-hover/field-item',
|
||||
hover: 'hover',
|
||||
};
|
||||
|
||||
const AVAILABLE_RECIPIENT_COLORS = ['green', 'blue', 'purple', 'orange', 'yellow', 'pink'] as const;
|
||||
|
||||
export const RECIPIENT_DYNAMIC_CLASS = {
|
||||
pattern: new RegExp(
|
||||
`(${Object.values(CSS_PROPERTY).join('|')})-recipient-(${AVAILABLE_RECIPIENT_COLORS.join('|')})(\\/(15|30))?$`,
|
||||
),
|
||||
variants: Object.values(CSS_VARIANT),
|
||||
};
|
||||
|
||||
export const getRecipientColor = (index: number) => {
|
||||
return AVAILABLE_RECIPIENT_COLORS[Math.max(index, 0) % AVAILABLE_RECIPIENT_COLORS.length];
|
||||
};
|
||||
|
||||
export const getRecipientColorStyles = (colorOrIndex: TRecipientColor | number) => {
|
||||
const color = typeof colorOrIndex === 'number' ? getRecipientColor(colorOrIndex) : colorOrIndex;
|
||||
|
||||
return RECIPIENT_COLOR_STYLES[color]();
|
||||
};
|
||||
|
||||
@@ -66,14 +66,13 @@
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^0.554.0",
|
||||
"luxon": "^3.7.2",
|
||||
"perfect-freehand": "^1.2.2",
|
||||
"pdfjs-dist": "5.4.296",
|
||||
"perfect-freehand": "^1.2.2",
|
||||
"react": "^18",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.66.1",
|
||||
"react-pdf": "^10.3.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"remeda": "^2.32.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
|
||||
@@ -20,6 +20,8 @@ const badgeVariants = cva(
|
||||
'bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20',
|
||||
secondary:
|
||||
'bg-blue-50 text-blue-700 ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30',
|
||||
orange:
|
||||
'bg-orange-50 text-orange-700 ring-orange-700/10 dark:bg-orange-400/10 dark:text-orange-400 dark:ring-orange-400/30',
|
||||
},
|
||||
size: {
|
||||
small: 'px-1.5 py-0.5 text-xs',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Trans } from '@lingui/react/macro';
|
||||
import type {
|
||||
ColumnDef,
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
Table as TTable,
|
||||
Updater,
|
||||
VisibilityState,
|
||||
@@ -15,7 +16,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '.
|
||||
|
||||
export type DataTableChildren<TData> = (_table: TTable<TData>) => React.ReactNode;
|
||||
|
||||
export type { ColumnDef as DataTableColumnDef } from '@tanstack/react-table';
|
||||
export type { ColumnDef as DataTableColumnDef, RowSelectionState } from '@tanstack/react-table';
|
||||
|
||||
export interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
@@ -40,6 +41,10 @@ export interface DataTableProps<TData, TValue> {
|
||||
enable: boolean;
|
||||
component?: React.ReactNode;
|
||||
};
|
||||
enableRowSelection?: boolean;
|
||||
rowSelection?: RowSelectionState;
|
||||
onRowSelectionChange?: (selection: RowSelectionState) => void;
|
||||
getRowId?: (row: TData) => string;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
@@ -58,6 +63,10 @@ export function DataTable<TData, TValue>({
|
||||
rowClassName,
|
||||
children,
|
||||
emptyState,
|
||||
enableRowSelection,
|
||||
rowSelection,
|
||||
onRowSelectionChange,
|
||||
getRowId,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const pagination = useMemo<PaginationState>(() => {
|
||||
if (currentPage !== undefined && perPage !== undefined) {
|
||||
@@ -85,6 +94,13 @@ export function DataTable<TData, TValue>({
|
||||
}
|
||||
};
|
||||
|
||||
const onTableRowSelectionChange = (updater: Updater<RowSelectionState>) => {
|
||||
if (onRowSelectionChange) {
|
||||
const newSelection = typeof updater === 'function' ? updater(rowSelection ?? {}) : updater;
|
||||
onRowSelectionChange(newSelection);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
@@ -92,10 +108,14 @@ export function DataTable<TData, TValue>({
|
||||
state: {
|
||||
pagination: manualPagination ? pagination : undefined,
|
||||
columnVisibility,
|
||||
rowSelection: rowSelection ?? {},
|
||||
},
|
||||
manualPagination,
|
||||
pageCount: totalPages,
|
||||
onPaginationChange: onTablePaginationChange,
|
||||
enableRowSelection,
|
||||
onRowSelectionChange: onTableRowSelectionChange,
|
||||
getRowId,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -162,7 +182,7 @@ export function DataTable<TData, TValue>({
|
||||
{hasFilters && onClearFilters !== undefined && (
|
||||
<button
|
||||
onClick={() => onClearFilters()}
|
||||
className="text-foreground mt-1 text-sm"
|
||||
className="mt-1 text-sm text-foreground"
|
||||
>
|
||||
<Trans>Clear filters</Trans>
|
||||
</button>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
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 type { Field } from '@prisma/client';
|
||||
import { FieldType, Prisma, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import {
|
||||
CalendarDays,
|
||||
@@ -23,11 +23,12 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR, getPdfPagesCount } from '@documenso/lib/constants/pdf-viewer';
|
||||
import {
|
||||
type TFieldMetaSchema as FieldMeta,
|
||||
ZFieldMetaSchema,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { validateFieldsUninserted } from '@documenso/lib/utils/fields';
|
||||
@@ -35,10 +36,11 @@ import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||
import {
|
||||
canRecipientBeModified,
|
||||
canRecipientFieldsBeModified,
|
||||
getRecipientsWithMissingFields,
|
||||
} from '@documenso/lib/utils/recipients';
|
||||
|
||||
import { FieldToolTip } from '../../components/field/field-tooltip';
|
||||
import { useRecipientColors } from '../../lib/recipient-colors';
|
||||
import { getRecipientColorStyles } from '../../lib/recipient-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Alert, AlertDescription } from '../alert';
|
||||
import { Card, CardContent } from '../card';
|
||||
@@ -82,7 +84,7 @@ export type FieldFormType = {
|
||||
export type AddFieldsFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
hideRecipients?: boolean;
|
||||
recipients: Recipient[];
|
||||
recipients: TRecipientLite[];
|
||||
fields: Field[];
|
||||
onSubmit: (_data: TAddFieldsFormSchema) => void;
|
||||
onAutoSave: (_data: TAddFieldsFormSchema) => Promise<void>;
|
||||
@@ -171,7 +173,7 @@ export const AddFieldsFormPartial = ({
|
||||
});
|
||||
|
||||
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
|
||||
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
|
||||
const [selectedSigner, setSelectedSigner] = useState<TRecipientLite | null>(null);
|
||||
const [lastActiveField, setLastActiveField] = useState<TAddFieldsFormSchema['fields'][0] | null>(
|
||||
null,
|
||||
);
|
||||
@@ -179,9 +181,7 @@ export const AddFieldsFormPartial = ({
|
||||
null,
|
||||
);
|
||||
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
|
||||
const selectedSignerStyles = useRecipientColors(
|
||||
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
|
||||
);
|
||||
const selectedSignerStyles = getRecipientColorStyles(selectedSignerIndex);
|
||||
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
|
||||
@@ -430,13 +430,15 @@ export const AddFieldsFormPartial = ({
|
||||
}
|
||||
|
||||
if (duplicateAll) {
|
||||
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
|
||||
const totalPages = getPdfPagesCount();
|
||||
|
||||
pages.forEach((_, index) => {
|
||||
const pageNumber = index + 1;
|
||||
if (totalPages < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
|
||||
if (pageNumber === lastActiveField.pageNumber) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newField: TAddFieldsFormSchema['fields'][0] = {
|
||||
@@ -449,7 +451,7 @@ export const AddFieldsFormPartial = ({
|
||||
};
|
||||
|
||||
append(newField);
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -535,7 +537,7 @@ export const AddFieldsFormPartial = ({
|
||||
}, [recipients]);
|
||||
|
||||
const recipientsByRole = useMemo(() => {
|
||||
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
||||
const recipientsByRole: Record<RecipientRole, TRecipientLite[]> = {
|
||||
CC: [],
|
||||
VIEWER: [],
|
||||
SIGNER: [],
|
||||
@@ -555,15 +557,11 @@ export const AddFieldsFormPartial = ({
|
||||
};
|
||||
|
||||
const handleGoNextClick = () => {
|
||||
const everySignerHasSignature = recipientsByRole.SIGNER.every((signer) =>
|
||||
localFields.some(
|
||||
(field) =>
|
||||
(field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) &&
|
||||
field.signerEmail === signer.email,
|
||||
),
|
||||
);
|
||||
// localFields already have recipientId set correctly (see field creation at line 338)
|
||||
// Using the existing recipientId is important for handling duplicate email recipients
|
||||
const recipientsMissingFields = getRecipientsWithMissingFields(recipients, localFields);
|
||||
|
||||
if (!everySignerHasSignature) {
|
||||
if (recipientsMissingFields.length > 0) {
|
||||
setIsMissingSignatureDialogVisible(true);
|
||||
return;
|
||||
}
|
||||
@@ -756,7 +754,7 @@ export const AddFieldsFormPartial = ({
|
||||
)}
|
||||
>
|
||||
<Contact className="h-4 w-4" />
|
||||
Initials
|
||||
<Trans>Initials</Trans>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -906,7 +904,7 @@ export const AddFieldsFormPartial = ({
|
||||
)}
|
||||
>
|
||||
<Disc className="h-4 w-4" />
|
||||
Radio
|
||||
<Trans>Radio</Trans>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -931,8 +929,7 @@ export const AddFieldsFormPartial = ({
|
||||
)}
|
||||
>
|
||||
<CheckSquare className="h-4 w-4" />
|
||||
{/* Not translated on purpose. */}
|
||||
Checkbox
|
||||
<Trans>Checkbox</Trans>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
DocumentStatus,
|
||||
DocumentVisibility,
|
||||
type Field,
|
||||
type Recipient,
|
||||
SendStatus,
|
||||
TeamMemberRole,
|
||||
} from '@prisma/client';
|
||||
@@ -21,6 +20,7 @@ import { DOCUMENT_SIGNATURE_TYPES } from '@documenso/lib/constants/document';
|
||||
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 type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
|
||||
import {
|
||||
@@ -74,7 +74,7 @@ import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSettingsFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
recipients: TRecipientLite[];
|
||||
fields: Field[];
|
||||
isDocumentPdfLoaded: boolean;
|
||||
document: TDocument;
|
||||
@@ -242,7 +242,7 @@ export const AddSettingsFormPartial = ({
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<Trans>
|
||||
Controls the language for the document, including the language to be used
|
||||
for email notifications, and the final certificate that is generated and
|
||||
@@ -269,7 +269,7 @@ export const AddSettingsFormPartial = ({
|
||||
<SelectContent>
|
||||
{Object.entries(SUPPORTED_LANGUAGES).map(([code, language]) => (
|
||||
<SelectItem key={code} value={code}>
|
||||
{language.full}
|
||||
{t(language.full)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -361,11 +361,11 @@ export const AddSettingsFormPartial = ({
|
||||
|
||||
<Accordion type="multiple" className="mt-6">
|
||||
<AccordionItem value="advanced-options" className="border-none">
|
||||
<AccordionTrigger className="text-foreground mb-2 rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
|
||||
<AccordionTrigger className="mb-2 rounded border px-3 py-2 text-left text-foreground hover:bg-neutral-200/30 hover:no-underline">
|
||||
<Trans>Advanced Options</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
|
||||
<AccordionContent className="-mx-1 px-1 pt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
<div className="flex flex-col space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -379,7 +379,7 @@ export const AddSettingsFormPartial = ({
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<TooltipContent className="max-w-xs text-muted-foreground">
|
||||
<Trans>
|
||||
Add an external ID to the document. This can be used to identify
|
||||
the document in external systems.
|
||||
@@ -418,7 +418,7 @@ export const AddSettingsFormPartial = ({
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
className="bg-background w-full"
|
||||
className="w-full bg-background"
|
||||
emptySelectionPlaceholder={t`Select signature types`}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -506,7 +506,7 @@ export const AddSettingsFormPartial = ({
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<TooltipContent className="max-w-xs text-muted-foreground">
|
||||
<Trans>
|
||||
Add a URL to redirect the user to once the document is signed
|
||||
</Trans>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
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 type { Field } from '@prisma/client';
|
||||
import { DocumentSigningOrder, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GripVerticalIcon, HelpCircle, Plus, Trash } from 'lucide-react';
|
||||
@@ -19,6 +19,7 @@ import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounce
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||
import type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -54,12 +55,12 @@ import { SigningOrderConfirmation } from './signing-order-confirmation';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
type AutoSaveResponse = {
|
||||
recipients: Recipient[];
|
||||
recipients: TRecipientLite[];
|
||||
};
|
||||
|
||||
export type AddSignersFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
recipients: TRecipientLite[];
|
||||
fields: Field[];
|
||||
signingOrder?: DocumentSigningOrder | null;
|
||||
allowDictateNextSigner?: boolean;
|
||||
|
||||
@@ -3,16 +3,14 @@ import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZAddSignersFormSchema = z.object({
|
||||
signers: z.array(
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: msg`Invalid email`.id })
|
||||
.min(1),
|
||||
email: zEmail(msg`Invalid email`.id).min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
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 type { Field } from '@prisma/client';
|
||||
import { DocumentDistributionMethod, DocumentStatus, RecipientRole } from '@prisma/client';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
@@ -15,6 +15,7 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { TDocument } from '@documenso/lib/types/document';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||
@@ -59,7 +60,7 @@ import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSubjectFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
recipients: TRecipientLite[];
|
||||
fields: Field[];
|
||||
document: TDocument;
|
||||
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||
@@ -191,10 +192,10 @@ export const AddSubjectFormPartial = ({
|
||||
>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger className="w-full" value={DocumentDistributionMethod.EMAIL}>
|
||||
Email
|
||||
<Trans>Email</Trans>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value={DocumentDistributionMethod.NONE}>
|
||||
None
|
||||
<Trans>None</Trans>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@@ -2,14 +2,12 @@ import { DocumentDistributionMethod } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZAddSubjectFormSchema = z.object({
|
||||
meta: z.object({
|
||||
emailId: z.string().nullable(),
|
||||
emailReplyTo: z.preprocess(
|
||||
(val) => (val === '' ? undefined : val),
|
||||
z.string().email().optional(),
|
||||
),
|
||||
emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()),
|
||||
// emailReplyName: z.string().optional(),
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { CopyPlus, Settings2, SquareStack, Trash } from 'lucide-react';
|
||||
import { createPortal } from 'react-dom';
|
||||
@@ -9,11 +10,12 @@ import { Rnd } from 'react-rnd';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds';
|
||||
import { useIsPageInDom } from '@documenso/lib/client-only/hooks/use-is-page-in-dom';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import { useRecipientColors } from '../../lib/recipient-colors';
|
||||
import { getRecipientColorStyles } from '../../lib/recipient-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { FieldContent } from './field-content';
|
||||
import type { TDocumentFlowFormSchema } from './types';
|
||||
@@ -49,7 +51,17 @@ export type FieldItemProps = {
|
||||
/**
|
||||
* The item when editing fields??
|
||||
*/
|
||||
export const FieldItem = ({
|
||||
export const FieldItem = (props: FieldItemProps) => {
|
||||
const isPageInDom = useIsPageInDom(props.field.pageNumber);
|
||||
|
||||
if (!isPageInDom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <FieldItemInner {...props} />;
|
||||
};
|
||||
|
||||
const FieldItemInner = ({
|
||||
fieldClassName,
|
||||
field,
|
||||
passive,
|
||||
@@ -88,7 +100,7 @@ export const FieldItem = ({
|
||||
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
|
||||
);
|
||||
|
||||
const signerStyles = useRecipientColors(recipientIndex);
|
||||
const signerStyles = getRecipientColorStyles(recipientIndex);
|
||||
|
||||
const isDevMode = searchParams.get('devmode') === 'true';
|
||||
|
||||
@@ -321,22 +333,44 @@ export const FieldItem = ({
|
||||
</div>
|
||||
|
||||
{isDevMode && (
|
||||
<div className="absolute -top-20 left-1/2 z-50 -translate-x-1/2 rounded-md border border-border bg-background/95 px-2 py-1 shadow-sm backdrop-blur-sm">
|
||||
<div className="absolute bottom-full left-1/2 z-50 mb-1 -translate-x-1/2 rounded-md border border-border bg-background/95 px-2 py-1 shadow-sm backdrop-blur-sm">
|
||||
<div className="flex flex-col gap-0.5 text-[9px]">
|
||||
{field.nativeId && (
|
||||
<span>
|
||||
<span className="text-muted-foreground">
|
||||
<Trans>Field ID:</Trans>
|
||||
</span>{' '}
|
||||
<span className="font-mono text-foreground">{field.nativeId}</span>
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
<span className="text-muted-foreground">Pos X: </span>
|
||||
<span className="text-muted-foreground">
|
||||
<Trans>Recipient ID:</Trans>
|
||||
</span>{' '}
|
||||
<span className="font-mono text-foreground">{field.recipientId}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">
|
||||
<Trans>Pos X:</Trans>
|
||||
</span>{' '}
|
||||
<span className="font-mono text-foreground">{field.pageX.toFixed(2)}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">Pos Y: </span>
|
||||
<span className="text-muted-foreground">
|
||||
<Trans>Pos Y:</Trans>
|
||||
</span>{' '}
|
||||
<span className="font-mono text-foreground">{field.pageY.toFixed(2)}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">Width: </span>
|
||||
<span className="text-muted-foreground">
|
||||
<Trans>Width:</Trans>
|
||||
</span>{' '}
|
||||
<span className="font-mono text-foreground">{field.pageWidth.toFixed(2)}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">Height: </span>
|
||||
<span className="text-muted-foreground">
|
||||
<Trans>Height:</Trans>
|
||||
</span>{' '}
|
||||
<span className="font-mono text-foreground">{field.pageHeight.toFixed(2)}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+9
-9
@@ -36,14 +36,14 @@ export const DropdownFieldAdvancedSettings = ({
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [showValidation, setShowValidation] = useState(false);
|
||||
const [values, setValues] = useState(fieldState.values ?? [{ value: 'Option 1' }]);
|
||||
const [values, setValues] = useState(fieldState.values ?? [{ value: _(msg`Option 1`) }]);
|
||||
const [readOnly, setReadOnly] = useState(fieldState.readOnly ?? false);
|
||||
const [required, setRequired] = useState(fieldState.required ?? false);
|
||||
const [defaultValue, setDefaultValue] = useState(fieldState.defaultValue ?? 'Option 1');
|
||||
const [defaultValue, setDefaultValue] = useState(fieldState.defaultValue ?? _(msg`Option 1`));
|
||||
|
||||
const addValue = () => {
|
||||
setValues([...values, { value: 'New option' }]);
|
||||
handleFieldChange('values', [...values, { value: 'New option' }]);
|
||||
setValues([...values, { value: _(msg`New option`) }]);
|
||||
handleFieldChange('values', [...values, { value: _(msg`New option`) }]);
|
||||
};
|
||||
|
||||
const removeValue = (index: number) => {
|
||||
@@ -90,11 +90,11 @@ export const DropdownFieldAdvancedSettings = ({
|
||||
}, [values]);
|
||||
|
||||
useEffect(() => {
|
||||
setValues(fieldState.values ?? [{ value: 'Option 1' }]);
|
||||
setValues(fieldState.values ?? [{ value: _(msg`Option 1`) }]);
|
||||
}, [fieldState.values]);
|
||||
|
||||
useEffect(() => {
|
||||
setDefaultValue(fieldState.defaultValue ?? 'Option 1');
|
||||
setDefaultValue(fieldState.defaultValue ?? _(msg`Option 1`));
|
||||
}, [fieldState.defaultValue]);
|
||||
|
||||
return (
|
||||
@@ -114,7 +114,7 @@ export const DropdownFieldAdvancedSettings = ({
|
||||
handleFieldChange('defaultValue', val);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
|
||||
<SelectTrigger className="mt-2 w-full bg-background text-muted-foreground">
|
||||
<SelectValue defaultValue={defaultValue} placeholder={`-- ${_(msg`Select`)} --`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
@@ -152,7 +152,7 @@ export const DropdownFieldAdvancedSettings = ({
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 mt-2 border"
|
||||
className="mt-2 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
|
||||
variant="outline"
|
||||
onClick={() => setShowValidation((prev) => !prev)}
|
||||
>
|
||||
@@ -183,7 +183,7 @@ export const DropdownFieldAdvancedSettings = ({
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 ml-9 mt-4 border"
|
||||
className="ml-9 mt-4 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
|
||||
variant="outline"
|
||||
onClick={addValue}
|
||||
>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const RadioFieldAdvancedSettings = ({
|
||||
|
||||
const [showValidation, setShowValidation] = useState(false);
|
||||
const [values, setValues] = useState(
|
||||
fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }],
|
||||
fieldState.values ?? [{ id: 1, checked: false, value: _(msg`Default value`) }],
|
||||
);
|
||||
const [readOnly, setReadOnly] = useState(fieldState.readOnly ?? false);
|
||||
const [required, setRequired] = useState(fieldState.required ?? false);
|
||||
@@ -99,7 +99,7 @@ export const RadioFieldAdvancedSettings = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setValues(fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }]);
|
||||
setValues(fieldState.values ?? [{ id: 1, checked: false, value: _(msg`Default value`) }]);
|
||||
}, [fieldState.values]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -122,7 +122,7 @@ export const RadioFieldAdvancedSettings = ({
|
||||
</Label>
|
||||
<Input
|
||||
id="label"
|
||||
className="bg-background mt-2"
|
||||
className="mt-2 bg-background"
|
||||
placeholder={_(msg`Field label`)}
|
||||
value={fieldState.label}
|
||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
||||
@@ -150,7 +150,7 @@ export const RadioFieldAdvancedSettings = ({
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 mt-2 border"
|
||||
className="mt-2 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
|
||||
variant="outline"
|
||||
onClick={() => setShowValidation((prev) => !prev)}
|
||||
>
|
||||
@@ -167,7 +167,7 @@ export const RadioFieldAdvancedSettings = ({
|
||||
{values.map((value) => (
|
||||
<div key={value.id} className="mt-2 flex items-center gap-4">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-documenso border-foreground/30 data-[state=checked]:ring-primary dark:data-[state=checked]:ring-offset-background h-5 w-5 rounded-full data-[state=checked]:ring-1 data-[state=checked]:ring-offset-2 data-[state=checked]:ring-offset-white"
|
||||
className="h-5 w-5 rounded-full border-foreground/30 data-[state=checked]:bg-documenso data-[state=checked]:ring-1 data-[state=checked]:ring-primary data-[state=checked]:ring-offset-2 data-[state=checked]:ring-offset-white dark:data-[state=checked]:ring-offset-background"
|
||||
checked={value.checked}
|
||||
onCheckedChange={(checked) => handleCheckedChange(Boolean(checked), value.id)}
|
||||
/>
|
||||
@@ -186,7 +186,7 @@ export const RadioFieldAdvancedSettings = ({
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 ml-9 mt-4 border"
|
||||
className="ml-9 mt-4 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
|
||||
variant="outline"
|
||||
onClick={addValue}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FieldType } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZDocumentFlowFormSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
@@ -12,7 +13,7 @@ export const ZDocumentFlowFormSchema = z.object({
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().min(1).email(),
|
||||
email: zEmail().min(1),
|
||||
name: z.string(),
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import {
|
||||
CalendarDays,
|
||||
@@ -103,10 +104,10 @@ export const FieldSelector = ({
|
||||
)}
|
||||
>
|
||||
<CardContent className="relative flex items-center justify-center gap-x-2 px-6 py-4">
|
||||
{Icon && <Icon className="text-muted-foreground h-4 w-4" />}
|
||||
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
|
||||
<span
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground text-sm',
|
||||
'text-sm text-muted-foreground group-data-[selected]:text-foreground',
|
||||
field.type === FieldType.SIGNATURE && 'invisible',
|
||||
)}
|
||||
>
|
||||
@@ -114,8 +115,8 @@ export const FieldSelector = ({
|
||||
</span>
|
||||
|
||||
{field.type === FieldType.SIGNATURE && (
|
||||
<div className="text-muted-foreground font-signature absolute inset-0 flex items-center justify-center text-lg">
|
||||
Signature
|
||||
<div className="absolute inset-0 flex items-center justify-center font-signature text-lg text-muted-foreground">
|
||||
<Trans>Signature</Trans>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import {
|
||||
type LoadedPDFDocument,
|
||||
type OnPDFViewerPageClick,
|
||||
PDFViewer,
|
||||
type PDFViewerProps,
|
||||
} from './base';
|
||||
|
||||
export { PDFViewer, type LoadedPDFDocument, type OnPDFViewerPageClick, type PDFViewerProps };
|
||||
|
||||
export default PDFViewer;
|
||||
@@ -1,290 +0,0 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { EnvelopeItem } from '@prisma/client';
|
||||
import { base64 } from '@scure/base';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { useToast } from '../use-toast';
|
||||
|
||||
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||
|
||||
/**
|
||||
* This imports the worker from the `pdfjs-dist` package.
|
||||
* Wrapped in typeof window check to prevent SSR evaluation.
|
||||
*/
|
||||
if (typeof window !== 'undefined') {
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
}
|
||||
|
||||
const pdfViewerOptions = {
|
||||
cMapUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/static/cmaps/`,
|
||||
};
|
||||
|
||||
export type OnPDFViewerPageClick = (_event: {
|
||||
pageNumber: number;
|
||||
numPages: number;
|
||||
originalEvent: React.MouseEvent<HTMLDivElement, MouseEvent>;
|
||||
pageHeight: number;
|
||||
pageWidth: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
}) => void | Promise<void>;
|
||||
|
||||
const PDFLoader = () => (
|
||||
<>
|
||||
<Loader className="h-12 w-12 animate-spin text-documenso" />
|
||||
|
||||
<p className="mt-4 text-muted-foreground">
|
||||
<Trans>Loading document...</Trans>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
export type PDFViewerProps = {
|
||||
className?: string;
|
||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||
token: string | undefined;
|
||||
presignToken?: string | undefined;
|
||||
version: 'original' | 'signed';
|
||||
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
|
||||
onPageClick?: OnPDFViewerPageClick;
|
||||
overrideData?: string;
|
||||
customPageRenderer?: React.FunctionComponent;
|
||||
[key: string]: unknown;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
|
||||
|
||||
export const PDFViewer = ({
|
||||
className,
|
||||
envelopeItem,
|
||||
token,
|
||||
presignToken,
|
||||
version,
|
||||
onDocumentLoad,
|
||||
onPageClick,
|
||||
overrideData,
|
||||
customPageRenderer,
|
||||
...props
|
||||
}: PDFViewerProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const $el = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false);
|
||||
const [documentBytes, setDocumentBytes] = useState<Uint8Array | null>(
|
||||
overrideData ? base64.decode(overrideData) : null,
|
||||
);
|
||||
|
||||
const [width, setWidth] = useState(0);
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [pdfError, setPdfError] = useState(false);
|
||||
|
||||
const isLoading = isDocumentBytesLoading || !documentBytes;
|
||||
|
||||
const envelopeItemFile = useMemo(() => {
|
||||
if (!documentBytes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
data: documentBytes,
|
||||
};
|
||||
}, [documentBytes]);
|
||||
|
||||
const onDocumentLoaded = (doc: LoadedPDFDocument) => {
|
||||
setNumPages(doc.numPages);
|
||||
onDocumentLoad?.(doc);
|
||||
};
|
||||
|
||||
const onDocumentPageClick = (
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
pageNumber: number,
|
||||
) => {
|
||||
const $el = event.target instanceof HTMLElement ? event.target : null;
|
||||
|
||||
if (!$el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $page = $el.closest(PDF_VIEWER_PAGE_SELECTOR);
|
||||
|
||||
if (!$page) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { height, width, top, left } = $page.getBoundingClientRect();
|
||||
|
||||
const pageX = event.clientX - left;
|
||||
const pageY = event.clientY - top;
|
||||
|
||||
if (onPageClick) {
|
||||
void onPageClick({
|
||||
pageNumber,
|
||||
numPages,
|
||||
originalEvent: event,
|
||||
pageHeight: height,
|
||||
pageWidth: width,
|
||||
pageX,
|
||||
pageY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if ($el.current) {
|
||||
const $current = $el.current;
|
||||
|
||||
const { width } = $current.getBoundingClientRect();
|
||||
|
||||
setWidth(width);
|
||||
|
||||
const onResize = () => {
|
||||
const { width } = $current.getBoundingClientRect();
|
||||
|
||||
setWidth(width);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (overrideData) {
|
||||
const bytes = base64.decode(overrideData);
|
||||
|
||||
setDocumentBytes(bytes);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchDocumentBytes = async () => {
|
||||
try {
|
||||
setIsDocumentBytesLoading(true);
|
||||
|
||||
const documentUrl = getEnvelopeItemPdfUrl({
|
||||
type: 'view',
|
||||
envelopeItem: envelopeItem,
|
||||
token,
|
||||
presignToken,
|
||||
});
|
||||
|
||||
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||
|
||||
setDocumentBytes(new Uint8Array(bytes));
|
||||
|
||||
setIsDocumentBytesLoading(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while loading the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
void fetchDocumentBytes();
|
||||
}, [envelopeItem.envelopeId, envelopeItem.id, token, version, toast, overrideData]);
|
||||
|
||||
return (
|
||||
<div ref={$el} className={cn('overflow-hidden', className)} {...props}>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-[80vh] max-h-[60rem] w-full flex-col items-center justify-center overflow-hidden rounded',
|
||||
)}
|
||||
>
|
||||
<PDFLoader />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<PDFDocument
|
||||
file={envelopeItemFile}
|
||||
className={cn('w-full overflow-hidden rounded', {
|
||||
'h-[80vh] max-h-[60rem]': numPages === 0,
|
||||
})}
|
||||
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.
|
||||
onSourceError={() => {
|
||||
setPdfError(true);
|
||||
}}
|
||||
externalLinkTarget="_blank"
|
||||
loading={
|
||||
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50 dark:bg-background">
|
||||
{pdfError ? (
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>
|
||||
<Trans>Something went wrong while loading the document.</Trans>
|
||||
</p>
|
||||
<p className="mt-1 text-sm">
|
||||
<Trans>Please try again or contact our support.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<PDFLoader />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
error={
|
||||
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50 dark:bg-background">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>
|
||||
<Trans>Something went wrong while loading the document.</Trans>
|
||||
</p>
|
||||
<p className="mt-1 text-sm">
|
||||
<Trans>Please try again or contact our support.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
options={pdfViewerOptions}
|
||||
>
|
||||
{Array(numPages)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<div key={i} className="last:-mb-2">
|
||||
<div className="overflow-hidden rounded border border-border will-change-transform">
|
||||
<PDFPage
|
||||
pageNumber={i + 1}
|
||||
width={width}
|
||||
renderAnnotationLayer={false}
|
||||
renderTextLayer={false}
|
||||
loading={() => ''}
|
||||
renderMode={customPageRenderer ? 'custom' : 'canvas'}
|
||||
customRenderer={customPageRenderer}
|
||||
onClick={(e) => onDocumentPageClick(e, i + 1)}
|
||||
/>
|
||||
</div>
|
||||
<p className="my-2 text-center text-[11px] text-muted-foreground/80">
|
||||
<Trans>
|
||||
Page {i + 1} of {numPages}
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</PDFDocument>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFViewer;
|
||||
@@ -1 +0,0 @@
|
||||
export * from './base';
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ClientOnly } from '../../components/client-only';
|
||||
|
||||
import { PDFViewer, type PDFViewerProps } from './base.client';
|
||||
|
||||
export const PDFViewerLazy = (props: PDFViewerProps) => {
|
||||
return <ClientOnly fallback={<div>Loading...</div>}>{() => <PDFViewer {...props} />}</ClientOnly>;
|
||||
};
|
||||
@@ -2,12 +2,12 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
|
||||
import { Check, ChevronsUpDown, Info } from 'lucide-react';
|
||||
import { sortBy } from 'remeda';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
|
||||
import { getRecipientColorStyles } from '../lib/recipient-colors';
|
||||
import { cn } from '../lib/utils';
|
||||
@@ -18,9 +18,9 @@ import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
|
||||
|
||||
export interface RecipientSelectorProps {
|
||||
className?: string;
|
||||
selectedRecipient: Recipient | null;
|
||||
onSelectedRecipientChange: (recipient: Recipient) => void;
|
||||
recipients: Recipient[];
|
||||
selectedRecipient: TRecipientLite | null;
|
||||
onSelectedRecipientChange: (recipient: TRecipientLite) => void;
|
||||
recipients: TRecipientLite[];
|
||||
align?: 'center' | 'end' | 'start';
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const RecipientSelector = ({
|
||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||
|
||||
const recipientsByRole = useMemo(() => {
|
||||
const recipientsWithRole: Record<RecipientRole, Recipient[]> = {
|
||||
const recipientsWithRole: Record<RecipientRole, TRecipientLite[]> = {
|
||||
CC: [],
|
||||
VIEWER: [],
|
||||
SIGNER: [],
|
||||
@@ -67,12 +67,12 @@ export const RecipientSelector = ({
|
||||
[(r) => r.signingOrder || Number.MAX_SAFE_INTEGER, 'asc'],
|
||||
[(r) => r.id, 'asc'],
|
||||
),
|
||||
] as [RecipientRole, Recipient[]],
|
||||
] as [RecipientRole, TRecipientLite[]],
|
||||
);
|
||||
}, [recipientsByRole]);
|
||||
|
||||
const getRecipientLabel = useCallback(
|
||||
(recipient: Recipient) => {
|
||||
(recipient: TRecipientLite) => {
|
||||
if (recipient.name && recipient.email) {
|
||||
return `${recipient.name} (${recipient.email})`;
|
||||
}
|
||||
@@ -101,13 +101,9 @@ export const RecipientSelector = ({
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
'bg-background text-muted-foreground hover:text-foreground justify-between font-normal',
|
||||
getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
||||
0,
|
||||
),
|
||||
).comboxBoxTrigger,
|
||||
'justify-between bg-background font-normal text-muted-foreground hover:text-foreground',
|
||||
getRecipientColorStyles(recipients.findIndex((r) => r.id === selectedRecipient?.id))
|
||||
.comboBoxTrigger,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -126,21 +122,21 @@ export const RecipientSelector = ({
|
||||
<CommandInput />
|
||||
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
<span className="inline-block px-4 text-muted-foreground">
|
||||
<Trans>No recipient matching this description was found.</Trans>
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
|
||||
<CommandGroup key={roleIndex}>
|
||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||
<div className="mb-1 ml-2 mt-2 text-xs font-medium text-muted-foreground">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
||||
</div>
|
||||
|
||||
{roleRecipients.length === 0 && (
|
||||
<div
|
||||
key={`${role}-empty`}
|
||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
||||
className="px-4 pb-4 pt-2.5 text-center text-xs text-muted-foreground/80"
|
||||
>
|
||||
<Trans>No recipients with this role</Trans>
|
||||
</div>
|
||||
@@ -151,12 +147,8 @@ export const RecipientSelector = ({
|
||||
key={recipient.id}
|
||||
className={cn(
|
||||
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
|
||||
getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === recipient.id),
|
||||
0,
|
||||
),
|
||||
).comboxBoxItem,
|
||||
getRecipientColorStyles(recipients.findIndex((r) => r.id === recipient.id))
|
||||
.comboBoxItem,
|
||||
{
|
||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||
},
|
||||
@@ -168,7 +160,7 @@ export const RecipientSelector = ({
|
||||
disabled={recipient.signingStatus !== SigningStatus.NOT_SIGNED}
|
||||
>
|
||||
<span
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
className={cn('truncate text-foreground/70', {
|
||||
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
||||
})}
|
||||
>
|
||||
@@ -190,7 +182,7 @@ export const RecipientSelector = ({
|
||||
<Info className="ml-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<TooltipContent className="max-w-xs text-muted-foreground">
|
||||
<Trans>
|
||||
This document has already been sent to this recipient. You can no longer
|
||||
edit this recipient.
|
||||
|
||||
@@ -23,7 +23,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border bg-transparent px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
disabled={loading || props.disabled}
|
||||
@@ -56,7 +56,7 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground animate-in fade-in-80 relative z-[1001] min-w-[8rem] overflow-hidden rounded-md border shadow-md',
|
||||
'relative z-[1001] min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80',
|
||||
position === 'popper' && 'translate-y-1',
|
||||
className,
|
||||
)}
|
||||
@@ -65,7 +65,7 @@ const SelectContent = React.forwardRef<
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
'max-h-60 overflow-y-auto p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
||||
)}
|
||||
@@ -98,7 +98,7 @@ const SelectItem = React.forwardRef<
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -121,7 +121,7 @@ const SelectSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('bg-muted -mx-1 my-1 h-px', className)}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
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 type { Field } from '@prisma/client';
|
||||
import { FieldType, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import {
|
||||
CalendarDays,
|
||||
@@ -24,13 +24,14 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR, getPdfPagesCount } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
import {
|
||||
type TFieldMetaSchema as FieldMeta,
|
||||
ZFieldMetaSchema,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||
@@ -57,7 +58,7 @@ import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/type
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { getRecipientColorStyles, useRecipientColors } from '../../lib/recipient-colors';
|
||||
import { getRecipientColorStyles } from '../../lib/recipient-colors';
|
||||
import type { FieldFormType } from '../document-flow/add-fields';
|
||||
import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings';
|
||||
import { Form } from '../form/form';
|
||||
@@ -75,7 +76,7 @@ const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
|
||||
|
||||
export type AddTemplateFieldsFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
recipients: TRecipientLite[];
|
||||
fields: Field[];
|
||||
onSubmit: (_data: TAddTemplateFieldsFormSchema) => void;
|
||||
onAutoSave: (_data: TAddTemplateFieldsFormSchema) => Promise<void>;
|
||||
@@ -154,13 +155,11 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
});
|
||||
|
||||
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
|
||||
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
|
||||
const [selectedSigner, setSelectedSigner] = useState<TRecipientLite | null>(null);
|
||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||
|
||||
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
|
||||
const selectedSignerStyles = useRecipientColors(
|
||||
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
|
||||
);
|
||||
const selectedSignerStyles = getRecipientColorStyles(selectedSignerIndex);
|
||||
|
||||
const onFieldCopy = useCallback(
|
||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
||||
@@ -188,13 +187,15 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
}
|
||||
|
||||
if (duplicateAll) {
|
||||
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
|
||||
const totalPages = getPdfPagesCount();
|
||||
|
||||
pages.forEach((_, index) => {
|
||||
const pageNumber = index + 1;
|
||||
if (totalPages < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
|
||||
if (pageNumber === lastActiveField.pageNumber) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
|
||||
@@ -208,7 +209,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
};
|
||||
|
||||
append(newField);
|
||||
});
|
||||
}
|
||||
|
||||
void handleAutoSave();
|
||||
return;
|
||||
@@ -491,7 +492,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
}, [recipients]);
|
||||
|
||||
const recipientsByRole = useMemo(() => {
|
||||
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
||||
const recipientsByRole: Record<RecipientRole, TRecipientLite[]> = {
|
||||
CC: [],
|
||||
VIEWER: [],
|
||||
SIGNER: [],
|
||||
@@ -520,7 +521,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
|
||||
const recipientsByRoleToDisplay = useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
|
||||
return (Object.entries(recipientsByRole) as [RecipientRole, TRecipientLite[]][]).filter(
|
||||
([role]) =>
|
||||
role !== RecipientRole.CC &&
|
||||
role !== RecipientRole.VIEWER &&
|
||||
@@ -651,7 +652,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
role="combobox"
|
||||
className={cn(
|
||||
'mb-12 mt-2 justify-between bg-background font-normal text-muted-foreground hover:text-foreground',
|
||||
selectedSignerStyles?.comboxBoxTrigger,
|
||||
selectedSignerStyles?.comboBoxTrigger,
|
||||
)}
|
||||
>
|
||||
{selectedSigner?.email &&
|
||||
@@ -708,11 +709,8 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
className={cn(
|
||||
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
|
||||
getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === recipient.id),
|
||||
0,
|
||||
),
|
||||
)?.comboxBoxItem,
|
||||
recipients.findIndex((r) => r.id === recipient.id),
|
||||
)?.comboBoxItem,
|
||||
)}
|
||||
onSelect={() => {
|
||||
setSelectedSigner(recipient);
|
||||
@@ -797,7 +795,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
)}
|
||||
>
|
||||
<Contact className="h-4 w-4" />
|
||||
Initials
|
||||
<Trans>Initials</Trans>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -953,7 +951,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
)}
|
||||
>
|
||||
<Disc className="h-4 w-4" />
|
||||
Radio
|
||||
<Trans>Radio</Trans>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -979,8 +977,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
)}
|
||||
>
|
||||
<CheckSquare className="h-4 w-4" />
|
||||
{/* Not translated on purpose. */}
|
||||
Checkbox
|
||||
<Trans>Checkbox</Trans>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 { DocumentSigningOrder, type Field, RecipientRole } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GripVerticalIcon, HelpCircle, Link2Icon, Plus, Trash } from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
@@ -17,6 +17,7 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||
import type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
|
||||
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||
@@ -49,12 +50,12 @@ import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-
|
||||
import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
|
||||
|
||||
type AutoSaveResponse = {
|
||||
recipients: Recipient[];
|
||||
recipients: TRecipientLite[];
|
||||
};
|
||||
|
||||
export type AddTemplatePlaceholderRecipientsFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
recipients: TRecipientLite[];
|
||||
fields: Field[];
|
||||
signingOrder?: DocumentSigningOrder | null;
|
||||
allowDictateNextSigner?: boolean;
|
||||
@@ -533,7 +534,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground ml-1 cursor-help">
|
||||
<span className="ml-1 cursor-help text-muted-foreground">
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -586,7 +587,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={cn('py-1', {
|
||||
'bg-widget-foreground pointer-events-none rounded-md pt-2':
|
||||
'pointer-events-none rounded-md bg-widget-foreground pt-2':
|
||||
snapshot.isDragging,
|
||||
})}
|
||||
>
|
||||
@@ -754,7 +755,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
handleRoleChange(index, value as RecipientRole);
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
hideCCRecipients={isSignerDirectRecipient(signer)}
|
||||
hideCCerRole={isSignerDirectRecipient(signer)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -768,11 +769,11 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
<TooltipTrigger className="col-span-1 mt-auto inline-flex h-10 w-10 items-center justify-center text-slate-500 hover:opacity-80">
|
||||
<Link2Icon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||
<h3 className="text-foreground text-lg font-semibold">
|
||||
<TooltipContent className="z-9999 max-w-md p-4 text-foreground">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
<Trans>Direct link receiver</Trans>
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
<Trans>
|
||||
This field cannot be modified or deleted. When you share
|
||||
this template's direct link or add it to your public
|
||||
@@ -829,7 +830,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10"
|
||||
className="bg-black/5 hover:bg-black/10 dark:bg-muted dark:hover:bg-muted/80"
|
||||
variant="secondary"
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
@@ -852,7 +853,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 text-sm"
|
||||
className="ml-2 text-sm text-muted-foreground"
|
||||
htmlFor="showAdvancedRecipientSettings"
|
||||
>
|
||||
<Trans>Show advanced settings</Trans>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
.object({
|
||||
@@ -9,7 +10,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().min(1).email(),
|
||||
email: zEmail().min(1),
|
||||
name: z.string().min(1, { message: 'Name is required' }),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useEffect } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentDistributionMethod, type Field, type Recipient } from '@prisma/client';
|
||||
import { DocumentVisibility, TeamMemberRole, TemplateType } from '@prisma/client';
|
||||
import { DocumentDistributionMethod, type Field } from '@prisma/client';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
@@ -19,6 +19,7 @@ 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 { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
|
||||
@@ -36,6 +37,10 @@ import {
|
||||
DocumentVisibilitySelect,
|
||||
DocumentVisibilityTooltip,
|
||||
} from '@documenso/ui/components/document/document-visibility-select';
|
||||
import {
|
||||
TemplateTypeSelect,
|
||||
TemplateTypeTooltip,
|
||||
} from '@documenso/ui/components/template/template-type-select';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -77,7 +82,7 @@ import { ZAddTemplateSettingsFormSchema } from './add-template-settings.types';
|
||||
|
||||
export type AddTemplateSettingsFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
recipients: TRecipientLite[];
|
||||
fields: Field[];
|
||||
isDocumentPdfLoaded: boolean;
|
||||
template: TTemplate;
|
||||
@@ -96,7 +101,7 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
onSubmit,
|
||||
onAutoSave,
|
||||
}: AddTemplateSettingsFormProps) => {
|
||||
const { t, i18n } = useLingui();
|
||||
const { t } = useLingui();
|
||||
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
@@ -109,6 +114,7 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
defaultValues: {
|
||||
title: template.title,
|
||||
externalId: template.externalId || undefined,
|
||||
templateType: template.type || TemplateType.PRIVATE,
|
||||
visibility: template.visibility || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||
@@ -262,7 +268,7 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
<SelectContent>
|
||||
{Object.entries(SUPPORTED_LANGUAGES).map(([code, language]) => (
|
||||
<SelectItem key={code} value={code}>
|
||||
{language.full}
|
||||
{t(language.full)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -325,6 +331,29 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="templateType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Template type</Trans>
|
||||
<TemplateTypeTooltip organisationTeamCount={organisation.teams.length} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<TemplateTypeSelect
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
void handleAutoSave();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.distributionMethod"
|
||||
@@ -391,7 +420,7 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
{Object.values(DOCUMENT_DISTRIBUTION_METHODS).map(
|
||||
({ value, description }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{i18n._(description)}
|
||||
{t(description)}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentDistributionMethod } from '@prisma/client';
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { DocumentVisibility, TemplateType } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
@@ -17,10 +17,12 @@ import {
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/lib/types/document-meta';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
|
||||
export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||
externalId: z.string().optional(),
|
||||
templateType: z.nativeEnum(TemplateType).optional(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
globalAccessAuth: z
|
||||
.array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')]))
|
||||
@@ -49,10 +51,7 @@ export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
.optional()
|
||||
.default('en'),
|
||||
emailId: z.string().nullable(),
|
||||
emailReplyTo: z.preprocess(
|
||||
(val) => (val === '' ? undefined : val),
|
||||
z.string().email().optional(),
|
||||
),
|
||||
emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()),
|
||||
emailSettings: ZDocumentEmailSettingsSchema,
|
||||
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
|
||||
message: msg`At least one signature type must be enabled`.id,
|
||||
|
||||
@@ -8,7 +8,14 @@ const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
const TooltipTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>
|
||||
>(({ type = 'button', ...props }, ref) => (
|
||||
<TooltipPrimitive.Trigger ref={ref} type={type} {...props} />
|
||||
));
|
||||
|
||||
TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
|
||||
@@ -137,6 +137,8 @@
|
||||
/* Surface */
|
||||
--new-surface-black: 0, 0%, 0%;
|
||||
--new-surface-white: 0, 0%, 91%;
|
||||
|
||||
--envelope-editor-background: 210 40% 98.04%;
|
||||
}
|
||||
|
||||
.dark:not(.dark-mode-disabled) {
|
||||
@@ -181,6 +183,8 @@
|
||||
--warning: 54 96% 45%;
|
||||
|
||||
--gold: 47.9 95.8% 53.1%;
|
||||
|
||||
--envelope-editor-background: 0 0% 14.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +234,15 @@
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.scrollbar-hidden {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hidden::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* .custom-scrollbar::-webkit-scrollbar-track {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const baseConfig = require('@documenso/tailwind-config');
|
||||
const { RECIPIENT_DYNAMIC_CLASS } = require('./lib/recipient-colors');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
presets: [baseConfig],
|
||||
content: [
|
||||
...baseConfig.content,
|
||||
'./primitives/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./lib/**/*.{ts,tsx}',
|
||||
],
|
||||
safelist: [RECIPIENT_DYNAMIC_CLASS],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user