mirror of
https://github.com/documenso/documenso.git
synced 2026-07-11 05:25:08 +10:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12eb82629e | |||
| 108060cc9a | |||
| 1789eff564 | |||
| 235d846d2b | |||
| ca3d65ad10 | |||
| 617e3a46e0 | |||
| 255c33cdab | |||
| ad7720b778 | |||
| b13cd61731 | |||
| 43e5bcf1df | |||
| 26d63690c5 | |||
| 26640e1fec | |||
| 044182966b | |||
| b07139c0d2 | |||
| beafc366f7 | |||
| adcdf2df58 | |||
| 5210256ae1 | |||
| 807e65d7e6 | |||
| b3f2ab7f95 | |||
| 066f88653e | |||
| 0e426dd1d1 | |||
| 95b95a2614 | |||
| 733a300c93 | |||
| b3ade016e1 | |||
| eb96f315b6 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.9.1-rc.5",
|
||||
"version": "1.9.1-rc.7",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { notFound } from 'next/navigation';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckCircle2, Clock8 } from 'lucide-react';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { env } from 'next-runtime-env';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@@ -16,10 +15,12 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
import { getNextInboxDocument } from '@documenso/lib/server-only/user/get-next-inbox-document';
|
||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
import { NextInboxItemButton } from '@documenso/ui/components/document/next-inbox-item-button';
|
||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
@@ -61,9 +62,10 @@ export default async function CompletedSigningPage({
|
||||
|
||||
const { documentData } = document;
|
||||
|
||||
const [fields, recipient] = await Promise.all([
|
||||
const [fields, recipient, nextInboxDocument] = await Promise.all([
|
||||
getFieldsForToken({ token }),
|
||||
getRecipientByToken({ token }).catch(() => null),
|
||||
getNextInboxDocument({ email: user?.email }).catch(() => null),
|
||||
]);
|
||||
|
||||
if (!recipient) {
|
||||
@@ -91,8 +93,7 @@ export default async function CompletedSigningPage({
|
||||
fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
||||
recipient.email;
|
||||
|
||||
const sessionData = await getServerSession();
|
||||
const isLoggedIn = !!sessionData?.user;
|
||||
const isLoggedIn = !!user;
|
||||
const canSignUp = !isExistingUser && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
||||
|
||||
return (
|
||||
@@ -182,12 +183,16 @@ export default async function CompletedSigningPage({
|
||||
</p>
|
||||
))}
|
||||
|
||||
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
||||
<div
|
||||
className={cn('mt-8 flex w-full items-center justify-center gap-4', {
|
||||
'max-w-sm': !nextInboxDocument,
|
||||
})}
|
||||
>
|
||||
<DocumentShareButton documentId={document.id} token={recipient.token} />
|
||||
|
||||
{document.status === DocumentStatus.COMPLETED ? (
|
||||
<DocumentDownloadButton
|
||||
className="flex-1"
|
||||
className="flex-1 text-xs"
|
||||
fileName={document.title}
|
||||
documentData={documentData}
|
||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||
@@ -199,6 +204,15 @@ export default async function CompletedSigningPage({
|
||||
documentData={documentData}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoggedIn && nextInboxDocument && (
|
||||
<NextInboxItemButton
|
||||
className="text-xs"
|
||||
userEmail={user?.email}
|
||||
documentData={documentData}
|
||||
nextInboxDocument={nextInboxDocument}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -220,7 +234,7 @@ export default async function CompletedSigningPage({
|
||||
)}
|
||||
|
||||
{isLoggedIn && (
|
||||
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-2">
|
||||
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-4">
|
||||
<Trans>Go Back Home</Trans>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -182,6 +182,23 @@ export const SigningFieldContainer = ({
|
||||
</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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
@@ -83,15 +83,12 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(false);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [hasAutoExpanded, setHasAutoExpanded] = useState(false);
|
||||
|
||||
const [isEmailLocked, setIsEmailLocked] = useState(false);
|
||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||
|
||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||
|
||||
const documentEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
||||
|
||||
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(() => fields);
|
||||
@@ -320,43 +317,6 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
}
|
||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||
|
||||
// Set up intersection observer to auto-expand widget when user reaches bottom of document
|
||||
useEffect(() => {
|
||||
if (!hasDocumentLoaded || !hasFinishedInit || hasAutoExpanded || isExpanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a delay to ensure document has fully rendered and stabilized
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Get the number of pages in the document
|
||||
const pageCount = document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR).length;
|
||||
|
||||
// Only set up the observer if there's more than one page
|
||||
if (pageCount <= 1) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const [entry] = entries;
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
setIsExpanded(true);
|
||||
setHasAutoExpanded(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 },
|
||||
);
|
||||
|
||||
if (documentEndRef.current) {
|
||||
observer.observe(documentEndRef.current);
|
||||
}
|
||||
}, 1500); // 1.5 second delay
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [hasDocumentLoaded, hasFinishedInit, hasAutoExpanded, isExpanded]);
|
||||
|
||||
if (hasCompletedDocument) {
|
||||
return (
|
||||
<EmbedDocumentCompleted
|
||||
@@ -384,14 +344,12 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
documentData={documentData}
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
{/* Observer target at the bottom of the document */}
|
||||
<div ref={documentEndRef} className="h-4 w-full" />
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
|
||||
@@ -110,7 +110,9 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
metadata={template.templateMeta}
|
||||
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||
hidePoweredBy={
|
||||
isCommunityPlan || isPlatformDocument || isEnterpriseDocument || hidePoweredBy
|
||||
}
|
||||
allowWhiteLabelling={isCommunityPlan || isPlatformDocument || isEnterpriseDocument}
|
||||
/>
|
||||
</RecipientProvider>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useId, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
@@ -91,14 +91,11 @@ export const EmbedSignDocumentClientPage = ({
|
||||
);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [hasAutoExpanded, setHasAutoExpanded] = useState(false);
|
||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||
|
||||
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
||||
|
||||
const documentEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
||||
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
||||
|
||||
@@ -244,42 +241,6 @@ export const EmbedSignDocumentClientPage = ({
|
||||
}
|
||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||
|
||||
// Set up intersection observer to auto-expand widget when user reaches bottom of document
|
||||
useEffect(() => {
|
||||
if (!hasDocumentLoaded || !hasFinishedInit || hasAutoExpanded || isExpanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a delay to ensure document has fully rendered and stabilized
|
||||
const timeoutId = setTimeout(() => {
|
||||
const pageCount = document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR).length;
|
||||
|
||||
// Only set up the observer if there's more than one page
|
||||
if (pageCount <= 1) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const [entry] = entries;
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
setIsExpanded(true);
|
||||
setHasAutoExpanded(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 },
|
||||
);
|
||||
|
||||
if (documentEndRef.current) {
|
||||
observer.observe(documentEndRef.current);
|
||||
}
|
||||
}, 1500); // 1.5 second delay
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [hasDocumentLoaded, hasFinishedInit, hasAutoExpanded, isExpanded]);
|
||||
|
||||
if (hasRejectedDocument) {
|
||||
return <EmbedDocumentRejected name={fullName} />;
|
||||
}
|
||||
@@ -322,14 +283,12 @@ export const EmbedSignDocumentClientPage = ({
|
||||
documentData={documentData}
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
{/* Observer target at the bottom of the document */}
|
||||
<div ref={documentEndRef} className="h-4 w-full" />
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
|
||||
@@ -131,7 +131,9 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
||||
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}
|
||||
/>
|
||||
|
||||
Generated
+3122
-8830
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.9.1-rc.5",
|
||||
"version": "1.9.1-rc.7",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
type GetNextInboxDocumentOptions = {
|
||||
email: string | undefined;
|
||||
};
|
||||
|
||||
export const getNextInboxDocument = async ({ email }: GetNextInboxDocumentOptions) => {
|
||||
if (!email) {
|
||||
throw new Error('User is required');
|
||||
}
|
||||
|
||||
return await prisma.document.findMany({
|
||||
where: {
|
||||
recipients: {
|
||||
some: {
|
||||
email,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
},
|
||||
},
|
||||
},
|
||||
status: { not: DocumentStatus.DRAFT },
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
title: true,
|
||||
status: true,
|
||||
recipients: {
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
select: {
|
||||
token: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
},
|
||||
orderBy: [{ createdAt: 'asc' }],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckCircle2, Clock, File } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||
|
||||
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
type FriendlyStatus = {
|
||||
label: MessageDescriptor;
|
||||
labelExtended: MessageDescriptor;
|
||||
icon?: LucideIcon;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export const FRIENDLY_STATUS_MAP: Record<ExtendedDocumentStatus, FriendlyStatus> = {
|
||||
PENDING: {
|
||||
label: msg`Pending`,
|
||||
labelExtended: msg`Document pending`,
|
||||
icon: Clock,
|
||||
color: 'text-blue-600 dark:text-blue-300',
|
||||
},
|
||||
COMPLETED: {
|
||||
label: msg`Completed`,
|
||||
labelExtended: msg`Document completed`,
|
||||
icon: CheckCircle2,
|
||||
color: 'text-green-500 dark:text-green-300',
|
||||
},
|
||||
DRAFT: {
|
||||
label: msg`Draft`,
|
||||
labelExtended: msg`Document draft`,
|
||||
icon: File,
|
||||
color: 'text-yellow-500 dark:text-yellow-200',
|
||||
},
|
||||
INBOX: {
|
||||
label: msg`Inbox`,
|
||||
labelExtended: msg`Document inbox`,
|
||||
icon: SignatureIcon,
|
||||
color: 'text-muted-foreground',
|
||||
},
|
||||
ALL: {
|
||||
label: msg`All`,
|
||||
labelExtended: msg`Document All`,
|
||||
color: 'text-muted-foreground',
|
||||
},
|
||||
};
|
||||
|
||||
export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
||||
status: ExtendedDocumentStatus;
|
||||
inheritColor?: boolean;
|
||||
};
|
||||
|
||||
export const DocumentStatus = ({
|
||||
className,
|
||||
status,
|
||||
inheritColor,
|
||||
...props
|
||||
}: DocumentStatusProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { label, icon: Icon, color } = FRIENDLY_STATUS_MAP[status];
|
||||
|
||||
return (
|
||||
<span className={cn('flex items-center', className)} {...props}>
|
||||
{Icon && (
|
||||
<Icon
|
||||
className={cn('mr-2 inline-block h-4 w-4', {
|
||||
[color]: !inheritColor,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{_(label)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { CheckCircle, EyeIcon, Pencil } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { type DocumentData, type Prisma, RecipientRole } from '@documenso/prisma/client';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@documenso/ui/primitives/sheet';
|
||||
|
||||
import { DocumentStatus } from './document-status';
|
||||
|
||||
type GetNextInboxDocumentResult =
|
||||
| Prisma.DocumentGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
createdAt: true;
|
||||
title: true;
|
||||
status: true;
|
||||
recipients: {
|
||||
select: {
|
||||
token: true;
|
||||
role: true;
|
||||
};
|
||||
};
|
||||
documentMeta: true;
|
||||
};
|
||||
}>[]
|
||||
| null;
|
||||
|
||||
export type NextInboxItemButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
disabled?: boolean;
|
||||
documentData?: DocumentData;
|
||||
userEmail: string | undefined;
|
||||
nextInboxDocument: GetNextInboxDocumentResult;
|
||||
};
|
||||
|
||||
export const NextInboxItemButton = ({
|
||||
className,
|
||||
documentData,
|
||||
nextInboxDocument,
|
||||
userEmail,
|
||||
disabled,
|
||||
...props
|
||||
}: NextInboxItemButtonProps) => {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className={className}
|
||||
disabled={disabled || !documentData || !userEmail}
|
||||
{...props}
|
||||
>
|
||||
<SignatureIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Sign Next Document</Trans>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-2xl">Inbox</SheetTitle>
|
||||
<SheetDescription>Documents awaiting your signature or review</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="mt-8 space-y-6">
|
||||
{nextInboxDocument?.map((document) => {
|
||||
const recipient = document.recipients[0];
|
||||
|
||||
return (
|
||||
<div key={document.id} className="flex items-center justify-between space-y-1">
|
||||
<div>
|
||||
<p className="text-foreground text-lg font-semibold">{document.title}</p>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<DocumentStatus status={document.status} />
|
||||
|
||||
{document.createdAt && (
|
||||
<p className="text-muted-foreground">
|
||||
<Trans>
|
||||
Created {DateTime.fromJSDate(document.createdAt).toFormat('LLL ‘yy')}
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button asChild className="w-28">
|
||||
<Link href={`/sign/${recipient?.token}`}>
|
||||
{match(recipient?.role)
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<>
|
||||
<Pencil className="-ml-1 mr-2 h-4 w-4" />
|
||||
<Trans>Sign</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
<>
|
||||
<CheckCircle className="-ml-1 mr-2 h-4 w-4" />
|
||||
<Trans>Approve</Trans>
|
||||
</>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<>
|
||||
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
|
||||
<Trans>View</Trans>
|
||||
</>
|
||||
))}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
@@ -78,6 +78,7 @@
|
||||
"tailwind-merge": "^1.12.0",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"vaul": "^1.0.0",
|
||||
"zod": "3.24.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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';
|
||||
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
import { useSignerColors } from '../../lib/signer-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
@@ -185,11 +186,35 @@ export const FieldItem = ({
|
||||
() => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta),
|
||||
[field.fieldMeta],
|
||||
);
|
||||
|
||||
const radioHasValues = useMemo(
|
||||
() => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta),
|
||||
[field.fieldMeta],
|
||||
);
|
||||
|
||||
const hasCheckedValues = (fieldMeta: TFieldMetaSchema, type: FieldType) => {
|
||||
if (!fieldMeta || (type !== FieldType.RADIO && type !== FieldType.CHECKBOX)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type === FieldType.RADIO) {
|
||||
const parsed = ZRadioFieldMeta.parse(fieldMeta);
|
||||
return parsed.values?.some((value) => value.checked) ?? false;
|
||||
}
|
||||
|
||||
if (type === FieldType.CHECKBOX) {
|
||||
const parsed = ZCheckboxFieldMeta.parse(fieldMeta);
|
||||
return parsed.values?.some((value) => value.checked) ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const fieldHasCheckedValues = useMemo(
|
||||
() => hasCheckedValues(field.fieldMeta, field.type),
|
||||
[field.fieldMeta, field.type],
|
||||
);
|
||||
|
||||
const fixedSize = checkBoxHasValues || radioHasValues;
|
||||
|
||||
return createPortal(
|
||||
@@ -229,6 +254,21 @@ export const FieldItem = ({
|
||||
onMove?.(d.node);
|
||||
}}
|
||||
>
|
||||
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
|
||||
field.fieldMeta?.label && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute -top-16 left-0 right-0 rounded-md p-2 text-center text-xs text-gray-700',
|
||||
{
|
||||
'bg-foreground/5 border-primary border': !fieldHasCheckedValues,
|
||||
'bg-documenso-200 border-primary border': fieldHasCheckedValues,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{field.fieldMeta.label}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full w-full items-center justify-center bg-white',
|
||||
|
||||
@@ -126,6 +126,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>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
|
||||
|
||||
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
||||
@@ -27,6 +28,8 @@ export const RadioFieldAdvancedSettings = ({
|
||||
handleFieldChange,
|
||||
handleErrors,
|
||||
}: RadioFieldAdvancedSettingsProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [showValidation, setShowValidation] = useState(false);
|
||||
const [values, setValues] = useState(
|
||||
fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }],
|
||||
@@ -102,6 +105,18 @@ export const RadioFieldAdvancedSettings = ({
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>Label</Trans>
|
||||
</Label>
|
||||
<Input
|
||||
id="label"
|
||||
className="bg-background mt-2"
|
||||
placeholder={_(msg`Field label`)}
|
||||
value={fieldState.label}
|
||||
onChange={(e) => handleFieldChange('label', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Switch
|
||||
className="bg-background"
|
||||
|
||||
Reference in New Issue
Block a user