Compare commits
11 Commits
fix/checkb
...
fix/field-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
172a5be737 | ||
|
|
25bb6ffe77 | ||
|
|
e79d762710 | ||
|
|
d970976299 | ||
|
|
3dce814ab2 | ||
|
|
ad520bb032 | ||
|
|
596d30e2e5 | ||
|
|
6474b4a524 | ||
|
|
5b4db51051 | ||
|
|
cf58c80e31 | ||
|
|
11dbb8873e |
@@ -31,6 +31,11 @@ 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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { PDFViewer } from '@documenso/ui/primitives/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">
|
||||
<LazyPDFViewer key={document?.id} documentData={documentData} />
|
||||
<PDFViewer key={document?.id} documentData={documentData} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ export const TemplateBulkSendDialog = ({
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
{trigger ?? (
|
||||
<Button>
|
||||
<Button variant="outline">
|
||||
<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,9 +35,10 @@ export const EmbedAuthenticationRequired = ({
|
||||
</Alert>
|
||||
|
||||
<SignInForm
|
||||
isGoogleSSOEnabled={isGoogleSSOEnabled}
|
||||
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
||||
oidcProviderLabel={oidcProviderLabel}
|
||||
// Embed currently not supported.
|
||||
// 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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { PDFViewer } from '@documenso/ui/primitives/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">
|
||||
<LazyPDFViewer
|
||||
<PDFViewer
|
||||
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 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 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"
|
||||
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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { PDFViewer } from '@documenso/ui/primitives/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">
|
||||
<LazyPDFViewer
|
||||
<PDFViewer
|
||||
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 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 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"
|
||||
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,6 +9,10 @@ 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,
|
||||
@@ -16,7 +20,6 @@ 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,
|
||||
@@ -97,14 +100,14 @@ export const DirectTemplateConfigureForm = ({
|
||||
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
directTemplateRecipient.fields.map((field, index) => (
|
||||
<ShowFieldItem
|
||||
key={index}
|
||||
field={field}
|
||||
recipients={recipientsWithBlankDirectRecipientEmail}
|
||||
/>
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields
|
||||
fields={mapFieldsWithRecipients(
|
||||
directTemplateRecipient.fields,
|
||||
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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { PDFViewer } from '@documenso/ui/primitives/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">
|
||||
<LazyPDFViewer
|
||||
<PDFViewer
|
||||
key={template.id}
|
||||
documentData={template.templateDocumentData}
|
||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||
|
||||
@@ -45,26 +45,24 @@ export const DocumentSigningCheckboxField = ({
|
||||
|
||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
const parsedFieldMeta = ZCheckboxFieldMeta.parse(
|
||||
field.fieldMeta ?? {
|
||||
type: 'checkbox',
|
||||
values: [{ id: 1, checked: false, value: '' }],
|
||||
},
|
||||
);
|
||||
|
||||
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(
|
||||
field.inserted && parsedCheckedValues.length > 0
|
||||
? parsedCheckedValues
|
||||
: values
|
||||
?.map((item) =>
|
||||
item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
|
||||
)
|
||||
.filter(Boolean) || [],
|
||||
values
|
||||
?.map((item) =>
|
||||
item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
|
||||
)
|
||||
.filter(Boolean) || [],
|
||||
);
|
||||
|
||||
const isReadOnly = parsedFieldMeta.readOnly;
|
||||
@@ -179,13 +177,19 @@ export const DocumentSigningCheckboxField = ({
|
||||
let updatedValues: string[] = [];
|
||||
|
||||
try {
|
||||
const itemValue = item.value.length > 0 ? item.value : `empty-value-${item.id}`;
|
||||
const isChecked = checkedValues.includes(itemValue);
|
||||
const isChecked = checkedValues.includes(
|
||||
item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
||||
);
|
||||
|
||||
if (!isChecked) {
|
||||
updatedValues = [...checkedValues, itemValue];
|
||||
updatedValues = [
|
||||
...checkedValues,
|
||||
item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
||||
];
|
||||
} else {
|
||||
updatedValues = checkedValues.filter((v) => v !== itemValue);
|
||||
updatedValues = checkedValues.filter(
|
||||
(v) => v !== item.value && v !== `empty-value-${item.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
setCheckedValues(updatedValues);
|
||||
@@ -216,12 +220,6 @@ export const DocumentSigningCheckboxField = ({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (field.inserted && parsedCheckedValues.length > 0) {
|
||||
setCheckedValues(parsedCheckedValues);
|
||||
}
|
||||
}, [field.inserted, parsedCheckedValues]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoSignField) {
|
||||
void executeActionAuthProcedure({
|
||||
@@ -231,6 +229,11 @@ export const DocumentSigningCheckboxField = ({
|
||||
}
|
||||
}, [checkedValues, isLengthConditionMet, field.inserted]);
|
||||
|
||||
const parsedCheckedValues = useMemo(
|
||||
() => fromCheckboxValue(field.customText),
|
||||
[field.customText],
|
||||
);
|
||||
|
||||
return (
|
||||
<DocumentSigningFieldContainer
|
||||
field={field}
|
||||
@@ -251,20 +254,19 @@ export const DocumentSigningCheckboxField = ({
|
||||
{validationSign?.label} {checkboxValidationLength}
|
||||
</FieldToolTip>
|
||||
)}
|
||||
<div className="z-50 flex flex-col gap-y-2">
|
||||
<div className="z-50 my-0.5 flex flex-col gap-y-1">
|
||||
{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-4 w-4"
|
||||
id={checkboxId}
|
||||
className="h-3 w-3"
|
||||
id={`checkbox-${index}`}
|
||||
checked={checkedValues.includes(itemValue)}
|
||||
onCheckedChange={() => handleCheckboxChange(item.value, item.id)}
|
||||
/>
|
||||
<Label htmlFor={checkboxId}>
|
||||
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal">
|
||||
{item.value.includes('empty-value-') ? '' : item.value}
|
||||
</Label>
|
||||
</div>
|
||||
@@ -275,21 +277,20 @@ export const DocumentSigningCheckboxField = ({
|
||||
)}
|
||||
|
||||
{field.inserted && (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="my-0.5 flex flex-col gap-y-1">
|
||||
{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-4 w-4"
|
||||
id={checkboxId}
|
||||
checked={checkedValues.includes(itemValue)}
|
||||
className="h-3 w-3"
|
||||
id={`checkbox-${index}`}
|
||||
checked={parsedCheckedValues.includes(itemValue)}
|
||||
disabled={isLoading}
|
||||
onCheckedChange={() => void handleCheckboxOptionClick(item)}
|
||||
/>
|
||||
<Label htmlFor={checkboxId}>
|
||||
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal">
|
||||
{item.value.includes('empty-value-') ? '' : item.value}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -151,12 +151,10 @@ 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-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||
'text-muted-foreground dark:text-background/80 w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||
{
|
||||
'text-left': parsedFieldMeta?.textAlign === 'left',
|
||||
'text-center':
|
||||
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
|
||||
'text-right': parsedFieldMeta?.textAlign === 'right',
|
||||
'!text-center': parsedFieldMeta?.textAlign === 'center',
|
||||
'!text-right': parsedFieldMeta?.textAlign === 'right',
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -136,12 +136,10 @@ 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-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||
'text-muted-foreground dark:text-background/80 w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||
{
|
||||
'text-left': parsedFieldMeta?.textAlign === 'left',
|
||||
'text-center':
|
||||
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
|
||||
'text-right': parsedFieldMeta?.textAlign === 'right',
|
||||
'!text-center': parsedFieldMeta?.textAlign === 'center',
|
||||
'!text-right': parsedFieldMeta?.textAlign === 'right',
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -181,6 +181,23 @@ 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,12 +182,10 @@ 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-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||
'text-muted-foreground dark:text-background/80 w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||
{
|
||||
'text-left': parsedFieldMeta?.textAlign === 'left',
|
||||
'text-center':
|
||||
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
|
||||
'text-right': parsedFieldMeta?.textAlign === 'right',
|
||||
'!text-center': parsedFieldMeta?.textAlign === 'center',
|
||||
'!text-right': parsedFieldMeta?.textAlign === 'right',
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -272,12 +272,10 @@ 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-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||
'text-muted-foreground dark:text-background/80 w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||
{
|
||||
'text-left': parsedFieldMeta?.textAlign === 'left',
|
||||
'text-center':
|
||||
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
|
||||
'text-right': parsedFieldMeta?.textAlign === 'right',
|
||||
'!text-center': parsedFieldMeta?.textAlign === 'center',
|
||||
'!text-right': parsedFieldMeta?.textAlign === 'right',
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -19,9 +19,10 @@ 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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
|
||||
|
||||
import { DocumentSigningAutoSign } from '~/components/general/document-signing/document-signing-auto-sign';
|
||||
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
|
||||
@@ -36,7 +37,6 @@ 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,12 +140,7 @@ export const DocumentSigningPageView = ({
|
||||
gradient
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<LazyPDFViewer
|
||||
key={documentData.id}
|
||||
documentData={documentData}
|
||||
document={document}
|
||||
password={documentMeta?.password}
|
||||
/>
|
||||
<PDFViewer key={documentData.id} documentData={documentData} document={document} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -157,17 +157,20 @@ export const DocumentSigningRadioField = ({
|
||||
)}
|
||||
|
||||
{!field.inserted && (
|
||||
<RadioGroup onValueChange={(value) => handleSelectItem(value)} className="z-10">
|
||||
<RadioGroup
|
||||
onValueChange={(value) => handleSelectItem(value)}
|
||||
className="z-10 my-0.5 gap-y-1"
|
||||
>
|
||||
{values?.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<RadioGroupItem
|
||||
className="h-4 w-4 shrink-0"
|
||||
className="h-3 w-3 shrink-0"
|
||||
value={item.value}
|
||||
id={`option-${index}`}
|
||||
checked={item.checked}
|
||||
/>
|
||||
|
||||
<Label htmlFor={`option-${index}`}>
|
||||
<Label htmlFor={`option-${index}`} className="text-xs font-normal">
|
||||
{item.value.includes('empty-value-') ? '' : item.value}
|
||||
</Label>
|
||||
</div>
|
||||
@@ -176,7 +179,7 @@ export const DocumentSigningRadioField = ({
|
||||
)}
|
||||
|
||||
{field.inserted && (
|
||||
<RadioGroup className="gap-y-1">
|
||||
<RadioGroup className="my-0.5 gap-y-1">
|
||||
{values?.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<RadioGroupItem
|
||||
@@ -185,7 +188,7 @@ export const DocumentSigningRadioField = ({
|
||||
id={`option-${index}`}
|
||||
checked={item.value === field.customText}
|
||||
/>
|
||||
<Label htmlFor={`option-${index}`} className="text-xs">
|
||||
<Label htmlFor={`option-${index}`} className="text-xs font-normal">
|
||||
{item.value.includes('empty-value-') ? '' : item.value}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -277,12 +277,11 @@ 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-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||
'text-muted-foreground dark:text-background/80 w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||
{
|
||||
'text-left': parsedFieldMeta?.textAlign === 'left',
|
||||
'text-center':
|
||||
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
|
||||
'text-right': parsedFieldMeta?.textAlign === 'right',
|
||||
// Todo: Test
|
||||
'!text-center': parsedFieldMeta?.textAlign === 'center',
|
||||
'!text-right': parsedFieldMeta?.textAlign === 'right',
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -304,11 +303,9 @@ 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 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 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':
|
||||
userInputHasErrors,
|
||||
'text-left': parsedFieldMeta?.textAlign === 'left',
|
||||
'text-center':
|
||||
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
|
||||
'text-center': 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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
|
||||
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
@@ -132,9 +132,6 @@ export const DocumentEditForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: setPasswordForDocument } =
|
||||
trpc.document.setPasswordForDocument.useMutation();
|
||||
|
||||
const documentFlow: Record<EditDocumentStep, DocumentFlowStep> = {
|
||||
settings: {
|
||||
title: msg`General`,
|
||||
@@ -315,13 +312,6 @@ export const DocumentEditForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onPasswordSubmit = async (password: string) => {
|
||||
await setPasswordForDocument({
|
||||
documentId: document.id,
|
||||
password,
|
||||
});
|
||||
};
|
||||
|
||||
const currentDocumentFlow = documentFlow[step];
|
||||
|
||||
/**
|
||||
@@ -340,12 +330,10 @@ export const DocumentEditForm = ({
|
||||
gradient
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<LazyPDFViewer
|
||||
<PDFViewer
|
||||
key={document.documentData.id}
|
||||
documentData={document.documentData}
|
||||
document={document}
|
||||
password={document.documentMeta?.password}
|
||||
onPasswordSubmit={onPasswordSubmit}
|
||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { PDFViewer } from '@documenso/ui/primitives/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">
|
||||
<LazyPDFViewer
|
||||
<PDFViewer
|
||||
key={templateDocumentData.id}
|
||||
documentData={templateDocumentData}
|
||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||
|
||||
@@ -17,6 +17,7 @@ function PosthogInit() {
|
||||
if (postHogConfig) {
|
||||
posthog.init(postHogConfig.key, {
|
||||
api_host: postHogConfig.host,
|
||||
capture_exceptions: true,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import Plausible from 'plausible-tracker';
|
||||
import posthog from 'posthog-js';
|
||||
import {
|
||||
Links,
|
||||
Meta,
|
||||
@@ -180,6 +181,7 @@ 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 const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const requestHeaders = Object.fromEntries(request.headers.entries());
|
||||
|
||||
const session = await getOptionalSession(request);
|
||||
@@ -40,7 +40,7 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
banner,
|
||||
limits,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||
const { user, teams } = useSession();
|
||||
|
||||
@@ -13,10 +13,11 @@ 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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
|
||||
|
||||
import { DocumentHistorySheet } from '~/components/general/document/document-history-sheet';
|
||||
import { DocumentPageViewButton } from '~/components/general/document/document-page-view-button';
|
||||
@@ -24,7 +25,6 @@ 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,12 +196,16 @@ export default function DocumentPage() {
|
||||
gradient
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<LazyPDFViewer document={document} key={documentData.id} documentData={documentData} />
|
||||
<PDFViewer document={document} key={documentData.id} documentData={documentData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{document.status === DocumentStatus.PENDING && (
|
||||
<DocumentReadOnlyFields fields={fields} documentMeta={documentMeta || undefined} />
|
||||
{document.status !== DocumentStatus.COMPLETED && (
|
||||
<DocumentReadOnlyFields
|
||||
fields={fields}
|
||||
documentMeta={documentMeta || undefined}
|
||||
showRecipientTooltip={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||
|
||||
@@ -16,6 +16,7 @@ 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';
|
||||
|
||||
@@ -62,17 +63,17 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
const isMissingOrInactiveOrFreePlan =
|
||||
!subscription || subscription.status === SubscriptionStatus.INACTIVE;
|
||||
|
||||
return {
|
||||
return superLoaderJson({
|
||||
prices,
|
||||
subscription,
|
||||
subscriptionProductName: subscriptionProduct?.name,
|
||||
isMissingOrInactiveOrFreePlan,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function TeamsSettingBillingPage({ loaderData }: Route.ComponentProps) {
|
||||
export default function TeamsSettingBillingPage() {
|
||||
const { prices, subscription, subscriptionProductName, isMissingOrInactiveOrFreePlan } =
|
||||
loaderData;
|
||||
useSuperLoaderData<typeof loader>();
|
||||
|
||||
const { i18n } = useLingui();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ 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';
|
||||
|
||||
@@ -31,16 +32,16 @@ export async function loader({ request, params }: Route.LoaderArgs) {
|
||||
teamSubscription = await stripe.subscriptions.retrieve(team.subscription.planId);
|
||||
}
|
||||
|
||||
return {
|
||||
return superLoaderJson({
|
||||
team,
|
||||
teamSubscription,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function TeamsSettingBillingPage({ loaderData }: Route.ComponentProps) {
|
||||
export default function TeamsSettingBillingPage() {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { team, teamSubscription } = loaderData;
|
||||
const { team, teamSubscription } = useSuperLoaderData<typeof loader>();
|
||||
|
||||
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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { PDFViewer } from '@documenso/ui/primitives/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,17 +144,14 @@ export default function TemplatePage() {
|
||||
gradient
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<LazyPDFViewer
|
||||
document={template}
|
||||
key={template.id}
|
||||
documentData={templateDocumentData}
|
||||
/>
|
||||
<PDFViewer document={template} key={template.id} documentData={templateDocumentData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<DocumentReadOnlyFields
|
||||
fields={readOnlyFields}
|
||||
showFieldStatus={false}
|
||||
showRecipientTooltip={true}
|
||||
documentMeta={mockedDocumentMeta}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,14 +1,47 @@
|
||||
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 { isAuthenticated } = await getOptionalSession(request);
|
||||
const session = await getOptionalSession(request);
|
||||
|
||||
if (isAuthenticated) {
|
||||
throw redirect('/documents');
|
||||
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));
|
||||
}
|
||||
|
||||
throw redirect('/signin');
|
||||
|
||||
@@ -145,7 +145,9 @@ export default function EmbedDirectTemplatePage() {
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
metadata={template.templateMeta}
|
||||
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||
hidePoweredBy={
|
||||
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
|
||||
}
|
||||
allowWhiteLabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
|
||||
/>
|
||||
</DocumentSigningRecipientProvider>
|
||||
|
||||
@@ -169,7 +169,9 @@ export default function EmbedSignDocumentPage() {
|
||||
fields={fields}
|
||||
metadata={document.documentMeta}
|
||||
isCompleted={document.status === DocumentStatus.COMPLETED}
|
||||
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||
hidePoweredBy={
|
||||
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
|
||||
}
|
||||
allowWhitelabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
|
||||
allRecipients={allRecipients}
|
||||
/>
|
||||
|
||||
1
apps/remix/app/types/empty-module.ts
Normal file
1
apps/remix/app/types/empty-module.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
@@ -49,8 +49,8 @@
|
||||
"luxon": "^3.4.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"plausible-tracker": "^0.3.9",
|
||||
"posthog-js": "^1.223.3",
|
||||
"posthog-node": "^4.7.0",
|
||||
"posthog-js": "^1.224.0",
|
||||
"posthog-node": "^4.8.1",
|
||||
"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.1",
|
||||
"@react-router/dev": "^7.1.5",
|
||||
"@react-router/remix-routes-option-adapter": "^7.1.5",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-commonjs": "^28.0.2",
|
||||
|
||||
75
apps/remix/server/middleware.ts
Normal file
75
apps/remix/server/middleware.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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.*)/;
|
||||
@@ -9,6 +9,7 @@ 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';
|
||||
|
||||
@@ -26,6 +27,11 @@ const app = new Hono<HonoEnv>();
|
||||
app.use(contextStorage());
|
||||
app.use(appContext);
|
||||
|
||||
/**
|
||||
* RR7 app middleware.
|
||||
*/
|
||||
app.use('*', appMiddleware);
|
||||
|
||||
// Auth server.
|
||||
app.route('/api/auth', auth);
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ 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',
|
||||
},
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -14,6 +16,7 @@ const config: LinguiConfig = {
|
||||
},
|
||||
],
|
||||
compileNamespace: 'es',
|
||||
format: formatter({ lineNumbers: false }),
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -131,8 +131,8 @@
|
||||
"luxon": "^3.4.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"plausible-tracker": "^0.3.9",
|
||||
"posthog-js": "^1.223.3",
|
||||
"posthog-node": "^4.7.0",
|
||||
"posthog-js": "^1.224.0",
|
||||
"posthog-node": "^4.8.1",
|
||||
"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.1",
|
||||
"@react-router/dev": "^7.1.5",
|
||||
"@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.7.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.7.0.tgz",
|
||||
"integrity": "sha512-RgdUKSW8MfMOkjUa8cYVqWndNjPePNuuxlGbrZC6z1WRBsVc6TdGl8caidmC10RW8mu/BOfmrGbP4cRTo2jARg==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.8.1.tgz",
|
||||
"integrity": "sha512-ApMEC1+DbctP/88+VhaCl8SRKpIoReibMf7Mb3rxw3yMthr1rKaM4opbHdZJ0buLhwS5zX8B2ckqLjpwpSjRPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.4"
|
||||
@@ -31060,9 +31060,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.223.3",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.223.3.tgz",
|
||||
"integrity": "sha512-ZQTc17M21IzkQmECJa2Xjont4tZrvIn252uGT3sTfmahTqZoW4j+kBj4eOJt9SNR6hOheFNkg7MSiI/rA6FaDA==",
|
||||
"version": "1.224.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.224.0.tgz",
|
||||
"integrity": "sha512-JT1XQQeYs0CKb4lU2vujmeLTDLWc61I5lT7d6oG/H/cnCpXAqBi5rMuCFFeotHeMy3hqJ/Tpu3eAPFE2p5ErHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
@@ -41567,6 +41567,7 @@
|
||||
"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",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
|
||||
@@ -50,13 +52,6 @@ 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,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;
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
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';
|
||||
@@ -12,18 +24,6 @@ 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,4 +1,5 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import {
|
||||
ZFindTeamMembersResponseSchema,
|
||||
@@ -10,7 +11,6 @@ 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 '@documenso/prisma/client';
|
||||
} 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';
|
||||
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 = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
||||
const enterprisePriceId = '';
|
||||
|
||||
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 = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
||||
const enterprisePriceId = '';
|
||||
|
||||
// Create a temporary PDF file for testing
|
||||
function createTempPdfFile() {
|
||||
|
||||
@@ -10,6 +10,7 @@ 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');
|
||||
@@ -38,15 +39,7 @@ export const sessionCookieOptions = {
|
||||
} as const;
|
||||
|
||||
export const extractSessionCookieFromHeaders = (headers: Headers): string | null => {
|
||||
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();
|
||||
return extractCookieFromHeaders(sessionCookieName, headers);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
14
packages/auth/server/lib/utils/cookies.ts
Normal file
14
packages/auth/server/lib/utils/cookies.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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,3 +1,4 @@
|
||||
import { UserSecurityAuditLogType } from '@prisma/client';
|
||||
import { OAuth2Client, decodeIdToken } from 'arctic';
|
||||
import type { Context } from 'hono';
|
||||
import { deleteCookie } from 'hono/cookie';
|
||||
@@ -6,7 +7,6 @@ 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,5 +1,6 @@
|
||||
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';
|
||||
@@ -22,7 +23,6 @@ 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,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,7 +1,8 @@
|
||||
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,15 +4,14 @@ 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 = typeof plan === 'string' ? [plan] : plan;
|
||||
const planTypes: string[] = typeof plan === 'string' ? [plan] : plan;
|
||||
|
||||
const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR ');
|
||||
|
||||
const { data: prices } = await stripe.prices.search({
|
||||
query,
|
||||
const prices = await stripe.prices.list({
|
||||
expand: ['data.product'],
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return prices.filter((price) => price.type === 'recurring');
|
||||
return prices.data.filter(
|
||||
(price) => price.type === 'recurring' && planTypes.includes(price.metadata.plan),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
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,6 +1,7 @@
|
||||
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,7 +1,8 @@
|
||||
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,7 +1,8 @@
|
||||
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,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,6 +1,7 @@
|
||||
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';
|
||||
|
||||
@@ -10,7 +11,6 @@ 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';
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"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,7 +17,6 @@ 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';
|
||||
@@ -104,13 +103,14 @@ export const sealDocument = async ({
|
||||
// !: Need to write the fields onto the document as a hard copy
|
||||
const pdfData = await getFileServerSide(documentData);
|
||||
|
||||
const certificateData =
|
||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
// debugging........
|
||||
// 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,5 +1,6 @@
|
||||
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,7 +36,8 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
const isSignatureField = isSignatureFieldType(field.type);
|
||||
const isDebugMode =
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true';
|
||||
false; // todo
|
||||
// true || process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true';
|
||||
|
||||
pdf.registerFontkit(fontkit);
|
||||
|
||||
@@ -227,8 +228,13 @@ 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 * 16;
|
||||
const offsetY = index * checkboxSpaceY + topPadding;
|
||||
|
||||
const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`);
|
||||
|
||||
@@ -237,7 +243,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
}
|
||||
|
||||
page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
|
||||
x: fieldX + 16,
|
||||
x: fieldX + leftCheckboxPadding + leftCheckboxLabelPadding,
|
||||
y: pageHeight - (fieldY + offsetY),
|
||||
size: 12,
|
||||
font,
|
||||
@@ -245,7 +251,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
});
|
||||
|
||||
checkbox.addToPage(page, {
|
||||
x: fieldX,
|
||||
x: fieldX + leftCheckboxPadding,
|
||||
y: pageHeight - (fieldY + offsetY),
|
||||
height: 8,
|
||||
width: 8,
|
||||
@@ -268,21 +274,28 @@ 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 * 16;
|
||||
const offsetY = index * radioSpaceY + topPadding;
|
||||
|
||||
const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`);
|
||||
|
||||
// Draw label.
|
||||
page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
|
||||
x: fieldX + 16,
|
||||
x: fieldX + leftRadioPadding + leftRadioLabelPadding,
|
||||
y: pageHeight - (fieldY + offsetY),
|
||||
size: 12,
|
||||
font,
|
||||
rotate: degrees(pageRotationInDegrees),
|
||||
});
|
||||
|
||||
// Draw radio button.
|
||||
radio.addOptionToPage(item.value, page, {
|
||||
x: fieldX,
|
||||
x: fieldX + leftRadioPadding,
|
||||
y: pageHeight - (fieldY + offsetY),
|
||||
height: 8,
|
||||
width: 8,
|
||||
@@ -308,7 +321,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 : 'center';
|
||||
const textAlign = meta?.success && meta.data.textAlign ? meta.data.textAlign : 'left'; // ???
|
||||
const longestLineInTextForWidth = field.customText
|
||||
.split('\n')
|
||||
.sort((a, b) => b.length - a.length)[0];
|
||||
@@ -325,41 +338,69 @@ 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; // PDF points, roughly equivalent to 0.5rem
|
||||
const padding = 8; // Todo: Play around with this.
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
let textY = fieldY + (fieldHeight - textHeight) / 2;
|
||||
|
||||
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
||||
textY = pageHeight - textY - textHeight;
|
||||
let textFieldBoxY = pageHeight - fieldY - fieldHeight;
|
||||
let textFieldBoxX = textX;
|
||||
|
||||
const textField = pdf.getForm().createTextField(`text.${field.secondaryId}`);
|
||||
|
||||
if (pageRotationInDegrees !== 0) {
|
||||
const adjustedPosition = adjustPositionForRotation(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
textX,
|
||||
textY,
|
||||
textFieldBoxX,
|
||||
textFieldBoxY,
|
||||
pageRotationInDegrees,
|
||||
);
|
||||
|
||||
textX = adjustedPosition.xPos;
|
||||
textY = adjustedPosition.yPos;
|
||||
textFieldBoxX = adjustedPosition.xPos;
|
||||
textFieldBoxY = adjustedPosition.yPos;
|
||||
}
|
||||
|
||||
page.drawText(field.customText, {
|
||||
x: textX,
|
||||
y: textY,
|
||||
size: fontSize,
|
||||
font,
|
||||
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.
|
||||
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,5 +1,6 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
export type GetApiTokensOptions = {
|
||||
userId: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 '@documenso/prisma/client';
|
||||
import type { Document, DocumentData, Recipient } from '@prisma/client';
|
||||
|
||||
export type DocumentWithRecipients = Document & {
|
||||
recipients: Recipient[];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 '@documenso/prisma/client';
|
||||
import type { Field, Signature } from '@prisma/client';
|
||||
|
||||
export type FieldWithSignature = Field & {
|
||||
signature?: Signature | null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
|
||||
export type RecipientWithFields = Recipient & {
|
||||
fields: Field[];
|
||||
|
||||
@@ -103,6 +103,14 @@ 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,3 +1,4 @@
|
||||
import type { Session } from '@prisma/client';
|
||||
import type { Context } from 'hono';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -5,7 +6,6 @@ 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,7 +4,6 @@ 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';
|
||||
@@ -26,7 +25,6 @@ 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';
|
||||
@@ -55,7 +53,6 @@ import {
|
||||
ZMoveDocumentToTeamSchema,
|
||||
ZResendDocumentMutationSchema,
|
||||
ZSearchDocumentsMutationSchema,
|
||||
ZSetPasswordForDocumentMutationSchema,
|
||||
ZSetSigningOrderForDocumentMutationSchema,
|
||||
ZSuccessResponseSchema,
|
||||
ZUpdateDocumentRequestSchema,
|
||||
@@ -444,35 +441,6 @@ 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 { LazyPDFViewerNoLoader } from '../../primitives/lazy-pdf-viewer';
|
||||
import PDFViewer from '../../primitives/pdf-viewer';
|
||||
|
||||
export type DocumentDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
@@ -43,7 +43,7 @@ export default function DocumentDialog({ trigger, documentData, ...props }: Docu
|
||||
)}
|
||||
onClick={() => props.onOpenChange?.(false)}
|
||||
>
|
||||
<LazyPDFViewerNoLoader
|
||||
<PDFViewer
|
||||
className="mx-auto w-full max-w-3xl xl:max-w-5xl"
|
||||
documentData={documentData}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
148
packages/ui/components/document/document-read-only-fields.tsx
Normal file
148
packages/ui/components/document/document-read-only-fields.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, 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;
|
||||
@@ -19,53 +17,6 @@ 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({
|
||||
@@ -76,13 +27,10 @@ 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`,
|
||||
@@ -97,10 +45,12 @@ export function FieldContainerPortal({
|
||||
);
|
||||
}
|
||||
|
||||
export function FieldRootContainer({ field, children, cardClassName }: FieldContainerPortalProps) {
|
||||
export function FieldRootContainer({ field, children }: FieldContainerPortalProps) {
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const signerStyles = useSignerColors(field.recipientId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
@@ -121,33 +71,36 @@ export function FieldRootContainer({ field, children, cardClassName }: FieldCont
|
||||
};
|
||||
}, []);
|
||||
|
||||
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 parsedField = useMemo(
|
||||
// () => (field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null),
|
||||
// [field.fieldMeta],
|
||||
// );
|
||||
|
||||
const cardClassNames = useMemo(
|
||||
() => getCardClassNames(field, parsedField, isValidating, isCheckboxOrRadio, cardClassName),
|
||||
[field, parsedField, isValidating, isCheckboxOrRadio, cardClassName],
|
||||
);
|
||||
// // todo: remove
|
||||
// const isCheckboxOrRadio = useMemo(
|
||||
// () => parsedField?.type === 'checkbox' || parsedField?.type === 'radio',
|
||||
// [parsedField],
|
||||
// );
|
||||
|
||||
return (
|
||||
<FieldContainerPortal field={field}>
|
||||
<Card
|
||||
<div
|
||||
id={`field-${field.id}`}
|
||||
ref={ref}
|
||||
data-field-type={field.type}
|
||||
data-inserted={field.inserted ? 'true' : 'false'}
|
||||
className={cardClassNames}
|
||||
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,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
{children}
|
||||
</div>
|
||||
</FieldContainerPortal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
// !: 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: {
|
||||
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))]',
|
||||
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))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-green)/15%)] active:bg-[hsl(var(--signer-green)/15%)]',
|
||||
},
|
||||
@@ -19,12 +17,9 @@ export const SIGNER_COLOR_STYLES = {
|
||||
|
||||
blue: {
|
||||
default: {
|
||||
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))]',
|
||||
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))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-blue)/15%)] active:bg-[hsl(var(--signer-blue)/15%)]',
|
||||
},
|
||||
@@ -32,12 +27,9 @@ export const SIGNER_COLOR_STYLES = {
|
||||
|
||||
purple: {
|
||||
default: {
|
||||
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))]',
|
||||
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))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-purple)/15%)] active:bg-[hsl(var(--signer-purple)/15%)]',
|
||||
},
|
||||
@@ -45,12 +37,9 @@ export const SIGNER_COLOR_STYLES = {
|
||||
|
||||
orange: {
|
||||
default: {
|
||||
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))]',
|
||||
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))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-orange)/15%)] active:bg-[hsl(var(--signer-orange)/15%)]',
|
||||
},
|
||||
@@ -58,12 +47,9 @@ export const SIGNER_COLOR_STYLES = {
|
||||
|
||||
yellow: {
|
||||
default: {
|
||||
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))]',
|
||||
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))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-yellow)/15%)] active:bg-[hsl(var(--signer-yellow)/15%)]',
|
||||
},
|
||||
@@ -71,12 +57,9 @@ export const SIGNER_COLOR_STYLES = {
|
||||
|
||||
pink: {
|
||||
default: {
|
||||
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))]',
|
||||
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))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-pink)/15%)] active:bg-[hsl(var(--signer-pink)/15%)]',
|
||||
},
|
||||
|
||||
@@ -88,7 +88,6 @@ export type FieldFormType = {
|
||||
|
||||
export type AddFieldsFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
hideRecipients?: boolean;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
onSubmit: (_data: TAddFieldsFormSchema) => void;
|
||||
@@ -100,7 +99,6 @@ export type AddFieldsFormProps = {
|
||||
|
||||
export const AddFieldsFormPartial = ({
|
||||
documentFlow,
|
||||
hideRecipients = false,
|
||||
recipients,
|
||||
fields,
|
||||
onSubmit,
|
||||
@@ -657,7 +655,6 @@ export const AddFieldsFormPartial = ({
|
||||
setCurrentField(field);
|
||||
handleAdvancedSettings();
|
||||
}}
|
||||
hideRecipients={hideRecipients}
|
||||
hasErrors={!!hasFieldError}
|
||||
active={activeFieldId === field.formId}
|
||||
onFieldActivate={() => setActiveFieldId(field.formId)}
|
||||
@@ -666,125 +663,123 @@ export const AddFieldsFormPartial = ({
|
||||
);
|
||||
})}
|
||||
|
||||
{!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>
|
||||
)}
|
||||
<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)}
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{roleRecipients.length === 0 && (
|
||||
<div
|
||||
key={`${role}-empty`}
|
||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
||||
{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,
|
||||
})}
|
||||
>
|
||||
<Trans>No recipients with this role</Trans>
|
||||
</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,
|
||||
},
|
||||
{recipient.name && (
|
||||
<span title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
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>
|
||||
{!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>
|
||||
<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>
|
||||
)}
|
||||
<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,6 +21,10 @@ 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,
|
||||
@@ -54,7 +58,6 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSettingsFormProps = {
|
||||
@@ -145,10 +148,9 @@ export const AddSettingsFormPartial = ({
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields fields={mapFieldsWithRecipients(fields, recipients)} />
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<fieldset
|
||||
|
||||
@@ -23,6 +23,10 @@ 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';
|
||||
@@ -39,7 +43,6 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import { SigningOrderConfirmation } from './signing-order-confirmation';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
@@ -361,10 +364,9 @@ export const AddSignersFormPartial = ({
|
||||
description={documentFlow.description}
|
||||
/>
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields fields={mapFieldsWithRecipients(fields, recipients)} />
|
||||
)}
|
||||
|
||||
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
|
||||
<Form {...form}>
|
||||
|
||||
@@ -16,6 +16,10 @@ 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';
|
||||
@@ -31,7 +35,6 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSubjectFormProps = {
|
||||
@@ -101,10 +104,9 @@ export const AddSubjectFormPartial = ({
|
||||
/>
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex flex-col">
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields fields={mapFieldsWithRecipients(fields, recipients)} />
|
||||
)}
|
||||
|
||||
<Tabs
|
||||
onValueChange={(value) =>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
166
packages/ui/primitives/document-flow/field-content.tsx
Normal file
166
packages/ui/primitives/document-flow/field-content.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
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,9 +11,7 @@ import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-
|
||||
|
||||
import { useSignerColors } from '../../lib/signer-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { CheckboxField } from './advanced-fields/checkbox';
|
||||
import { RadioField } from './advanced-fields/radio';
|
||||
import { FieldIcon } from './field-icon';
|
||||
import { FieldContent } from './field-content';
|
||||
import type { TDocumentFlowFormSchema } from './types';
|
||||
|
||||
type Field = TDocumentFlowFormSchema['fields'][0];
|
||||
@@ -34,13 +32,15 @@ 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,7 +57,6 @@ export const FieldItem = ({
|
||||
onBlur,
|
||||
onAdvancedSettings,
|
||||
recipientIndex = 0,
|
||||
hideRecipients = false,
|
||||
hasErrors,
|
||||
active,
|
||||
onFieldActivate,
|
||||
@@ -174,11 +173,35 @@ export const FieldItem = ({
|
||||
() => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta),
|
||||
[field.fieldMeta],
|
||||
);
|
||||
|
||||
const radioHasValues = useMemo(
|
||||
() => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta),
|
||||
[field.fieldMeta],
|
||||
);
|
||||
|
||||
const hasCheckedValues = (fieldMeta: TFieldMetaSchema, type: FieldType) => {
|
||||
if (!fieldMeta || (type !== FieldType.RADIO && type !== FieldType.CHECKBOX)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type === FieldType.RADIO) {
|
||||
const parsed = ZRadioFieldMeta.parse(fieldMeta);
|
||||
return parsed.values?.some((value) => value.checked) ?? false;
|
||||
}
|
||||
|
||||
if (type === FieldType.CHECKBOX) {
|
||||
const parsed = ZCheckboxFieldMeta.parse(fieldMeta);
|
||||
return parsed.values?.some((value) => value.checked) ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const fieldHasCheckedValues = useMemo(
|
||||
() => hasCheckedValues(field.fieldMeta, field.type),
|
||||
[field.fieldMeta, field.type],
|
||||
);
|
||||
|
||||
const fixedSize = checkBoxHasValues || radioHasValues;
|
||||
|
||||
return createPortal(
|
||||
@@ -218,13 +241,28 @@ 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',
|
||||
'relative flex h-full w-full items-center justify-center bg-white/90 ring-2 transition-colors',
|
||||
!hasErrors && signerStyles.default.base,
|
||||
!hasErrors && signerStyles.default.fieldItem,
|
||||
{
|
||||
'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)]':
|
||||
'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)]':
|
||||
hasErrors,
|
||||
},
|
||||
!fixedSize && '[container-type:size]',
|
||||
@@ -239,33 +277,27 @@ export const FieldItem = ({
|
||||
ref={$el}
|
||||
data-field-id={field.nativeId}
|
||||
>
|
||||
{match(field.type)
|
||||
.with('CHECKBOX', () => <CheckboxField field={field} />)
|
||||
.with('RADIO', () => <RadioField field={field} />)
|
||||
.otherwise(() => (
|
||||
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
|
||||
))}
|
||||
<FieldContent field={field} />
|
||||
|
||||
{!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>
|
||||
{/* 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() ?? '')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!disabled && settingsActive && (
|
||||
<div className="z-[60] mt-1 flex justify-center">
|
||||
<div className="absolute z-[60] mt-1 flex w-full items-center 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,6 +125,18 @@ export const CheckboxFieldAdvancedSettings = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="mb-2">
|
||||
<Label>
|
||||
<Trans>Label</Trans>
|
||||
</Label>
|
||||
<Input
|
||||
id="label"
|
||||
className="bg-background mt-2"
|
||||
placeholder={_(msg`Field label`)}
|
||||
value={fieldState.label}
|
||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-x-4">
|
||||
<div className="flex w-2/3 flex-col">
|
||||
<Label>
|
||||
@@ -205,7 +217,6 @@ 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
Reference in New Issue
Block a user