Compare commits

..

1 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan 9499a03668 fix: improve checkbox field rendering and interaction 2025-02-25 13:48:27 +00:00
108 changed files with 15918 additions and 15691 deletions
@@ -31,11 +31,6 @@ Our new API V2 supports the following typed SDKs:
- [Python](https://github.com/documenso/sdk-python)
- [Go](https://github.com/documenso/sdk-go)
<Callout type="info">
For the staging API, please use the following base URL:
`https://stg-app.documenso.dev/api/v2-beta/`
</Callout>
🚀 [V2 Announcement](https://documen.so/sdk-blog)
📖 [Documentation](https://documen.so/api-v2-docs)
@@ -1,7 +1,7 @@
import { DocumentStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => {
const qb = kyselyPrisma.$kysely
@@ -13,7 +13,7 @@ import {
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
@@ -97,7 +97,7 @@ export const DocumentDuplicateDialog = ({
</div>
) : (
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll">
<PDFViewer key={document?.id} documentData={documentData} />
<LazyPDFViewer key={document?.id} documentData={documentData} />
</div>
)}
@@ -114,7 +114,7 @@ export const TemplateBulkSendDialog = ({
<Dialog>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="outline">
<Button>
<Upload className="mr-2 h-4 w-4" />
<Trans>Bulk Send via CSV</Trans>
</Button>
@@ -16,9 +16,9 @@ export type EmbedAuthenticationRequiredProps = {
export const EmbedAuthenticationRequired = ({
email,
returnTo,
// isGoogleSSOEnabled,
// isOIDCSSOEnabled,
// oidcProviderLabel,
isGoogleSSOEnabled,
isOIDCSSOEnabled,
oidcProviderLabel,
}: EmbedAuthenticationRequiredProps) => {
return (
<div className="flex min-h-[100dvh] w-full items-center justify-center">
@@ -35,10 +35,9 @@ export const EmbedAuthenticationRequired = ({
</Alert>
<SignInForm
// Embed currently not supported.
// isGoogleSSOEnabled={isGoogleSSOEnabled}
// isOIDCSSOEnabled={isOIDCSSOEnabled}
// oidcProviderLabel={oidcProviderLabel}
isGoogleSSOEnabled={isGoogleSSOEnabled}
isOIDCSSOEnabled={isOIDCSSOEnabled}
oidcProviderLabel={oidcProviderLabel}
className="mt-4"
initialEmail={email}
returnTo={returnTo}
@@ -25,7 +25,7 @@ import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -338,7 +338,7 @@ export const EmbedDirectTemplateClientPage = ({
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="flex-1">
<PDFViewer
<LazyPDFViewer
documentData={documentData}
onDocumentLoad={() => setHasDocumentLoaded(true)}
/>
@@ -347,7 +347,7 @@ export const EmbedDirectTemplateClientPage = ({
{/* Widget */}
<div
key={isExpanded ? 'expanded' : 'collapsed'}
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
@@ -24,7 +24,7 @@ import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -278,7 +278,7 @@ export const EmbedSignDocumentClientPage = ({
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="embed--DocumentViewer flex-1">
<PDFViewer
<LazyPDFViewer
documentData={documentData}
onDocumentLoad={() => setHasDocumentLoaded(true)}
/>
@@ -287,7 +287,7 @@ export const EmbedSignDocumentClientPage = ({
{/* Widget */}
<div
key={isExpanded ? 'expanded' : 'collapsed'}
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
@@ -9,10 +9,6 @@ import { z } from 'zod';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import type { TTemplate } from '@documenso/lib/types/template';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '@documenso/ui/components/document/document-read-only-fields';
import {
DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent,
@@ -20,6 +16,7 @@ import {
DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep,
} from '@documenso/ui/primitives/document-flow/document-flow-root';
import { ShowFieldItem } from '@documenso/ui/primitives/document-flow/show-field-item';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import {
Form,
@@ -100,14 +97,14 @@ export const DirectTemplateConfigureForm = ({
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
<DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && (
<DocumentReadOnlyFields
fields={mapFieldsWithRecipients(
directTemplateRecipient.fields,
recipientsWithBlankDirectRecipientEmail,
)}
/>
)}
{isDocumentPdfLoaded &&
directTemplateRecipient.fields.map((field, index) => (
<ShowFieldItem
key={index}
field={field}
recipients={recipientsWithBlankDirectRecipientEmail}
/>
))}
<Form {...form}>
<fieldset
@@ -12,7 +12,7 @@ import { trpc } from '@documenso/trpc/react';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -136,7 +136,7 @@ export const DirectTemplatePageView = ({
gradient
>
<CardContent className="p-2">
<PDFViewer
<LazyPDFViewer
key={template.id}
documentData={template.templateDocumentData}
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
@@ -45,24 +45,26 @@ export const DocumentSigningCheckboxField = ({
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const parsedFieldMeta = ZCheckboxFieldMeta.parse(
field.fieldMeta ?? {
type: 'checkbox',
values: [{ id: 1, checked: false, value: '' }],
},
);
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
const values = parsedFieldMeta.values?.map((item) => ({
...item,
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
}));
const parsedCheckedValues = useMemo(
() => fromCheckboxValue(field.customText),
[field.customText],
);
const [checkedValues, setCheckedValues] = useState(
values
?.map((item) =>
item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
)
.filter(Boolean) || [],
field.inserted && parsedCheckedValues.length > 0
? parsedCheckedValues
: values
?.map((item) =>
item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
)
.filter(Boolean) || [],
);
const isReadOnly = parsedFieldMeta.readOnly;
@@ -177,19 +179,13 @@ export const DocumentSigningCheckboxField = ({
let updatedValues: string[] = [];
try {
const isChecked = checkedValues.includes(
item.value.length > 0 ? item.value : `empty-value-${item.id}`,
);
const itemValue = item.value.length > 0 ? item.value : `empty-value-${item.id}`;
const isChecked = checkedValues.includes(itemValue);
if (!isChecked) {
updatedValues = [
...checkedValues,
item.value.length > 0 ? item.value : `empty-value-${item.id}`,
];
updatedValues = [...checkedValues, itemValue];
} else {
updatedValues = checkedValues.filter(
(v) => v !== item.value && v !== `empty-value-${item.id}`,
);
updatedValues = checkedValues.filter((v) => v !== itemValue);
}
setCheckedValues(updatedValues);
@@ -220,6 +216,12 @@ export const DocumentSigningCheckboxField = ({
}
};
useEffect(() => {
if (field.inserted && parsedCheckedValues.length > 0) {
setCheckedValues(parsedCheckedValues);
}
}, [field.inserted, parsedCheckedValues]);
useEffect(() => {
if (shouldAutoSignField) {
void executeActionAuthProcedure({
@@ -229,11 +231,6 @@ export const DocumentSigningCheckboxField = ({
}
}, [checkedValues, isLengthConditionMet, field.inserted]);
const parsedCheckedValues = useMemo(
() => fromCheckboxValue(field.customText),
[field.customText],
);
return (
<DocumentSigningFieldContainer
field={field}
@@ -254,19 +251,20 @@ export const DocumentSigningCheckboxField = ({
{validationSign?.label} {checkboxValidationLength}
</FieldToolTip>
)}
<div className="z-50 my-0.5 flex flex-col gap-y-1">
<div className="z-50 flex flex-col gap-y-2">
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
const itemValue = item.value || `empty-value-${item.id}`;
const checkboxId = `checkbox-field-${field.id}-${index}`;
return (
<div key={index} className="flex items-center gap-x-1.5">
<Checkbox
className="h-3 w-3"
id={`checkbox-${index}`}
className="h-4 w-4"
id={checkboxId}
checked={checkedValues.includes(itemValue)}
onCheckedChange={() => handleCheckboxChange(item.value, item.id)}
/>
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal">
<Label htmlFor={checkboxId}>
{item.value.includes('empty-value-') ? '' : item.value}
</Label>
</div>
@@ -277,20 +275,21 @@ export const DocumentSigningCheckboxField = ({
)}
{field.inserted && (
<div className="my-0.5 flex flex-col gap-y-1">
<div className="flex flex-col gap-y-2">
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
const itemValue = item.value || `empty-value-${item.id}`;
const checkboxId = `checkbox-field-${field.id}-${index}-inserted`;
return (
<div key={index} className="flex items-center gap-x-1.5">
<Checkbox
className="h-3 w-3"
id={`checkbox-${index}`}
checked={parsedCheckedValues.includes(itemValue)}
className="h-4 w-4"
id={checkboxId}
checked={checkedValues.includes(itemValue)}
disabled={isLoading}
onCheckedChange={() => void handleCheckboxOptionClick(item)}
/>
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal">
<Label htmlFor={checkboxId}>
{item.value.includes('empty-value-') ? '' : item.value}
</Label>
</div>
@@ -151,10 +151,12 @@ export const DocumentSigningDateField = ({
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'!text-center': parsedFieldMeta?.textAlign === 'center',
'!text-right': parsedFieldMeta?.textAlign === 'right',
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
@@ -136,10 +136,12 @@ export const DocumentSigningEmailField = ({
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'!text-center': parsedFieldMeta?.textAlign === 'center',
'!text-right': parsedFieldMeta?.textAlign === 'right',
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
@@ -181,23 +181,6 @@ export const DocumentSigningFieldContainer = ({
</button>
)}
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
field.fieldMeta?.label && (
<div
className={cn(
'absolute -top-16 left-0 right-0 rounded-md p-2 text-center text-xs text-gray-700',
{
'bg-foreground/5 border-border border': !field.inserted,
},
{
'bg-documenso-200 border-primary border': field.inserted,
},
)}
>
{field.fieldMeta.label}
</div>
)}
{children}
</FieldRootContainer>
</div>
@@ -182,10 +182,12 @@ export const DocumentSigningNameField = ({
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'!text-center': parsedFieldMeta?.textAlign === 'center',
'!text-right': parsedFieldMeta?.textAlign === 'right',
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
@@ -272,10 +272,12 @@ export const DocumentSigningNumberField = ({
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'!text-center': parsedFieldMeta?.textAlign === 'center',
'!text-right': parsedFieldMeta?.textAlign === 'right',
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
@@ -19,10 +19,9 @@ import {
import type { CompletedField } from '@documenso/lib/types/fields';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { DocumentSigningAutoSign } from '~/components/general/document-signing/document-signing-auto-sign';
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
@@ -37,6 +36,7 @@ import { DocumentSigningRadioField } from '~/components/general/document-signing
import { DocumentSigningRejectDialog } from '~/components/general/document-signing/document-signing-reject-dialog';
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
import { DocumentSigningRecipientProvider } from './document-signing-recipient-provider';
@@ -140,7 +140,12 @@ export const DocumentSigningPageView = ({
gradient
>
<CardContent className="p-2">
<PDFViewer key={documentData.id} documentData={documentData} document={document} />
<LazyPDFViewer
key={documentData.id}
documentData={documentData}
document={document}
password={documentMeta?.password}
/>
</CardContent>
</Card>
@@ -157,20 +157,17 @@ export const DocumentSigningRadioField = ({
)}
{!field.inserted && (
<RadioGroup
onValueChange={(value) => handleSelectItem(value)}
className="z-10 my-0.5 gap-y-1"
>
<RadioGroup onValueChange={(value) => handleSelectItem(value)} className="z-10">
{values?.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5">
<RadioGroupItem
className="h-3 w-3 shrink-0"
className="h-4 w-4 shrink-0"
value={item.value}
id={`option-${index}`}
checked={item.checked}
/>
<Label htmlFor={`option-${index}`} className="text-xs font-normal">
<Label htmlFor={`option-${index}`}>
{item.value.includes('empty-value-') ? '' : item.value}
</Label>
</div>
@@ -179,7 +176,7 @@ export const DocumentSigningRadioField = ({
)}
{field.inserted && (
<RadioGroup className="my-0.5 gap-y-1">
<RadioGroup className="gap-y-1">
{values?.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5">
<RadioGroupItem
@@ -188,7 +185,7 @@ export const DocumentSigningRadioField = ({
id={`option-${index}`}
checked={item.value === field.customText}
/>
<Label htmlFor={`option-${index}`} className="text-xs font-normal">
<Label htmlFor={`option-${index}`} className="text-xs">
{item.value.includes('empty-value-') ? '' : item.value}
</Label>
</div>
@@ -277,11 +277,12 @@ export const DocumentSigningTextField = ({
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
// Todo: Test
'!text-center': parsedFieldMeta?.textAlign === 'center',
'!text-right': parsedFieldMeta?.textAlign === 'right',
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
@@ -303,9 +304,11 @@ export const DocumentSigningTextField = ({
id="custom-text"
placeholder={parsedFieldMeta?.placeholder ?? _(msg`Enter your text here`)}
className={cn('mt-2 w-full rounded-md', {
'border-2 border-red-300 text-left ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200':
'border-2 border-red-300 ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200':
userInputHasErrors,
'text-center': parsedFieldMeta?.textAlign === 'center',
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
})}
value={localText}
@@ -24,7 +24,7 @@ import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/ad
import type { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -132,6 +132,9 @@ export const DocumentEditForm = ({
},
});
const { mutateAsync: setPasswordForDocument } =
trpc.document.setPasswordForDocument.useMutation();
const documentFlow: Record<EditDocumentStep, DocumentFlowStep> = {
settings: {
title: msg`General`,
@@ -312,6 +315,13 @@ export const DocumentEditForm = ({
}
};
const onPasswordSubmit = async (password: string) => {
await setPasswordForDocument({
documentId: document.id,
password,
});
};
const currentDocumentFlow = documentFlow[step];
/**
@@ -330,10 +340,12 @@ export const DocumentEditForm = ({
gradient
>
<CardContent className="p-2">
<PDFViewer
<LazyPDFViewer
key={document.documentData.id}
documentData={document.documentData}
document={document}
password={document.documentMeta?.password}
onPasswordSubmit={onPasswordSubmit}
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
/>
</CardContent>
@@ -0,0 +1,171 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentMeta } from '@prisma/client';
import { FieldType, SigningStatus } from '@prisma/client';
import { Clock, EyeOffIcon } from 'lucide-react';
import { P, match } from 'ts-pattern';
import {
DEFAULT_DOCUMENT_DATE_FORMAT,
convertToLocalSystemFormat,
} from '@documenso/lib/constants/date-formats';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { cn } from '@documenso/ui/lib/utils';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge';
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { PopoverHover } from '@documenso/ui/primitives/popover';
export type DocumentReadOnlyFieldsProps = {
fields: DocumentField[];
documentMeta?: DocumentMeta;
showFieldStatus?: boolean;
};
export const DocumentReadOnlyFields = ({
documentMeta,
fields,
showFieldStatus = true,
}: DocumentReadOnlyFieldsProps) => {
const { _ } = useLingui();
const [hiddenFieldIds, setHiddenFieldIds] = useState<Record<string, boolean>>({});
const handleHideField = (fieldId: string) => {
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
};
return (
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{fields.map(
(field) =>
!hiddenFieldIds[field.secondaryId] && (
<FieldRootContainer
field={field}
key={field.id}
cardClassName="border-gray-300/50 !shadow-none backdrop-blur-[1px] bg-gray-50 ring-0 ring-offset-0"
>
<div className="absolute -right-3 -top-3">
<PopoverHover
trigger={
<Avatar className="dark:border-foreground h-8 w-8 border-2 border-solid border-gray-200/50 transition-colors hover:border-gray-200">
<AvatarFallback className="bg-neutral-50 text-xs text-gray-400">
{extractInitials(field.recipient.name || field.recipient.email)}
</AvatarFallback>
</Avatar>
}
contentProps={{
className: 'relative flex w-fit flex-col p-4 text-sm',
}}
>
{showFieldStatus && (
<Badge
className="mx-auto mb-1 py-0.5"
variant={
field.recipient.signingStatus === SigningStatus.SIGNED
? 'default'
: 'secondary'
}
>
{field.recipient.signingStatus === SigningStatus.SIGNED ? (
<>
<SignatureIcon className="mr-1 h-3 w-3" />
<Trans>Signed</Trans>
</>
) : (
<>
<Clock className="mr-1 h-3 w-3" />
<Trans>Pending</Trans>
</>
)}
</Badge>
)}
<p className="text-center font-semibold">
<span>{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field</span>
</p>
<p className="text-muted-foreground mt-1 text-center text-xs">
{field.recipient.name
? `${field.recipient.name} (${field.recipient.email})`
: field.recipient.email}{' '}
</p>
<button
className="absolute right-0 top-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
onClick={() => handleHideField(field.secondaryId)}
title="Hide field"
>
<EyeOffIcon className="h-3 w-3" />
</button>
</PopoverHover>
</div>
<div className="text-muted-foreground dark:text-background/70 break-all text-sm">
{field.recipient.signingStatus === SigningStatus.SIGNED &&
match(field)
.with({ type: FieldType.SIGNATURE }, (field) =>
field.signature?.signatureImageAsBase64 ? (
<img
src={field.signature.signatureImageAsBase64}
alt="Signature"
className="h-full w-full object-contain dark:invert"
/>
) : (
<p className="font-signature text-muted-foreground text-lg duration-200 sm:text-xl md:text-2xl">
{field.signature?.typedSignature}
</p>
),
)
.with(
{
type: P.union(
FieldType.NAME,
FieldType.INITIALS,
FieldType.EMAIL,
FieldType.NUMBER,
FieldType.RADIO,
FieldType.CHECKBOX,
FieldType.DROPDOWN,
),
},
() => field.customText,
)
.with({ type: FieldType.TEXT }, () => field.customText.substring(0, 20) + '...')
.with({ type: FieldType.DATE }, () =>
convertToLocalSystemFormat(
field.customText,
documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
),
)
.with({ type: FieldType.FREE_SIGNATURE }, () => null)
.exhaustive()}
{field.recipient.signingStatus === SigningStatus.NOT_SIGNED && (
<p
className={cn('text-muted-foreground text-lg duration-200', {
'font-signature sm:text-xl md:text-2xl':
field.type === FieldType.SIGNATURE ||
field.type === FieldType.FREE_SIGNATURE,
})}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}
</p>
)}
</div>
</FieldRootContainer>
),
)}
</ElementVisible>
);
};
@@ -15,7 +15,7 @@ import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { AddTemplateFieldsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-fields';
import type { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types';
@@ -236,7 +236,7 @@ export const TemplateEditForm = ({
gradient
>
<CardContent className="p-2">
<PDFViewer
<LazyPDFViewer
key={templateDocumentData.id}
documentData={templateDocumentData}
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
-1
View File
@@ -17,7 +17,6 @@ function PosthogInit() {
if (postHogConfig) {
posthog.init(postHogConfig.key, {
api_host: postHogConfig.host,
capture_exceptions: true,
});
}
}, []);
-2
View File
@@ -1,7 +1,6 @@
import { useEffect } from 'react';
import Plausible from 'plausible-tracker';
import posthog from 'posthog-js';
import {
Links,
Meta,
@@ -181,7 +180,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (errorCode !== 404) {
console.error('[RootErrorBoundary]', error);
posthog.captureException(error);
}
return <GenericErrorLayout errorCode={errorCode} />;
@@ -20,7 +20,7 @@ import type { Route } from './+types/_layout';
*/
export const shouldRevalidate = () => false;
export async function loader({ request }: Route.LoaderArgs) {
export const loader = async ({ request }: Route.LoaderArgs) => {
const requestHeaders = Object.fromEntries(request.headers.entries());
const session = await getOptionalSession(request);
@@ -40,7 +40,7 @@ export async function loader({ request }: Route.LoaderArgs) {
banner,
limits,
};
}
};
export default function Layout({ loaderData }: Route.ComponentProps) {
const { user, teams } = useSession();
@@ -13,11 +13,10 @@ import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/g
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { DocumentHistorySheet } from '~/components/general/document/document-history-sheet';
import { DocumentPageViewButton } from '~/components/general/document/document-page-view-button';
@@ -25,6 +24,7 @@ import { DocumentPageViewDropdown } from '~/components/general/document/document
import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity';
import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients';
import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
import {
DocumentStatus as DocumentStatusComponent,
@@ -196,16 +196,12 @@ export default function DocumentPage() {
gradient
>
<CardContent className="p-2">
<PDFViewer document={document} key={documentData.id} documentData={documentData} />
<LazyPDFViewer document={document} key={documentData.id} documentData={documentData} />
</CardContent>
</Card>
{document.status !== DocumentStatus.COMPLETED && (
<DocumentReadOnlyFields
fields={fields}
documentMeta={documentMeta || undefined}
showRecipientTooltip={true}
/>
{document.status === DocumentStatus.PENDING && (
<DocumentReadOnlyFields fields={fields} documentMeta={documentMeta || undefined} />
)}
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
@@ -16,7 +16,6 @@ import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscriptio
import { BillingPlans } from '~/components/general/billing-plans';
import { BillingPortalButton } from '~/components/general/billing-portal-button';
import { appMetaTags } from '~/utils/meta';
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/billing';
@@ -63,17 +62,17 @@ export async function loader({ request }: Route.LoaderArgs) {
const isMissingOrInactiveOrFreePlan =
!subscription || subscription.status === SubscriptionStatus.INACTIVE;
return superLoaderJson({
return {
prices,
subscription,
subscriptionProductName: subscriptionProduct?.name,
isMissingOrInactiveOrFreePlan,
});
};
}
export default function TeamsSettingBillingPage() {
export default function TeamsSettingBillingPage({ loaderData }: Route.ComponentProps) {
const { prices, subscription, subscriptionProductName, isMissingOrInactiveOrFreePlan } =
useSuperLoaderData<typeof loader>();
loaderData;
const { i18n } = useLingui();
@@ -14,7 +14,6 @@ import { Card, CardContent } from '@documenso/ui/primitives/card';
import { SettingsHeader } from '~/components/general/settings-header';
import { TeamBillingPortalButton } from '~/components/general/teams/team-billing-portal-button';
import { TeamSettingsBillingInvoicesTable } from '~/components/tables/team-settings-billing-invoices-table';
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/settings.billing';
@@ -32,16 +31,16 @@ export async function loader({ request, params }: Route.LoaderArgs) {
teamSubscription = await stripe.subscriptions.retrieve(team.subscription.planId);
}
return superLoaderJson({
return {
team,
teamSubscription,
});
};
}
export default function TeamsSettingBillingPage() {
export default function TeamsSettingBillingPage({ loaderData }: Route.ComponentProps) {
const { _ } = useLingui();
const { team, teamSubscription } = useSuperLoaderData<typeof loader>();
const { team, teamSubscription } = loaderData;
const canManageBilling = canExecuteTeamAction('MANAGE_BILLING', team.currentTeamMember.role);
@@ -7,14 +7,14 @@ import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { TemplateBulkSendDialog } from '~/components/dialogs/template-bulk-send-dialog';
import { TemplateDirectLinkDialogWrapper } from '~/components/dialogs/template-direct-link-dialog-wrapper';
import { TemplateUseDialog } from '~/components/dialogs/template-use-dialog';
import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge';
import { TemplatePageViewDocumentsTable } from '~/components/general/template/template-page-view-documents-table';
import { TemplatePageViewInformation } from '~/components/general/template/template-page-view-information';
@@ -144,14 +144,17 @@ export default function TemplatePage() {
gradient
>
<CardContent className="p-2">
<PDFViewer document={template} key={template.id} documentData={templateDocumentData} />
<LazyPDFViewer
document={template}
key={template.id}
documentData={templateDocumentData}
/>
</CardContent>
</Card>
<DocumentReadOnlyFields
fields={readOnlyFields}
showFieldStatus={false}
showRecipientTooltip={true}
documentMeta={mockedDocumentMeta}
/>
+3 -36
View File
@@ -1,47 +1,14 @@
import { redirect } from 'react-router';
import { extractCookieFromHeaders } from '@documenso/auth/server/lib/utils/cookies';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { ZTeamUrlSchema } from '@documenso/trpc/server/team-router/schema';
import type { Route } from './+types/_index';
export async function loader({ request }: Route.LoaderArgs) {
const session = await getOptionalSession(request);
const { isAuthenticated } = await getOptionalSession(request);
if (session.isAuthenticated) {
const teamUrlCookie = extractCookieFromHeaders('preferred-team-url', request.headers);
const referrer = request.headers.get('referer');
let isReferrerFromTeamUrl = false;
if (referrer) {
const referrerUrl = new URL(referrer);
if (referrerUrl.pathname.startsWith('/t/')) {
isReferrerFromTeamUrl = true;
}
}
const preferredTeamUrl =
teamUrlCookie && ZTeamUrlSchema.safeParse(teamUrlCookie).success ? teamUrlCookie : undefined;
// Early return for no preferred team.
if (!preferredTeamUrl || isReferrerFromTeamUrl) {
throw redirect('/documents');
}
const teams = await getTeams({ userId: session.user.id });
const currentTeam = teams.find((team) => team.url === preferredTeamUrl);
if (!currentTeam) {
throw redirect('/documents');
}
throw redirect(formatDocumentsPath(currentTeam.url));
if (isAuthenticated) {
throw redirect('/documents');
}
throw redirect('/signin');
+1 -3
View File
@@ -145,9 +145,7 @@ export default function EmbedDirectTemplatePage() {
recipient={recipient}
fields={fields}
metadata={template.templateMeta}
hidePoweredBy={
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
}
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
allowWhiteLabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
/>
</DocumentSigningRecipientProvider>
+1 -3
View File
@@ -169,9 +169,7 @@ export default function EmbedSignDocumentPage() {
fields={fields}
metadata={document.documentMeta}
isCompleted={document.status === DocumentStatus.COMPLETED}
hidePoweredBy={
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
}
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
allowWhitelabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
allRecipients={allRecipients}
/>
-1
View File
@@ -1 +0,0 @@
export default {};
+3 -3
View File
@@ -49,8 +49,8 @@
"luxon": "^3.4.0",
"papaparse": "^5.4.1",
"plausible-tracker": "^0.3.9",
"posthog-js": "^1.224.0",
"posthog-node": "^4.8.1",
"posthog-js": "^1.223.3",
"posthog-node": "^4.7.0",
"react": "^18",
"react-call": "^1.3.0",
"react-dom": "^18",
@@ -76,7 +76,7 @@
"@babel/preset-typescript": "^7.26.0",
"@lingui/babel-plugin-lingui-macro": "^5.2.0",
"@lingui/vite-plugin": "^5.2.0",
"@react-router/dev": "^7.1.5",
"@react-router/dev": "^7.1.1",
"@react-router/remix-routes-option-adapter": "^7.1.5",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.2",
-75
View File
@@ -1,75 +0,0 @@
import type { Context, Next } from 'hono';
import { deleteCookie, setCookie } from 'hono/cookie';
import { AppDebugger } from '@documenso/lib/utils/debugger';
const debug = new AppDebugger('Middleware');
/**
* Middleware for initial page loads.
*
* You won't be able to easily handle sequential page loads because they will be
* called under `path.data`
*
* Example an initial page load would be `/documents` then if the user click templates
* the path here would be `/templates.data`.
*/
export const appMiddleware = async (c: Context, next: Next) => {
const { req } = c;
const { path } = req;
// Paths to ignore.
if (nonPagePathRegex.test(path)) {
return next();
}
// PRE-HANDLER CODE: Place code here to execute BEFORE the route handler runs.
await next();
// POST-HANDLER CODE: Place code here to execute AFTER the route handler completes.
// This is useful for:
// - Setting cookies
// - Any operations that should happen after all route handlers but before sending the response
debug.log('Path', path);
const pathname = path.replace('.data', '');
const referrer = c.req.header('referer');
const referrerUrl = referrer ? new URL(referrer) : null;
const referrerPathname = referrerUrl ? referrerUrl.pathname : null;
// Whether to reset the preferred team url cookie if the user accesses a non team page from a team page.
const resetPreferredTeamUrl =
referrerPathname &&
referrerPathname.startsWith('/t/') &&
(!pathname.startsWith('/t/') || pathname === '/');
// Set the preferred team url cookie if user accesses a team page.
if (pathname.startsWith('/t/')) {
debug.log('Setting preferred team url cookie');
setCookie(c, 'preferred-team-url', pathname.split('/')[2], {
sameSite: 'lax',
});
return;
}
// Clear preferred team url cookie if user accesses a non team page from a team page.
if (resetPreferredTeamUrl || pathname === '/documents') {
debug.log('Deleting preferred team url cookie');
deleteCookie(c, 'preferred-team-url');
return;
}
};
// This regex matches any path that:
// 1. Starts with /api/, /ingest/, /__manifest/, or /assets/
// 2. Starts with /apple- (like /apple-touch-icon.png)
// 3. Starts with /favicon (like /favicon.ico)
// The ^ ensures matching from the beginning of the string
// The | acts as OR operator between different patterns
const nonPagePathRegex = /^(\/api\/|\/ingest\/|\/__manifest|\/assets\/|\/apple-.*|\/favicon.*)/;
-6
View File
@@ -9,7 +9,6 @@ import { openApiDocument } from '@documenso/trpc/server/open-api';
import { filesRoute } from './api/files';
import { type AppContext, appContext } from './context';
import { appMiddleware } from './middleware';
import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
import { reactRouterTrpcServer } from './trpc/hono-trpc-remix';
@@ -27,11 +26,6 @@ const app = new Hono<HonoEnv>();
app.use(contextStorage());
app.use(appContext);
/**
* RR7 app middleware.
*/
app.use('*', appMiddleware);
// Auth server.
app.route('/api/auth', auth);
-1
View File
@@ -46,7 +46,6 @@ export default defineConfig({
https: 'node:https',
'.prisma/client/default': '../../node_modules/.prisma/client/default.js',
'.prisma/client/index-browser': '../../node_modules/.prisma/client/index-browser.js',
canvas: './app/types/empty-module.ts',
},
},
/**
-3
View File
@@ -1,6 +1,4 @@
import { defineConfig } from '@lingui/cli';
import type { LinguiConfig } from '@lingui/conf';
import { formatter } from '@lingui/format-po';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
@@ -16,7 +14,6 @@ const config: LinguiConfig = {
},
],
compileNamespace: 'es',
format: formatter({ lineNumbers: false }),
};
export default config;
+9 -10
View File
@@ -131,8 +131,8 @@
"luxon": "^3.4.0",
"papaparse": "^5.4.1",
"plausible-tracker": "^0.3.9",
"posthog-js": "^1.224.0",
"posthog-node": "^4.8.1",
"posthog-js": "^1.223.3",
"posthog-node": "^4.7.0",
"react": "^18",
"react-call": "^1.3.0",
"react-dom": "^18",
@@ -158,7 +158,7 @@
"@babel/preset-typescript": "^7.26.0",
"@lingui/babel-plugin-lingui-macro": "^5.2.0",
"@lingui/vite-plugin": "^5.2.0",
"@react-router/dev": "^7.1.5",
"@react-router/dev": "^7.1.1",
"@react-router/remix-routes-option-adapter": "^7.1.5",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.2",
@@ -902,9 +902,9 @@
}
},
"apps/remix/node_modules/posthog-node": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.8.1.tgz",
"integrity": "sha512-ApMEC1+DbctP/88+VhaCl8SRKpIoReibMf7Mb3rxw3yMthr1rKaM4opbHdZJ0buLhwS5zX8B2ckqLjpwpSjRPg==",
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.7.0.tgz",
"integrity": "sha512-RgdUKSW8MfMOkjUa8cYVqWndNjPePNuuxlGbrZC6z1WRBsVc6TdGl8caidmC10RW8mu/BOfmrGbP4cRTo2jARg==",
"license": "MIT",
"dependencies": {
"axios": "^1.7.4"
@@ -31060,9 +31060,9 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.224.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.224.0.tgz",
"integrity": "sha512-JT1XQQeYs0CKb4lU2vujmeLTDLWc61I5lT7d6oG/H/cnCpXAqBi5rMuCFFeotHeMy3hqJ/Tpu3eAPFE2p5ErHA==",
"version": "1.223.3",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.223.3.tgz",
"integrity": "sha512-ZQTc17M21IzkQmECJa2Xjont4tZrvIn252uGT3sTfmahTqZoW4j+kBj4eOJt9SNR6hOheFNkg7MSiI/rA6FaDA==",
"license": "MIT",
"dependencies": {
"core-js": "^3.38.1",
@@ -41567,7 +41567,6 @@
"pdf-lib": "^1.17.1",
"pg": "^8.11.3",
"playwright": "1.43.0",
"posthog-js": "^1.224.0",
"react": "^18",
"remeda": "^2.17.3",
"sharp": "0.32.6",
+7 -2
View File
@@ -1,5 +1,3 @@
import type { Prisma } from '@prisma/client';
import { DocumentDataType, DocumentStatus, SigningStatus, TeamMemberRole } from '@prisma/client';
import { tsr } from '@ts-rest/serverless/fetch';
import { match } from 'ts-pattern';
@@ -52,6 +50,13 @@ import {
} from '@documenso/lib/universal/upload/server-actions';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import {
DocumentDataType,
DocumentStatus,
SigningStatus,
TeamMemberRole,
} from '@documenso/prisma/client';
import { ApiContractV1 } from './contract';
import { authenticatedMiddleware } from './middleware/authenticated';
+1 -1
View File
@@ -1,10 +1,10 @@
import type { Team, User } from '@prisma/client';
import type { TsRestRequest } from '@ts-rest/serverless';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { Team, User } from '@documenso/prisma/client';
type B = {
// appRoute: any;
+12 -12
View File
@@ -1,16 +1,4 @@
import { extendZodWithOpenApi } from '@anatine/zod-openapi';
import {
DocumentDataType,
DocumentDistributionMethod,
DocumentSigningOrder,
FieldType,
ReadStatus,
RecipientRole,
SendStatus,
SigningStatus,
TeamMemberRole,
TemplateType,
} from '@prisma/client';
import { z } from 'zod';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@@ -24,6 +12,18 @@ import {
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import {
DocumentDataType,
DocumentDistributionMethod,
DocumentSigningOrder,
FieldType,
ReadStatus,
RecipientRole,
SendStatus,
SigningStatus,
TeamMemberRole,
TemplateType,
} from '@documenso/prisma/client';
extendZodWithOpenApi(z);
@@ -1,5 +1,4 @@
import { expect, test } from '@playwright/test';
import { TeamMemberRole } from '@prisma/client';
import {
ZFindTeamMembersResponseSchema,
@@ -11,6 +10,7 @@ import {
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -1,11 +1,11 @@
import { expect, test } from '@playwright/test';
import { FieldType } from '@prisma/client';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import {
createDocumentAuthOptions,
createRecipientAuthOptions,
} from '@documenso/lib/utils/document-auth';
import { FieldType } from '@documenso/prisma/client';
import {
seedPendingDocumentNoFields,
seedPendingDocumentWithFullFields,
@@ -1,16 +1,16 @@
import { expect, test } from '@playwright/test';
import { DateTime } from 'luxon';
import path from 'node:path';
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
import { prisma } from '@documenso/prisma';
import {
DocumentSigningOrder,
DocumentStatus,
FieldType,
RecipientRole,
SigningStatus,
} from '@prisma/client';
import { DateTime } from 'luxon';
import path from 'node:path';
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
import { prisma } from '@documenso/prisma';
} from '@documenso/prisma/client';
import {
seedBlankDocument,
seedPendingDocumentWithFullFields,
@@ -1,10 +1,10 @@
import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType } from '@prisma/client';
import { PDFDocument } from 'pdf-lib';
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
import { seedTeam, seedTeamEmail, seedTeamMember } from '@documenso/prisma/seed/teams';
@@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test';
import { TeamMemberRole } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@@ -12,7 +12,7 @@ import { apiSignin } from '../fixtures/authentication';
test.describe.configure({ mode: 'parallel' });
test.describe('[EE_ONLY]', () => {
const enterprisePriceId = '';
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
test.beforeEach(() => {
test.skip(
@@ -1,11 +1,11 @@
import { expect, test } from '@playwright/test';
import { DocumentDataType, TeamMemberRole } from '@prisma/client';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { DocumentDataType, TeamMemberRole } from '@documenso/prisma/client';
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@@ -15,7 +15,7 @@ import { apiSignin } from '../fixtures/authentication';
test.describe.configure({ mode: 'parallel' });
const enterprisePriceId = '';
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
// Create a temporary PDF file for testing
function createTempPdfFile() {
@@ -10,7 +10,6 @@ import { appLog } from '@documenso/lib/utils/debugger';
import { env } from '@documenso/lib/utils/env';
import { AUTH_SESSION_LIFETIME } from '../../config';
import { extractCookieFromHeaders } from '../utils/cookies';
import { generateSessionToken } from './session';
export const sessionCookieName = formatSecureCookieName('sessionId');
@@ -39,7 +38,15 @@ export const sessionCookieOptions = {
} as const;
export const extractSessionCookieFromHeaders = (headers: Headers): string | null => {
return extractCookieFromHeaders(sessionCookieName, headers);
const cookieHeader = headers.get('cookie') || '';
const cookiePairs = cookieHeader.split(';');
const sessionCookie = cookiePairs.find((pair) => pair.trim().startsWith(sessionCookieName));
if (!sessionCookie) {
return null;
}
return sessionCookie.split('=')[1].trim();
};
/**
-14
View File
@@ -1,14 +0,0 @@
/**
* Todo: Use library for cookies instead.
*/
export const extractCookieFromHeaders = (cookieName: string, headers: Headers): string | null => {
const cookieHeader = headers.get('cookie') || '';
const cookiePairs = cookieHeader.split(';');
const cookie = cookiePairs.find((pair) => pair.trim().startsWith(cookieName));
if (!cookie) {
return null;
}
return cookie.split('=')[1].trim();
};
@@ -1,4 +1,3 @@
import { UserSecurityAuditLogType } from '@prisma/client';
import { OAuth2Client, decodeIdToken } from 'arctic';
import type { Context } from 'hono';
import { deleteCookie } from 'hono/cookie';
@@ -7,6 +6,7 @@ import { nanoid } from 'nanoid';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import type { OAuthClientOptions } from '../../config';
import { AuthenticationErrorCode } from '../errors/error-codes';
@@ -1,6 +1,5 @@
import { sValidator } from '@hono/standard-validator';
import { compare } from '@node-rs/bcrypt';
import { UserSecurityAuditLogType } from '@prisma/client';
import { Hono } from 'hono';
import { DateTime } from 'luxon';
import { z } from 'zod';
@@ -23,6 +22,7 @@ import { updatePassword } from '@documenso/lib/server-only/user/update-password'
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
import { getCsrfCookie } from '../lib/session/session-cookies';
+1 -1
View File
@@ -1,8 +1,8 @@
import { DocumentSource, SubscriptionStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { prisma } from '@documenso/prisma';
import { DocumentSource, SubscriptionStatus } from '@documenso/prisma/client';
import { getDocumentRelatedPrices } from '../stripe/get-document-related-prices.ts';
import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS, TEAM_PLAN_LIMITS } from './constants';
@@ -1,8 +1,7 @@
import type { User } from '@prisma/client';
import { STRIPE_CUSTOMER_TYPE } from '@documenso/lib/constants/billing';
import { stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import type { User } from '@documenso/prisma/client';
import { onSubscriptionUpdated } from './webhook/on-subscription-updated';
@@ -4,14 +4,15 @@ import { stripe } from '@documenso/lib/server-only/stripe';
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
const planTypes: string[] = typeof plan === 'string' ? [plan] : plan;
const planTypes = typeof plan === 'string' ? [plan] : plan;
const prices = await stripe.prices.list({
const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR ');
const { data: prices } = await stripe.prices.search({
query,
expand: ['data.product'],
limit: 100,
});
return prices.data.filter(
(price) => price.type === 'recurring' && planTypes.includes(price.metadata.plan),
);
return prices.filter((price) => price.type === 'recurring');
};
@@ -1,10 +1,10 @@
import { type Subscription, type Team, type User } from '@prisma/client';
import type Stripe from 'stripe';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { stripe } from '@documenso/lib/server-only/stripe';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import { type Subscription, type Team, type User } from '@documenso/prisma/client';
import { deleteCustomerPaymentMethods } from './delete-customer-payment-methods';
import { getTeamPrices } from './get-team-prices';
@@ -1,7 +1,6 @@
import { SubscriptionStatus } from '@prisma/client';
import type { Stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client';
export type OnSubscriptionDeletedOptions = {
subscription: Stripe.Subscription;
@@ -1,9 +1,9 @@
import type { Prisma } from '@prisma/client';
import { SubscriptionStatus } from '@prisma/client';
import { match } from 'ts-pattern';
import type { Stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
import { SubscriptionStatus } from '@documenso/prisma/client';
export type OnSubscriptionUpdatedOptions = {
userId?: number;
@@ -1,7 +1,6 @@
import type { Subscription } from '@prisma/client';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import type { Subscription } from '@documenso/prisma/client';
import { getCommunityPlanPriceIds } from '../stripe/get-community-plan-prices';
@@ -1,8 +1,7 @@
import type { Subscription } from '@prisma/client';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import type { Subscription } from '@documenso/prisma/client';
import { getEnterprisePlanPriceIds } from '../stripe/get-enterprise-plan-prices';
@@ -1,8 +1,7 @@
import type { Document, Subscription } from '@prisma/client';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import type { Document, Subscription } from '@documenso/prisma/client';
import { getPlatformPlanPriceIds } from '../stripe/get-platform-plan-prices';
@@ -2,10 +2,10 @@ import { useMemo } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { RecipientRole } from '@prisma/client';
import { P, match } from 'ts-pattern';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { RecipientRole } from '@documenso/prisma/client';
import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
@@ -1,9 +1,9 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { RecipientRole } from '@prisma/client';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { RecipientRole } from '@documenso/prisma/client';
import { Body, Button, Container, Head, Html, Img, Preview, Section, Text } from '../components';
import { useBranding } from '../providers/branding';
+1 -1
View File
@@ -1,9 +1,9 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { RecipientRole } from '@prisma/client';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { RecipientRole } from '@documenso/prisma/client';
import { Body, Container, Head, Hr, Html, Img, Link, Preview, Section, Text } from '../components';
import { useBranding } from '../providers/branding';
@@ -1,12 +1,12 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import React from 'react';
import type { Session } from '@prisma/client';
import { useLocation } from 'react-router';
import { authClient } from '@documenso/auth/client';
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { type TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import type { Session } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/client';
export type AppSession = {
@@ -1,7 +1,6 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import type { TeamGlobalSettings } from '@prisma/client';
import { parse } from 'csv-parse/sync';
import { z } from 'zod';
@@ -11,6 +10,7 @@ import { sendDocument } from '@documenso/lib/server-only/document/send-document'
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { prisma } from '@documenso/prisma';
import type { TeamGlobalSettings } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
-1
View File
@@ -45,7 +45,6 @@
"pdf-lib": "^1.17.1",
"pg": "^8.11.3",
"playwright": "1.43.0",
"posthog-js": "^1.224.0",
"react": "^18",
"remeda": "^2.17.3",
"sharp": "0.32.6",
@@ -17,6 +17,7 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
import { flattenAnnotations } from '../pdf/flatten-annotations';
import { flattenForm } from '../pdf/flatten-form';
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
@@ -103,14 +104,13 @@ export const sealDocument = async ({
// !: Need to write the fields onto the document as a hard copy
const pdfData = await getFileServerSide(documentData);
// debugging........
// const certificateData =
// (document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
// ? await getCertificatePdf({
// documentId,
// language: document.documentMeta?.language,
// }).catch(() => null)
// : null;
const certificateData =
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
const doc = await PDFDocument.load(pdfData);
@@ -119,15 +119,15 @@ export const sealDocument = async ({
flattenForm(doc);
flattenAnnotations(doc);
// if (certificateData) {
// const certificate = await PDFDocument.load(certificateData);
if (certificateData) {
const certificate = await PDFDocument.load(certificateData);
// const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices());
const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices());
// certificatePages.forEach((page) => {
// doc.addPage(page);
// });
// }
certificatePages.forEach((page) => {
doc.addPage(page);
});
}
for (const field of fields) {
await insertFieldInPDF(doc, field);
@@ -1,6 +1,5 @@
import { FieldType, RecipientRole, SigningStatus } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { FieldType, RecipientRole, SigningStatus } from '@documenso/prisma/client';
export type GetFieldsForTokenOptions = {
token: string;
@@ -36,8 +36,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const isSignatureField = isSignatureFieldType(field.type);
const isDebugMode =
// eslint-disable-next-line turbo/no-undeclared-env-vars
false; // todo
// true || process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true';
process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true';
pdf.registerFontkit(fontkit);
@@ -228,13 +227,8 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const selected: string[] = fromCheckboxValue(field.customText);
const topPadding = 13;
const leftCheckboxPadding = 6;
const leftCheckboxLabelPadding = 12;
const checkboxSpaceY = 13;
for (const [index, item] of (values ?? []).entries()) {
const offsetY = index * checkboxSpaceY + topPadding;
const offsetY = index * 16;
const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`);
@@ -243,7 +237,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
}
page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
x: fieldX + leftCheckboxPadding + leftCheckboxLabelPadding,
x: fieldX + 16,
y: pageHeight - (fieldY + offsetY),
size: 12,
font,
@@ -251,7 +245,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
});
checkbox.addToPage(page, {
x: fieldX + leftCheckboxPadding,
x: fieldX,
y: pageHeight - (fieldY + offsetY),
height: 8,
width: 8,
@@ -274,28 +268,21 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const selected = field.customText.split(',');
const topPadding = 13;
const leftRadioPadding = 6;
const leftRadioLabelPadding = 12;
const radioSpaceY = 13;
for (const [index, item] of (values ?? []).entries()) {
const offsetY = index * radioSpaceY + topPadding;
const offsetY = index * 16;
const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`);
// Draw label.
page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
x: fieldX + leftRadioPadding + leftRadioLabelPadding,
x: fieldX + 16,
y: pageHeight - (fieldY + offsetY),
size: 12,
font,
rotate: degrees(pageRotationInDegrees),
});
// Draw radio button.
radio.addOptionToPage(item.value, page, {
x: fieldX + leftRadioPadding,
x: fieldX,
y: pageHeight - (fieldY + offsetY),
height: 8,
width: 8,
@@ -321,7 +308,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const meta = Parser ? Parser.safeParse(field.fieldMeta) : null;
const customFontSize = meta?.success && meta.data.fontSize ? meta.data.fontSize : null;
const textAlign = meta?.success && meta.data.textAlign ? meta.data.textAlign : 'left'; // ???
const textAlign = meta?.success && meta.data.textAlign ? meta.data.textAlign : 'center';
const longestLineInTextForWidth = field.customText
.split('\n')
.sort((a, b) => b.length - a.length)[0];
@@ -338,69 +325,41 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
// Add padding similar to web display (roughly 0.5rem equivalent in PDF units)
const padding = 8; // Todo: Play around with this.
const padding = 8; // PDF points, roughly equivalent to 0.5rem
// Calculate X position based on text alignment with padding
let textX = fieldX + padding; // Left alignment starts after padding
if (textAlign === 'center') {
textX = fieldX + (fieldWidth - textWidth) / 2; // Center alignment ignores padding
} else if (textAlign === 'right') {
textX = fieldX + fieldWidth - textWidth - padding; // Right alignment respects right padding
}
// Invert the Y axis since PDFs use a bottom-left coordinate system
let textFieldBoxY = pageHeight - fieldY - fieldHeight;
let textFieldBoxX = textX;
let textY = fieldY + (fieldHeight - textHeight) / 2;
const textField = pdf.getForm().createTextField(`text.${field.secondaryId}`);
// Invert the Y axis since PDFs use a bottom-left coordinate system
textY = pageHeight - textY - textHeight;
if (pageRotationInDegrees !== 0) {
const adjustedPosition = adjustPositionForRotation(
pageWidth,
pageHeight,
textFieldBoxX,
textFieldBoxY,
textX,
textY,
pageRotationInDegrees,
);
textFieldBoxX = adjustedPosition.xPos;
textFieldBoxY = adjustedPosition.yPos;
textX = adjustedPosition.xPos;
textY = adjustedPosition.yPos;
}
if (isDebugMode) {
page.drawRectangle({
x: textFieldBoxX,
y: textFieldBoxY,
width: textWidth,
height: textHeight,
borderColor: rgb(1, 0, 0),
borderWidth: 1,
rotate: degrees(pageRotationInDegrees),
});
}
// Set the position and size of the text field
textField.addToPage(page, {
x: textFieldBoxX,
y: textFieldBoxY,
width: fieldWidth,
height: fieldHeight,
borderWidth: 0, // Hide border.
borderColor: rgb(1, 1, 1), // Hide border.
backgroundColor: undefined, // Makes transparent so background doesn't cover other text.
page.drawText(field.customText, {
x: textX,
y: textY,
size: fontSize,
font,
rotate: degrees(pageRotationInDegrees),
// Draw debug box if debug mode is enabled.
...(isDebugMode && {
borderColor: rgb(1, 0, 0),
borderWidth: 1,
}),
});
// Set properties for the text field
textField.setFontSize(fontSize);
textField.setText(field.customText);
});
return pdf;
@@ -1,6 +1,5 @@
import { TeamMemberRole } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
export type GetApiTokensOptions = {
userId: number;
@@ -1,6 +1,5 @@
import { FieldType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { FieldType } from '@documenso/prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
import type { Document, DocumentData, Recipient } from '@prisma/client';
import type { Document, DocumentData, Recipient } from '@documenso/prisma/client';
export type DocumentWithRecipients = Document & {
recipients: Recipient[];
@@ -1,6 +1,5 @@
import type { Field, Signature } from '@prisma/client';
import { type TFieldMetaSchema as FieldMeta } from '@documenso/lib/types/field-meta';
import type { Field, Signature } from '@documenso/prisma/client';
export type FieldWithSignatureAndFieldMeta = Field & {
signature?: Signature | null;
@@ -1,4 +1,4 @@
import type { Field, Signature } from '@prisma/client';
import type { Field, Signature } from '@documenso/prisma/client';
export type FieldWithSignature = Field & {
signature?: Signature | null;
@@ -1,4 +1,4 @@
import type { Field, Recipient } from '@prisma/client';
import type { Field, Recipient } from '@documenso/prisma/client';
export type RecipientWithFields = Recipient & {
fields: Field[];
-8
View File
@@ -103,14 +103,6 @@ module.exports = {
900: '#364772',
950: '#252d46',
},
signer: {
green: 'hsl(var(--signer-green))',
blue: 'hsl(var(--signer-blue))',
purple: 'hsl(var(--signer-purple))',
orange: 'hsl(var(--signer-orange))',
yellow: 'hsl(var(--signer-yellow))',
pink: 'hsl(var(--signer-pink))',
},
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
+1 -1
View File
@@ -1,4 +1,3 @@
import type { Session } from '@prisma/client';
import type { Context } from 'hono';
import { z } from 'zod';
@@ -6,6 +5,7 @@ import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { Session } from '@documenso/prisma/client';
type CreateTrpcContextOptions = {
c: Context;
@@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
@@ -25,6 +26,7 @@ import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { authenticatedProcedure, procedure, router } from '../trpc';
@@ -53,6 +55,7 @@ import {
ZMoveDocumentToTeamSchema,
ZResendDocumentMutationSchema,
ZSearchDocumentsMutationSchema,
ZSetPasswordForDocumentMutationSchema,
ZSetSigningOrderForDocumentMutationSchema,
ZSuccessResponseSchema,
ZUpdateDocumentRequestSchema,
@@ -441,6 +444,35 @@ export const documentRouter = router({
});
}),
/**
* @private
*/
setPasswordForDocument: authenticatedProcedure
.input(ZSetPasswordForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, password } = input;
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
throw new Error('Missing encryption key');
}
const securePassword = symmetricEncrypt({
data: password,
key,
});
await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
password: securePassword,
requestMetadata: ctx.metadata,
});
}),
/**
* @private
*
@@ -6,7 +6,7 @@ import { X } from 'lucide-react';
import { cn } from '../../lib/utils';
import { Dialog, DialogOverlay, DialogPortal, DialogTrigger } from '../../primitives/dialog';
import PDFViewer from '../../primitives/pdf-viewer';
import { LazyPDFViewerNoLoader } from '../../primitives/lazy-pdf-viewer';
export type DocumentDialogProps = {
trigger?: React.ReactNode;
@@ -43,7 +43,7 @@ export default function DocumentDialog({ trigger, documentData, ...props }: Docu
)}
onClick={() => props.onOpenChange?.(false)}
>
<PDFViewer
<LazyPDFViewerNoLoader
className="mx-auto w-full max-w-3xl xl:max-w-5xl"
documentData={documentData}
onClick={(e) => e.stopPropagation()}
@@ -1,148 +0,0 @@
import { useEffect, useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, Field, Recipient } from '@prisma/client';
import { SigningStatus } from '@prisma/client';
import { Clock, EyeOffIcon } from 'lucide-react';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge';
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { PopoverHover } from '@documenso/ui/primitives/popover';
import { FieldContent } from '../../primitives/document-flow/field-content';
export type DocumentReadOnlyFieldsProps = {
fields: DocumentField[];
documentMeta?: DocumentMeta;
showFieldStatus?: boolean;
/**
* Whether to show the recipient tooltip.
*
* @default false
*/
showRecipientTooltip?: boolean;
};
export const mapFieldsWithRecipients = (
fields: Field[],
recipients: Recipient[],
): DocumentField[] => {
return fields.map((field) => {
const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || {
name: 'Unknown',
email: 'Unknown',
signingStatus: SigningStatus.NOT_SIGNED,
};
return { ...field, recipient, signature: null };
});
};
export const DocumentReadOnlyFields = ({
documentMeta,
fields,
showFieldStatus = true,
showRecipientTooltip = false,
}: DocumentReadOnlyFieldsProps) => {
const { _ } = useLingui();
const [hiddenFieldIds, setHiddenFieldIds] = useState<Record<string, boolean>>({});
const handleHideField = (fieldId: string) => {
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
};
const isMounted = useIsMounted();
useEffect(() => {
console.log(isMounted);
}, [isMounted]);
if (!isMounted) {
return null;
}
return (
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{fields.map(
(field) =>
!hiddenFieldIds[field.secondaryId] && (
<FieldRootContainer field={field} key={field.id}>
{showRecipientTooltip && (
<div className="absolute -right-3 -top-3">
<PopoverHover
trigger={
<Avatar className="dark:border-foreground h-6 w-6 border-2 border-solid border-gray-200/50 transition-colors hover:border-gray-200">
<AvatarFallback className="bg-neutral-50 text-xs text-gray-400">
{extractInitials(field.recipient.name || field.recipient.email)}
</AvatarFallback>
</Avatar>
}
contentProps={{
className: 'relative flex w-fit flex-col p-4 text-sm',
}}
>
{showFieldStatus && (
<Badge
className="mx-auto mb-1 py-0.5"
variant={
field.recipient.signingStatus === SigningStatus.SIGNED
? 'default'
: 'secondary'
}
>
{field.recipient.signingStatus === SigningStatus.SIGNED ? (
<>
<SignatureIcon className="mr-1 h-3 w-3" />
<Trans>Signed</Trans>
</>
) : (
<>
<Clock className="mr-1 h-3 w-3" />
<Trans>Pending</Trans>
</>
)}
</Badge>
)}
<p className="text-center font-semibold">
<span>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field
</span>
</p>
<p className="text-muted-foreground mt-1 text-center text-xs">
{field.recipient.name
? `${field.recipient.name} (${field.recipient.email})`
: field.recipient.email}{' '}
</p>
<button
className="absolute right-0 top-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
onClick={() => handleHideField(field.secondaryId)}
title="Hide field"
>
<EyeOffIcon className="h-3 w-3" />
</button>
</PopoverHover>
</div>
)}
<FieldContent field={field} documentMeta={documentMeta} />
</FieldRootContainer>
),
)}
</ElementVisible>
);
};
+73 -26
View File
@@ -1,12 +1,14 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import type { Field } from '@prisma/client';
import { createPortal } from 'react-dom';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { useSignerColors } from '../../lib/signer-colors';
import { cn } from '../../lib/utils';
import { Card, CardContent } from '../../primitives/card';
export type FieldRootContainerProps = {
field: Field;
@@ -17,6 +19,53 @@ export type FieldContainerPortalProps = {
field: Field;
className?: string;
children: React.ReactNode;
cardClassName?: string;
};
const getCardClassNames = (
field: Field,
parsedField: TFieldMetaSchema | null,
isValidating: boolean,
checkBoxOrRadio: boolean,
cardClassName?: string,
) => {
const baseClasses =
'field--FieldRootContainer field-card-container relative z-20 h-full w-full transition-all';
const insertedClasses =
'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none';
const nonRequiredClasses =
'border-yellow-300 shadow-none ring-2 ring-yellow-100 ring-offset-2 ring-offset-yellow-100 dark:border-2';
const validatingClasses = 'border-orange-300 ring-1 ring-orange-300';
const requiredClasses =
'border-red-500 shadow-none ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 hover:text-red-500';
const requiredCheckboxRadioClasses = 'border-dashed border-red-500';
if (checkBoxOrRadio) {
return cn(
{
[insertedClasses]: field.inserted,
'ring-offset-yellow-200 border-dashed border-yellow-300 ring-2 ring-yellow-200 ring-offset-2 dark:shadow-none':
!field.inserted && !parsedField?.required,
'shadow-none': !field.inserted,
[validatingClasses]: !field.inserted && isValidating,
[requiredCheckboxRadioClasses]: !field.inserted && parsedField?.required,
},
cardClassName,
);
}
return cn(
baseClasses,
{
[insertedClasses]: field.inserted,
[nonRequiredClasses]: !field.inserted && !parsedField?.required,
'shadow-none': !field.inserted && checkBoxOrRadio,
[validatingClasses]: !field.inserted && isValidating,
[requiredClasses]: !field.inserted && parsedField?.required && !checkBoxOrRadio,
},
cardClassName,
);
};
export function FieldContainerPortal({
@@ -27,10 +76,13 @@ export function FieldContainerPortal({
const coords = useFieldPageCoords(field);
const isCheckboxOrRadioField = field.type === 'CHECKBOX' || field.type === 'RADIO';
const isFieldSigned = field.inserted;
const style = {
top: `${coords.y}px`,
left: `${coords.x}px`,
// height: `${coords.height}px`,
// width: `${coords.width}px`,
...(!isCheckboxOrRadioField && {
height: `${coords.height}px`,
width: `${coords.width}px`,
@@ -45,12 +97,10 @@ export function FieldContainerPortal({
);
}
export function FieldRootContainer({ field, children }: FieldContainerPortalProps) {
export function FieldRootContainer({ field, children, cardClassName }: FieldContainerPortalProps) {
const [isValidating, setIsValidating] = useState(false);
const ref = React.useRef<HTMLDivElement>(null);
const signerStyles = useSignerColors(field.recipientId);
useEffect(() => {
if (!ref.current) {
return;
@@ -71,36 +121,33 @@ export function FieldRootContainer({ field, children }: FieldContainerPortalProp
};
}, []);
// // todo: remove
// const parsedField = useMemo(
// () => (field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null),
// [field.fieldMeta],
// );
const parsedField = useMemo(
() => (field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null),
[field.fieldMeta],
);
const isCheckboxOrRadio = useMemo(
() => parsedField?.type === 'checkbox' || parsedField?.type === 'radio',
[parsedField],
);
// // todo: remove
// const isCheckboxOrRadio = useMemo(
// () => parsedField?.type === 'checkbox' || parsedField?.type === 'radio',
// [parsedField],
// );
const cardClassNames = useMemo(
() => getCardClassNames(field, parsedField, isValidating, isCheckboxOrRadio, cardClassName),
[field, parsedField, isValidating, isCheckboxOrRadio, cardClassName],
);
return (
<FieldContainerPortal field={field}>
<div
<Card
id={`field-${field.id}`}
ref={ref}
data-field-type={field.type}
data-inserted={field.inserted ? 'true' : 'false'}
className={cn(
'field--FieldRootContainer field-card-container relative z-20 h-full w-full rounded-sm ring-2 transition-all',
'ring-signer-green bg-white/90',
'px-2', // This is specific to try sync with field insertion. See insert-field-in-pdf before changing this.
{
'flex items-center justify-center': !field.inserted,
},
)}
className={cardClassNames}
>
{children}
</div>
<CardContent className="text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2">
{children}
</CardContent>
</Card>
</FieldContainerPortal>
);
}
+36 -19
View File
@@ -2,14 +2,16 @@
// !: 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
// !: values of the declared variable to do all the background, border and shadow styles.
export const SIGNER_COLOR_STYLES = {
green: {
default: {
base: 'ring-signer-green hover:bg-signer-green/30',
fieldItem: 'group/field-item rounded-sm',
fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-green))]',
background: 'bg-[hsl(var(--signer-green))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-green)/10%),0_0_0_2px_hsl(var(--signer-green)/60%),0_0_0_0.5px_hsl(var(--signer-green))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-green))]/10 hover:to-[hsl(var(--signer-green))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-green))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-green)/15%)] active:bg-[hsl(var(--signer-green)/15%)]',
},
@@ -17,9 +19,12 @@ export const SIGNER_COLOR_STYLES = {
blue: {
default: {
base: 'ring-signer-blue hover:bg-signer-blue/30',
fieldItem: 'group/field-item rounded-sm',
fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-blue))]',
background: 'bg-[hsl(var(--signer-blue))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-blue)/10%),0_0_0_2px_hsl(var(--signer-blue)/60%),0_0_0_0.5px_hsl(var(--signer-blue))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-blue))]/10 hover:to-[hsl(var(--signer-blue))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-blue))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-blue)/15%)] active:bg-[hsl(var(--signer-blue)/15%)]',
},
@@ -27,9 +32,12 @@ export const SIGNER_COLOR_STYLES = {
purple: {
default: {
base: 'ring-signer-purple hover:bg-signer-purple/30',
fieldItem: 'group/field-item rounded-sm',
fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-purple))]',
background: 'bg-[hsl(var(--signer-purple))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-purple)/10%),0_0_0_2px_hsl(var(--signer-purple)/60%),0_0_0_0.5px_hsl(var(--signer-purple))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-purple))]/10 hover:to-[hsl(var(--signer-purple))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-purple))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-purple)/15%)] active:bg-[hsl(var(--signer-purple)/15%)]',
},
@@ -37,9 +45,12 @@ export const SIGNER_COLOR_STYLES = {
orange: {
default: {
base: 'ring-signer-orange hover:bg-signer-orange/30',
fieldItem: 'group/field-item rounded-sm',
fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-orange))]',
background: 'bg-[hsl(var(--signer-orange))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-orange)/10%),0_0_0_2px_hsl(var(--signer-orange)/60%),0_0_0_0.5px_hsl(var(--signer-orange))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-orange))]/10 hover:to-[hsl(var(--signer-orange))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-orange))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-orange)/15%)] active:bg-[hsl(var(--signer-orange)/15%)]',
},
@@ -47,9 +58,12 @@ export const SIGNER_COLOR_STYLES = {
yellow: {
default: {
base: 'ring-signer-yellow hover:bg-signer-yellow/30',
fieldItem: 'group/field-item rounded-sm',
fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-yellow))]',
background: 'bg-[hsl(var(--signer-yellow))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-yellow)/10%),0_0_0_2px_hsl(var(--signer-yellow)/60%),0_0_0_0.5px_hsl(var(--signer-yellow))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-yellow))]/10 hover:to-[hsl(var(--signer-yellow))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-yellow))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-yellow)/15%)] active:bg-[hsl(var(--signer-yellow)/15%)]',
},
@@ -57,9 +71,12 @@ export const SIGNER_COLOR_STYLES = {
pink: {
default: {
base: 'ring-signer-pink hover:bg-signer-pink/30',
fieldItem: 'group/field-item rounded-sm',
fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-pink))]',
background: 'bg-[hsl(var(--signer-pink))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-pink)/10%),0_0_0_2px_hsl(var(--signer-pink)/60%),0_0_0_0.5px_hsl(var(--signer-pink))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-pink))]/10 hover:to-[hsl(var(--signer-pink))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-pink))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-pink)/15%)] active:bg-[hsl(var(--signer-pink)/15%)]',
},
@@ -88,6 +88,7 @@ export type FieldFormType = {
export type AddFieldsFormProps = {
documentFlow: DocumentFlowStep;
hideRecipients?: boolean;
recipients: Recipient[];
fields: Field[];
onSubmit: (_data: TAddFieldsFormSchema) => void;
@@ -99,6 +100,7 @@ export type AddFieldsFormProps = {
export const AddFieldsFormPartial = ({
documentFlow,
hideRecipients = false,
recipients,
fields,
onSubmit,
@@ -655,6 +657,7 @@ export const AddFieldsFormPartial = ({
setCurrentField(field);
handleAdvancedSettings();
}}
hideRecipients={hideRecipients}
hasErrors={!!hasFieldError}
active={activeFieldId === field.formId}
onFieldActivate={() => setActiveFieldId(field.formId)}
@@ -663,123 +666,125 @@ export const AddFieldsFormPartial = ({
);
})}
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
role="combobox"
className={cn(
'bg-background text-muted-foreground hover:text-foreground mb-12 mt-2 justify-between font-normal',
selectedSignerStyles.default.base,
)}
>
{selectedSigner?.email && (
<span className="flex-1 truncate text-left">
{selectedSigner?.name} ({selectedSigner?.email})
</span>
)}
{!hideRecipients && (
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
role="combobox"
className={cn(
'bg-background text-muted-foreground hover:text-foreground mb-12 mt-2 justify-between font-normal',
selectedSignerStyles.default.base,
)}
>
{selectedSigner?.email && (
<span className="flex-1 truncate text-left">
{selectedSigner?.name} ({selectedSigner?.email})
</span>
)}
{!selectedSigner?.email && (
<span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
)}
{!selectedSigner?.email && (
<span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
)}
<ChevronsUpDown className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<ChevronsUpDown className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command value={selectedSigner?.email}>
<CommandInput />
<PopoverContent className="p-0" align="start">
<Command value={selectedSigner?.email}>
<CommandInput />
<CommandEmpty>
<span className="text-muted-foreground inline-block px-4">
<Trans>No recipient matching this description was found.</Trans>
</span>
</CommandEmpty>
<CommandEmpty>
<span className="text-muted-foreground inline-block px-4">
<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">
{_(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"
>
<Trans>No recipients with this role</Trans>
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
<CommandGroup key={roleIndex}>
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
</div>
)}
{roleRecipients.map((recipient) => (
<CommandItem
key={recipient.id}
className={cn(
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
getSignerColorStyles(
Math.max(
recipients.findIndex((r) => r.id === recipient.id),
0,
),
).default.comboxBoxItem,
{
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
},
)}
onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
>
<span
className={cn('text-foreground/70 truncate', {
'text-foreground/80': recipient === selectedSigner,
})}
{roleRecipients.length === 0 && (
<div
key={`${role}-empty`}
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
>
{recipient.name && (
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && (
<span title={recipient.email}>{recipient.email}</span>
)}
</span>
<div className="ml-auto flex items-center justify-center">
{recipient.sendStatus !== SendStatus.SENT ? (
<Check
aria-hidden={recipient !== selectedSigner}
className={cn('h-4 w-4 flex-shrink-0', {
'opacity-0': recipient !== selectedSigner,
'opacity-100': recipient === selectedSigner,
})}
/>
) : (
<Tooltip>
<TooltipTrigger>
<Info className="ml-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<Trans>
This document has already been sent to this recipient. You can
no longer edit this recipient.
</Trans>
</TooltipContent>
</Tooltip>
)}
<Trans>No recipients with this role</Trans>
</div>
</CommandItem>
))}
</CommandGroup>
))}
</Command>
</PopoverContent>
</Popover>
)}
{roleRecipients.map((recipient) => (
<CommandItem
key={recipient.id}
className={cn(
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
getSignerColorStyles(
Math.max(
recipients.findIndex((r) => r.id === recipient.id),
0,
),
).default.comboxBoxItem,
{
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
},
)}
onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
>
<span
className={cn('text-foreground/70 truncate', {
'text-foreground/80': recipient === selectedSigner,
})}
>
{recipient.name && (
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && (
<span title={recipient.email}>{recipient.email}</span>
)}
</span>
<div className="ml-auto flex items-center justify-center">
{recipient.sendStatus !== SendStatus.SENT ? (
<Check
aria-hidden={recipient !== selectedSigner}
className={cn('h-4 w-4 flex-shrink-0', {
'opacity-0': recipient !== selectedSigner,
'opacity-100': recipient === selectedSigner,
})}
/>
) : (
<Tooltip>
<TooltipTrigger>
<Info className="ml-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<Trans>
This document has already been sent to this recipient. You
can no longer edit this recipient.
</Trans>
</TooltipContent>
</Tooltip>
)}
</div>
</CommandItem>
))}
</CommandGroup>
))}
</Command>
</PopoverContent>
</Popover>
)}
<Form {...form}>
<FormField
@@ -21,10 +21,6 @@ import {
DocumentGlobalAuthActionSelect,
DocumentGlobalAuthActionTooltip,
} from '@documenso/ui/components/document/document-global-auth-action-select';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '@documenso/ui/components/document/document-read-only-fields';
import {
DocumentVisibilitySelect,
DocumentVisibilityTooltip,
@@ -58,6 +54,7 @@ import {
DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep,
} from './document-flow-root';
import { ShowFieldItem } from './show-field-item';
import type { DocumentFlowStep } from './types';
export type AddSettingsFormProps = {
@@ -148,9 +145,10 @@ export const AddSettingsFormPartial = ({
/>
<DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && (
<DocumentReadOnlyFields fields={mapFieldsWithRecipients(fields, recipients)} />
)}
{isDocumentPdfLoaded &&
fields.map((field, index) => (
<ShowFieldItem key={index} field={field} recipients={recipients} />
))}
<Form {...form}>
<fieldset
@@ -23,10 +23,6 @@ import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/re
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';
import { cn } from '@documenso/ui/lib/utils';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '../../components/document/document-read-only-fields';
import { Button } from '../button';
import { Checkbox } from '../checkbox';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
@@ -43,6 +39,7 @@ import {
DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep,
} from './document-flow-root';
import { ShowFieldItem } from './show-field-item';
import { SigningOrderConfirmation } from './signing-order-confirmation';
import type { DocumentFlowStep } from './types';
@@ -364,9 +361,10 @@ export const AddSignersFormPartial = ({
description={documentFlow.description}
/>
<DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && (
<DocumentReadOnlyFields fields={mapFieldsWithRecipients(fields, recipients)} />
)}
{isDocumentPdfLoaded &&
fields.map((field, index) => (
<ShowFieldItem key={index} field={field} recipients={recipients} />
))}
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
<Form {...form}>
@@ -16,10 +16,6 @@ import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { CopyTextButton } from '../../components/common/copy-text-button';
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '../../components/document/document-read-only-fields';
import { AvatarWithText } from '../avatar';
import { FormErrorMessage } from '../form/form-error-message';
import { Input } from '../input';
@@ -35,6 +31,7 @@ import {
DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep,
} from './document-flow-root';
import { ShowFieldItem } from './show-field-item';
import type { DocumentFlowStep } from './types';
export type AddSubjectFormProps = {
@@ -104,9 +101,10 @@ export const AddSubjectFormPartial = ({
/>
<DocumentFlowFormContainerContent>
<div className="flex flex-col">
{isDocumentPdfLoaded && (
<DocumentReadOnlyFields fields={mapFieldsWithRecipients(fields, recipients)} />
)}
{isDocumentPdfLoaded &&
fields.map((field, index) => (
<ShowFieldItem key={index} field={field} recipients={recipients} />
))}
<Tabs
onValueChange={(value) =>
@@ -0,0 +1,50 @@
import type { TCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import { Label } from '@documenso/ui/primitives/label';
import { FieldIcon } from '../field-icon';
import type { TDocumentFlowFormSchema } from '../types';
type Field = TDocumentFlowFormSchema['fields'][0];
export type CheckboxFieldProps = {
field: Field;
};
export const CheckboxField = ({ field }: CheckboxFieldProps) => {
let parsedFieldMeta: TCheckboxFieldMeta | undefined = undefined;
if (field.fieldMeta) {
parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
}
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
return <FieldIcon fieldMeta={field.fieldMeta} type={field.type} />;
}
return (
<div className="flex flex-col gap-y-1">
{!parsedFieldMeta?.values ? (
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
) : (
parsedFieldMeta.values.map((item: { value: string; checked: boolean }, index: number) => {
const checkboxId = `checkbox-field-${field.formId}-${index}`;
return (
<div key={index} className="flex items-center gap-x-1.5">
<Checkbox
className="dark:border-field-border h-3 w-3 bg-white"
id={checkboxId}
checked={item.checked}
/>
<Label htmlFor={checkboxId} className="text-xs font-normal text-black">
{item.value}
</Label>
</div>
);
})
)}
</div>
);
};
@@ -0,0 +1,49 @@
import { ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
import type { TRadioFieldMeta } from '@documenso/lib/types/field-meta';
import { Label } from '@documenso/ui/primitives/label';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { FieldIcon } from '../field-icon';
import type { TDocumentFlowFormSchema } from '../types';
type Field = TDocumentFlowFormSchema['fields'][0];
export type RadioFieldProps = {
field: Field;
};
export const RadioField = ({ field }: RadioFieldProps) => {
let parsedFieldMeta: TRadioFieldMeta | undefined = undefined;
if (field.fieldMeta) {
parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
}
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
return <FieldIcon fieldMeta={field.fieldMeta} type={field.type} />;
}
return (
<div className="flex flex-col gap-y-2">
{!parsedFieldMeta?.values ? (
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
) : (
<RadioGroup className="gap-y-1">
{parsedFieldMeta.values?.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5">
<RadioGroupItem
className="dark:border-field-border pointer-events-none h-3 w-3"
value={item.value}
id={`option-${index}`}
checked={item.checked}
/>
<Label htmlFor={`option-${index}`} className="text-xs font-normal text-black">
{item.value}
</Label>
</div>
))}
</RadioGroup>
)}
</div>
);
};
@@ -1,166 +0,0 @@
import { useLingui } from '@lingui/react';
import type { DocumentMeta, Field, Signature } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { ChevronDown } from 'lucide-react';
import {
DEFAULT_DOCUMENT_DATE_FORMAT,
convertToLocalSystemFormat,
} from '@documenso/lib/constants/date-formats';
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
import { cn } from '../../lib/utils';
import { Checkbox } from '../checkbox';
import { Label } from '../label';
import { RadioGroup, RadioGroupItem } from '../radio-group';
import { FRIENDLY_FIELD_TYPE } from './types';
type FieldIconProps = {
field: Field & { signature?: Signature };
documentMeta?: DocumentMeta;
};
/**
* Renders the content inside field containers prior to sealing.
*/
export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
const { _ } = useLingui();
const { type, fieldMeta } = field;
// Only render checkbox if values exist, otherwise render the empty checkbox field content.
if (
field.type === FieldType.CHECKBOX &&
field.fieldMeta?.type === 'checkbox' &&
field.fieldMeta.values &&
field.fieldMeta.values.length > 0
) {
let checkedValues: string[] = [];
try {
checkedValues = fromCheckboxValue(field.customText);
} catch (err) {
// Do nothing.
console.error(err);
}
return (
<div className="flex flex-col gap-y-1 py-0.5">
{field.fieldMeta.values.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5">
<Checkbox
className="h-3 w-3"
id={`checkbox-${index}`}
checked={checkedValues.includes(item.value)}
/>
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal">
{item.value}
</Label>
</div>
))}
</div>
);
}
// Only render radio if values exist, otherwise render the empty radio field content.
if (
field.type === FieldType.RADIO &&
field.fieldMeta?.type === 'radio' &&
field.fieldMeta.values &&
field.fieldMeta.values.length > 0
) {
return (
<div className="flex flex-col gap-y-2 py-0.5">
<RadioGroup className="gap-y-1">
{field.fieldMeta.values.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5">
<RadioGroupItem
className="pointer-events-none h-3 w-3"
value={item.value}
id={`option-${index}`}
checked={item.value === field.customText}
/>
<Label htmlFor={`option-${index}`} className="text-xs font-normal">
{item.value}
</Label>
</div>
))}
</RadioGroup>
</div>
);
}
if (
field.type === FieldType.DROPDOWN &&
field.fieldMeta?.type === 'dropdown' &&
!field.inserted
) {
return (
<div className="text-field-card-foreground flex flex-row items-center py-0.5 text-[clamp(0.07rem,25cqw,0.825rem)] text-sm">
<p>Select</p>
<ChevronDown className="h-4 w-4" />
</div>
);
}
if (
field.type === FieldType.SIGNATURE &&
field.signature?.signatureImageAsBase64 &&
field.inserted
) {
return (
<img
src={field.signature.signatureImageAsBase64}
alt="Signature"
className="h-full w-full object-contain dark:invert"
/>
);
}
let textToDisplay = fieldMeta?.label || _(FRIENDLY_FIELD_TYPE[type]) || '';
const isSignatureField =
field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE;
// Trim default labels.
if (textToDisplay.length > 20) {
textToDisplay = textToDisplay.substring(0, 20) + '...';
}
if (field.inserted) {
if (field.customText) {
textToDisplay = field.customText;
}
if (field.type === FieldType.DATE) {
textToDisplay = convertToLocalSystemFormat(
field.customText,
documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
);
}
if (isSignatureField && field.signature?.typedSignature) {
textToDisplay = field.signature.typedSignature;
}
}
const textAlign = fieldMeta && 'textAlign' in fieldMeta ? fieldMeta.textAlign : 'left';
return (
<div
className={cn(
'text-field-card-foreground flex h-full w-full items-center justify-center gap-x-1.5 whitespace-nowrap text-center text-[clamp(0.07rem,25cqw,0.825rem)]',
{
// Using justify instead of align because we also vertically center the text.
'justify-start': field.inserted && !isSignatureField && textAlign === 'left',
'justify-end': field.inserted && !isSignatureField && textAlign === 'right',
'font-signature text-[clamp(0.07rem,25cqw,1.125rem)]': isSignatureField,
},
)}
>
{textToDisplay}
</div>
);
};
@@ -0,0 +1,72 @@
import { Trans } from '@lingui/react/macro';
import { FieldType } from '@prisma/client';
import {
CalendarDays,
CheckSquare,
ChevronDown,
Contact,
Disc,
Hash,
Mail,
Type,
User,
} from 'lucide-react';
import type { TFieldMetaSchema as FieldMetaType } from '@documenso/lib/types/field-meta';
import { cn } from '../../lib/utils';
type FieldIconProps = {
fieldMeta: FieldMetaType;
type: FieldType;
};
const fieldIcons = {
[FieldType.INITIALS]: { icon: Contact, label: 'Initials' },
[FieldType.EMAIL]: { icon: Mail, label: 'Email' },
[FieldType.NAME]: { icon: User, label: 'Name' },
[FieldType.DATE]: { icon: CalendarDays, label: 'Date' },
[FieldType.TEXT]: { icon: Type, label: 'Text' },
[FieldType.NUMBER]: { icon: Hash, label: 'Number' },
[FieldType.RADIO]: { icon: Disc, label: 'Radio' },
[FieldType.CHECKBOX]: { icon: CheckSquare, label: 'Checkbox' },
[FieldType.DROPDOWN]: { icon: ChevronDown, label: 'Select' },
};
export const FieldIcon = ({ fieldMeta, type }: FieldIconProps) => {
if (type === 'SIGNATURE' || type === 'FREE_SIGNATURE') {
return (
<div
className={cn(
'text-field-card-foreground font-signature flex items-center justify-center gap-x-1 text-[clamp(0.575rem,25cqw,1.2rem)]',
)}
>
<Trans>Signature</Trans>
</div>
);
} else {
const Icon = fieldIcons[type]?.icon;
let label;
if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) {
if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) {
label =
fieldMeta.text.length > 20 ? fieldMeta.text.substring(0, 20) + '...' : fieldMeta.text;
} else if (fieldMeta.label) {
label =
fieldMeta.label.length > 20 ? fieldMeta.label.substring(0, 20) + '...' : fieldMeta.label;
} else {
label = fieldIcons[type]?.label;
}
} else {
label = fieldIcons[type]?.label;
}
return (
<div className="text-field-card-foreground flex items-center justify-center gap-x-1.5 text-[clamp(0.425rem,25cqw,0.825rem)]">
<Icon className="h-[clamp(0.625rem,20cqw,0.925rem)] w-[clamp(0.625rem,20cqw,0.925rem)]" />{' '}
{label}
</div>
);
}
};
@@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FieldType } from '@prisma/client';
import { CopyPlus, Settings2, Trash } from 'lucide-react';
import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd';
import { match } from 'ts-pattern';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
@@ -11,7 +11,9 @@ import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-
import { useSignerColors } from '../../lib/signer-colors';
import { cn } from '../../lib/utils';
import { FieldContent } from './field-content';
import { CheckboxField } from './advanced-fields/checkbox';
import { RadioField } from './advanced-fields/radio';
import { FieldIcon } from './field-icon';
import type { TDocumentFlowFormSchema } from './types';
type Field = TDocumentFlowFormSchema['fields'][0];
@@ -32,15 +34,13 @@ export type FieldItemProps = {
onFocus?: () => void;
onBlur?: () => void;
recipientIndex?: number;
hideRecipients?: boolean;
hasErrors?: boolean;
active?: boolean;
onFieldActivate?: () => void;
onFieldDeactivate?: () => void;
};
/**
* The item when editing fields??
*/
export const FieldItem = ({
field,
passive,
@@ -57,6 +57,7 @@ export const FieldItem = ({
onBlur,
onAdvancedSettings,
recipientIndex = 0,
hideRecipients = false,
hasErrors,
active,
onFieldActivate,
@@ -173,35 +174,11 @@ export const FieldItem = ({
() => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta),
[field.fieldMeta],
);
const radioHasValues = useMemo(
() => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta),
[field.fieldMeta],
);
const hasCheckedValues = (fieldMeta: TFieldMetaSchema, type: FieldType) => {
if (!fieldMeta || (type !== FieldType.RADIO && type !== FieldType.CHECKBOX)) {
return false;
}
if (type === FieldType.RADIO) {
const parsed = ZRadioFieldMeta.parse(fieldMeta);
return parsed.values?.some((value) => value.checked) ?? false;
}
if (type === FieldType.CHECKBOX) {
const parsed = ZCheckboxFieldMeta.parse(fieldMeta);
return parsed.values?.some((value) => value.checked) ?? false;
}
return false;
};
const fieldHasCheckedValues = useMemo(
() => hasCheckedValues(field.fieldMeta, field.type),
[field.fieldMeta, field.type],
);
const fixedSize = checkBoxHasValues || radioHasValues;
return createPortal(
@@ -241,28 +218,13 @@ export const FieldItem = ({
onMove?.(d.node);
}}
>
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
field.fieldMeta?.label && (
<div
className={cn(
'absolute -top-16 left-0 right-0 rounded-md p-2 text-center text-xs text-gray-700',
{
'bg-foreground/5 border-primary border': !fieldHasCheckedValues,
'bg-documenso-200 border-primary border': fieldHasCheckedValues,
},
)}
>
{field.fieldMeta.label}
</div>
)}
<div
className={cn(
'relative flex h-full w-full items-center justify-center bg-white/90 ring-2 transition-colors',
'relative flex h-full w-full items-center justify-center bg-white',
!hasErrors && signerStyles.default.base,
!hasErrors && signerStyles.default.fieldItem,
{
'rounded-sm border border-red-400 bg-red-400/20 shadow-[0_0_0_5px_theme(colors.red.500/10%),0_0_0_2px_theme(colors.red.500/40%),0_0_0_0.5px_theme(colors.red.500)]':
'rounded-lg border border-red-400 bg-red-400/20 shadow-[0_0_0_5px_theme(colors.red.500/10%),0_0_0_2px_theme(colors.red.500/40%),0_0_0_0.5px_theme(colors.red.500)]':
hasErrors,
},
!fixedSize && '[container-type:size]',
@@ -277,27 +239,33 @@ export const FieldItem = ({
ref={$el}
data-field-id={field.nativeId}
>
<FieldContent field={field} />
{match(field.type)
.with('CHECKBOX', () => <CheckboxField field={field} />)
.with('RADIO', () => <RadioField field={field} />)
.otherwise(() => (
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
))}
{/* On hover, display recipient initials on side of field. */}
<div className="absolute -right-5 top-0 z-20 hidden h-full w-5 items-center justify-center group-hover:flex">
<div
className={cn(
'flex h-5 w-5 flex-col items-center justify-center rounded-r-md text-[0.5rem] font-bold text-white opacity-0 transition duration-200 group-hover/field-item:opacity-100',
signerStyles.default.fieldItemInitials,
{
'!opacity-50': disabled || passive,
},
)}
>
{(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') +
(field.signerEmail?.charAt(1)?.toUpperCase() ?? '')}
{!hideRecipients && (
<div className="absolute -right-5 top-0 z-20 hidden h-full w-5 items-center justify-center group-hover:flex">
<div
className={cn(
'flex h-5 w-5 flex-col items-center justify-center rounded-r-md text-[0.5rem] font-bold text-white',
signerStyles.default.fieldItemInitials,
{
'!opacity-50': disabled || passive,
},
)}
>
{(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') +
(field.signerEmail?.charAt(1)?.toUpperCase() ?? '')}
</div>
</div>
</div>
)}
</div>
{!disabled && settingsActive && (
<div className="absolute z-[60] mt-1 flex w-full items-center justify-center">
<div className="z-[60] mt-1 flex justify-center">
<div className="dark:bg-background group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
{advancedField && (
<button
@@ -125,18 +125,6 @@ export const CheckboxFieldAdvancedSettings = ({
return (
<div className="flex flex-col gap-4">
<div className="mb-2">
<Label>
<Trans>Label</Trans>
</Label>
<Input
id="label"
className="bg-background mt-2"
placeholder={_(msg`Field label`)}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
/>
</div>
<div className="flex flex-row items-center gap-x-4">
<div className="flex w-2/3 flex-col">
<Label>
@@ -217,6 +205,7 @@ export const CheckboxFieldAdvancedSettings = ({
<div key={index} className="mt-2 flex items-center gap-4">
<Checkbox
className="data-[state=checked]:bg-primary border-foreground/30 h-5 w-5"
id={`checkbox-${value.id}-${index}`}
checked={value.checked}
onCheckedChange={(checked) => handleCheckboxValue(index, 'checked', checked)}
/>

Some files were not shown because too many files have changed in this diff Show More