Compare commits

...

20 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan
12eb82629e fix: build errors 2025-03-03 17:17:45 +00:00
Lucas Smith
108060cc9a Merge branch 'main' into power-signer 2025-02-28 21:11:35 +11:00
Ephraim Atta-Duncan
ad7720b778 fix: merge conflicts 2025-02-06 12:23:46 +00:00
Ephraim Duncan
b13cd61731 Merge branch 'main' into power-signer 2024-11-21 17:21:08 +00:00
Ephraim Atta-Duncan
43e5bcf1df fix: match translations with main 2024-11-19 07:47:58 +00:00
Ephraim Atta-Duncan
26d63690c5 chore: add gitignore 2024-11-19 07:40:56 +00:00
Ephraim Atta-Duncan
26640e1fec fix: translations 2024-11-19 07:22:44 +00:00
Ephraim Duncan
044182966b Merge branch 'main' into power-signer 2024-11-19 07:19:02 +00:00
Ephraim Atta-Duncan
b07139c0d2 chore: merge with main 2024-11-18 07:32:10 +00:00
Ephraim Atta-Duncan
beafc366f7 fix: merge conflicts 2024-11-15 10:50:31 +00:00
Ephraim Duncan
adcdf2df58 Merge branch 'main' into power-signer 2024-10-30 00:24:43 +00:00
Ephraim Atta-Duncan
5210256ae1 fix: translations conflicts 2024-10-28 10:21:26 +00:00
Ephraim Atta-Duncan
807e65d7e6 chore: delete translations 2024-10-28 10:19:36 +00:00
Ephraim Atta-Duncan
b3f2ab7f95 fix: translations 2024-10-28 10:16:59 +00:00
Ephraim Atta-Duncan
066f88653e chore: remove duplicate property 2024-10-28 10:13:16 +00:00
Ephraim Atta-Duncan
0e426dd1d1 fix: merge conflicts 2024-10-28 10:09:49 +00:00
Ephraim Duncan
95b95a2614 Merge branch 'main' into power-signer 2024-10-16 14:10:55 +00:00
Ephraim Atta-Duncan
733a300c93 fix: merge conflicts 2024-10-15 17:03:25 +00:00
Ephraim Atta-Duncan
b3ade016e1 chore: use shadcn sheets 2024-10-11 18:43:09 +00:00
Ephraim Atta-Duncan
eb96f315b6 feat: power signing mode 2024-10-10 20:23:32 +00:00
8 changed files with 3398 additions and 8835 deletions

View File

@@ -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>
)}

11946
package-lock.json generated

File diff suppressed because it is too large Load Diff

View 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' }],
});
};

View 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>
);
};

View 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>
);
};

View File

@@ -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"
}
}
}