Merge branch 'feat/refresh' into feat/improve-readability

This commit is contained in:
Adithya Krishna
2023-08-30 20:34:08 +05:30
committed by GitHub
83 changed files with 1388 additions and 753 deletions

View File

@ -22,14 +22,14 @@ import { LocaleDate } from '~/components/formatter/locale-date';
import { UploadDocument } from './upload-document';
export default async function DashboardPage() {
const session = await getRequiredServerComponentSession();
const user = await getRequiredServerComponentSession();
const [stats, results] = await Promise.all([
getStats({
userId: session.id,
user,
}),
findDocuments({
userId: session.id,
userId: user.id,
perPage: 10,
}),
]);

View File

@ -0,0 +1,65 @@
'use client';
import Link from 'next/link';
import { Edit, Pencil, Share } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { match } from 'ts-pattern';
import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
export type DataTableActionButtonProps = {
row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
};
};
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const { data: session } = useSession();
if (!session) {
return null;
}
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id;
const isRecipient = !!recipient;
const isDraft = row.status === DocumentStatus.DRAFT;
const isPending = row.status === DocumentStatus.PENDING;
const isComplete = row.status === DocumentStatus.COMPLETED;
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
return match({
isOwner,
isRecipient,
isDraft,
isPending,
isComplete,
isSigned,
})
.with({ isOwner: true, isDraft: true }, () => (
<Button className="w-24" asChild>
<Link href={`/documents/${row.id}`}>
<Edit className="-ml-1 mr-2 h-4 w-4" />
Edit
</Link>
</Button>
))
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
<Button className="w-24" asChild>
<Link href={`/sign/${recipient?.token}`}>
<Pencil className="-ml-1 mr-2 h-4 w-4" />
Sign
</Link>
</Button>
))
.otherwise(() => (
<Button className="w-24" disabled>
<Share className="-ml-1 mr-2 h-4 w-4" />
Share
</Button>
));
};

View File

@ -0,0 +1,133 @@
'use client';
import Link from 'next/link';
import {
Copy,
Download,
Edit,
History,
MoreHorizontal,
Pencil,
Share,
Trash2,
XCircle,
} from 'lucide-react';
import { useSession } from 'next-auth/react';
import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
export type DataTableActionDropdownProps = {
row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
};
};
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
const { data: session } = useSession();
if (!session) {
return null;
}
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id;
// const isRecipient = !!recipient;
// const isDraft = row.status === DocumentStatus.DRAFT;
// const isPending = row.status === DocumentStatus.PENDING;
const isComplete = row.status === DocumentStatus.COMPLETED;
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const onDownloadClick = () => {
let decodedDocument = row.document;
try {
decodedDocument = atob(decodedDocument);
} catch (err) {
// We're just going to ignore this error and try to download the document
console.error(err);
}
const documentBytes = Uint8Array.from(decodedDocument.split('').map((c) => c.charCodeAt(0)));
const blob = new Blob([documentBytes], {
type: 'application/pdf',
});
const link = window.document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = row.title || 'document.pdf';
link.click();
window.URL.revokeObjectURL(link.href);
};
return (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="h-5 w-5 text-gray-500" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>Action</DropdownMenuLabel>
<DropdownMenuItem disabled={!recipient} asChild>
<Link href={`/sign/${recipient?.token}`}>
<Pencil className="mr-2 h-4 w-4" />
Sign
</Link>
</DropdownMenuItem>
<DropdownMenuItem disabled={!isOwner} asChild>
<Link href={`/documents/${row.id}`}>
<Edit className="mr-2 h-4 w-4" />
Edit
</Link>
</DropdownMenuItem>
<DropdownMenuItem disabled={!isComplete} onClick={onDownloadClick}>
<Download className="mr-2 h-4 w-4" />
Download
</DropdownMenuItem>
<DropdownMenuItem disabled>
<Copy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem disabled>
<XCircle className="mr-2 h-4 w-4" />
Void
</DropdownMenuItem>
<DropdownMenuItem disabled>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
<DropdownMenuLabel>Share</DropdownMenuLabel>
<DropdownMenuItem disabled>
<History className="mr-2 h-4 w-4" />
Resend
</DropdownMenuItem>
<DropdownMenuItem disabled>
<Share className="mr-2 h-4 w-4" />
Share
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@ -8,7 +8,7 @@ import { Loader } from 'lucide-react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { FindResultSet } from '@documenso/lib/types/find-result-set';
import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient';
import { Document, Recipient, User } from '@documenso/prisma/client';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
@ -16,8 +16,16 @@ import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-a
import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
import { DataTableActionButton } from './data-table-action-button';
import { DataTableActionDropdown } from './data-table-action-dropdown';
export type DocumentsDataTableProps = {
results: FindResultSet<DocumentWithReciepient>;
results: FindResultSet<
Document & {
Recipient: Recipient[];
User: Pick<User, 'id' | 'name' | 'email'>;
}
>;
};
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
@ -45,7 +53,11 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
{
header: 'Title',
cell: ({ row }) => (
<Link href={`/documents/${row.original.id}`} className="font-medium hover:underline">
<Link
href={`/documents/${row.original.id}`}
title={row.original.title}
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
>
{row.original.title}
</Link>
),
@ -67,6 +79,15 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
accessorKey: 'created',
cell: ({ row }) => <LocaleDate date={row.getValue('created')} />,
},
{
header: 'Actions',
cell: ({ row }) => (
<div className="flex items-center gap-x-4">
<DataTableActionButton row={row.original} />
<DataTableActionDropdown row={row.original} />
</div>
),
},
]}
data={results.data}
perPage={results.perPage}

View File

@ -3,8 +3,8 @@ import Link from 'next/link';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import { isDocumentStatus } from '@documenso/lib/types/is-document-status';
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
@ -16,7 +16,7 @@ import { DocumentsDataTable } from './data-table';
export type DocumentsPageProps = {
searchParams?: {
status?: InternalDocumentStatus | 'ALL';
status?: ExtendedDocumentStatus;
period?: PeriodSelectorValue;
page?: string;
perPage?: string;
@ -24,22 +24,20 @@ export type DocumentsPageProps = {
};
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
const session = await getRequiredServerComponentSession();
const user = await getRequiredServerComponentSession();
const stats = await getStats({
userId: session.id,
user,
});
const status = isDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
// const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 20;
const shouldDefaultToPending = status === 'ALL' && stats.PENDING > 0;
const results = await findDocuments({
userId: session.id,
status: status === 'ALL' ? undefined : status,
userId: user.id,
status,
orderBy: {
column: 'created',
direction: 'desc',
@ -57,10 +55,6 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
params.delete('page');
}
if (value === 'ALL') {
params.delete('status');
}
return `/documents?${params.toString()}`;
};
@ -70,47 +64,28 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
<h1 className="mt-12 text-4xl font-semibold">Documents</h1>
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6 ">
<Tabs
className="overflow-x-auto"
defaultValue={shouldDefaultToPending ? InternalDocumentStatus.PENDING : status}
>
<div className="mt-8 flex flex-wrap gap-x-4 gap-y-6">
<Tabs defaultValue={status} className="overflow-x-auto">
<TabsList>
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.PENDING} asChild>
<Link href={getTabHref(InternalDocumentStatus.PENDING)} scroll={false}>
<DocumentStatus status={InternalDocumentStatus.PENDING} />
{[
ExtendedDocumentStatus.INBOX,
ExtendedDocumentStatus.PENDING,
ExtendedDocumentStatus.COMPLETED,
ExtendedDocumentStatus.DRAFT,
ExtendedDocumentStatus.ALL,
].map((value) => (
<TabsTrigger key={value} className="min-w-[60px]" value={value} asChild>
<Link href={getTabHref(value)} scroll={false}>
<DocumentStatus status={value} />
<span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats.PENDING, 99)}
</span>
</Link>
</TabsTrigger>
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.COMPLETED} asChild>
<Link href={getTabHref(InternalDocumentStatus.COMPLETED)} scroll={false}>
<DocumentStatus status={InternalDocumentStatus.COMPLETED} />
<span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats.COMPLETED, 99)}
</span>
</Link>
</TabsTrigger>
<TabsTrigger className="min-w-[60px]" value={InternalDocumentStatus.DRAFT} asChild>
<Link href={getTabHref(InternalDocumentStatus.DRAFT)} scroll={false}>
<DocumentStatus status={InternalDocumentStatus.DRAFT} />
<span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats.DRAFT, 99)}
</span>
</Link>
</TabsTrigger>
<TabsTrigger className="min-w-[60px]" value="ALL" asChild>
<Link href={getTabHref('ALL')} scroll={false}>
All
</Link>
</TabsTrigger>
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats[value], 99)}
</span>
)}
</Link>
</TabsTrigger>
))}
</TabsList>
</Tabs>

View File

@ -1,8 +1,14 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
import { SubscriptionStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { PasswordForm } from '~/components/forms/password';
import { LocaleDate } from '~/components/formatter/locale-date';
import { getServerComponentFlag } from '~/helpers/get-server-component-feature-flag';
export default async function BillingSettingsPage() {
@ -15,17 +21,55 @@ export default async function BillingSettingsPage() {
redirect('/settings/profile');
}
let subscription = await getSubscriptionByUserId({ userId: user.id });
// If we don't have a customer record, create one as well as an empty subscription.
if (!subscription?.customerId) {
subscription = await createCustomer({ user });
}
let billingPortalUrl = '';
if (subscription?.customerId) {
billingPortalUrl = await getPortalSession({
customerId: subscription.customerId,
returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`,
});
}
return (
<div>
<h3 className="text-lg font-medium">Billing</h3>
<p className="mt-2 text-sm text-slate-500">
Here you can update and manage your subscription.
Your subscription is{' '}
{subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}.
{subscription?.periodEnd && (
<>
{' '}
Your next payment is due on{' '}
<span className="font-semibold">
<LocaleDate date={subscription.periodEnd} />
</span>
.
</>
)}
</p>
<hr className="my-4" />
<PasswordForm user={user} className="max-w-xl" />
{billingPortalUrl && (
<Button asChild>
<Link href={billingPortalUrl}>Manage Subscription</Link>
</Button>
)}
{!billingPortalUrl && (
<p className="max-w-[60ch] text-base text-slate-500">
You do not currently have a customer record, this should not happen. Please contact
support for assistance.
</p>
)}
</div>
);
}

View File

@ -149,7 +149,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => {
disabled={!localFullName}
onClick={() => {
setShowFullNameModal(false);
onSign('local');
void onSign('local');
}}
>
Sign

View File

@ -182,7 +182,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
disabled={!localSignature}
onClick={() => {
setShowSignatureModal(false);
onSign('local');
void onSign('local');
}}
>
Sign