Compare commits
20 Commits
v1.9.1-rc.
...
power-sign
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12eb82629e | ||
|
|
108060cc9a | ||
|
|
ad7720b778 | ||
|
|
b13cd61731 | ||
|
|
43e5bcf1df | ||
|
|
26d63690c5 | ||
|
|
26640e1fec | ||
|
|
044182966b | ||
|
|
b07139c0d2 | ||
|
|
beafc366f7 | ||
|
|
adcdf2df58 | ||
|
|
5210256ae1 | ||
|
|
807e65d7e6 | ||
|
|
b3f2ab7f95 | ||
|
|
066f88653e | ||
|
|
0e426dd1d1 | ||
|
|
95b95a2614 | ||
|
|
733a300c93 | ||
|
|
b3ade016e1 | ||
|
|
eb96f315b6 |
@@ -4,7 +4,6 @@ import { notFound } from 'next/navigation';
|
|||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { CheckCircle2, Clock8 } from 'lucide-react';
|
import { CheckCircle2, Clock8 } from 'lucide-react';
|
||||||
import { getServerSession } from 'next-auth';
|
|
||||||
import { env } from 'next-runtime-env';
|
import { env } from 'next-runtime-env';
|
||||||
import { match } from 'ts-pattern';
|
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 { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-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 { 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 { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-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 { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
import { Badge } from '@documenso/ui/primitives/badge';
|
||||||
@@ -61,9 +62,10 @@ export default async function CompletedSigningPage({
|
|||||||
|
|
||||||
const { documentData } = document;
|
const { documentData } = document;
|
||||||
|
|
||||||
const [fields, recipient] = await Promise.all([
|
const [fields, recipient, nextInboxDocument] = await Promise.all([
|
||||||
getFieldsForToken({ token }),
|
getFieldsForToken({ token }),
|
||||||
getRecipientByToken({ token }).catch(() => null),
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
|
getNextInboxDocument({ email: user?.email }).catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
@@ -91,8 +93,7 @@ export default async function CompletedSigningPage({
|
|||||||
fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
||||||
recipient.email;
|
recipient.email;
|
||||||
|
|
||||||
const sessionData = await getServerSession();
|
const isLoggedIn = !!user;
|
||||||
const isLoggedIn = !!sessionData?.user;
|
|
||||||
const canSignUp = !isExistingUser && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
const canSignUp = !isExistingUser && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -182,12 +183,16 @@ export default async function CompletedSigningPage({
|
|||||||
</p>
|
</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} />
|
<DocumentShareButton documentId={document.id} token={recipient.token} />
|
||||||
|
|
||||||
{document.status === DocumentStatus.COMPLETED ? (
|
{document.status === DocumentStatus.COMPLETED ? (
|
||||||
<DocumentDownloadButton
|
<DocumentDownloadButton
|
||||||
className="flex-1"
|
className="flex-1 text-xs"
|
||||||
fileName={document.title}
|
fileName={document.title}
|
||||||
documentData={documentData}
|
documentData={documentData}
|
||||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||||
@@ -199,6 +204,15 @@ export default async function CompletedSigningPage({
|
|||||||
documentData={documentData}
|
documentData={documentData}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isLoggedIn && nextInboxDocument && (
|
||||||
|
<NextInboxItemButton
|
||||||
|
className="text-xs"
|
||||||
|
userEmail={user?.email}
|
||||||
|
documentData={documentData}
|
||||||
|
nextInboxDocument={nextInboxDocument}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -220,7 +234,7 @@ export default async function CompletedSigningPage({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoggedIn && (
|
{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>
|
<Trans>Go Back Home</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
11946
package-lock.json
generated
11946
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
packages/lib/server-only/user/get-next-inbox-document.ts
Normal file
46
packages/lib/server-only/user/get-next-inbox-document.ts
Normal file
@@ -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' }],
|
||||||
|
});
|
||||||
|
};
|
||||||
79
packages/ui/components/document/document-status.tsx
Normal file
79
packages/ui/components/document/document-status.tsx
Normal file
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
131
packages/ui/components/document/next-inbox-item-button.tsx
Normal file
131
packages/ui/components/document/next-inbox-item-button.tsx
Normal file
@@ -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",
|
"tailwind-merge": "^1.12.0",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
|
"vaul": "^1.0.0",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user