From 171a5ba4ee5a167d5fb225b29d12908c808003c9 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 09:16:31 +0300 Subject: [PATCH 01/15] feat: creating the admin ui for metrics --- .../(dashboard)/layout/profile-dropdown.tsx | 13 ++++++++++++- packages/lib/index.ts | 6 +++++- .../20230907075057_user_roles/migration.sql | 5 +++++ packages/prisma/schema.prisma | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 packages/prisma/migrations/20230907075057_user_roles/migration.sql diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 02af86d70..19a15564b 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -11,10 +11,12 @@ import { Monitor, Moon, Sun, + UserCog, } from 'lucide-react'; import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; +import { isAdmin } from '@documenso/lib/'; import { User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; @@ -35,8 +37,8 @@ export type ProfileDropdownProps = { export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { const { theme, setTheme } = useTheme(); - const { getFlag } = useFeatureFlags(); + const userIsAdmin = isAdmin(user); const isBillingEnabled = getFlag('app_billing'); @@ -67,6 +69,15 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { + {userIsAdmin && ( + + + + Admin + + + )} + diff --git a/packages/lib/index.ts b/packages/lib/index.ts index cb0ff5c3b..2801305dd 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -1 +1,5 @@ -export {}; +import { Role, User } from '@documenso/prisma/client'; + +const isAdmin = (user: User) => user.roles.includes(Role.ADMIN); + +export { isAdmin }; diff --git a/packages/prisma/migrations/20230907075057_user_roles/migration.sql b/packages/prisma/migrations/20230907075057_user_roles/migration.sql new file mode 100644 index 000000000..f47e48361 --- /dev/null +++ b/packages/prisma/migrations/20230907075057_user_roles/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER'); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "roles" "Role"[] DEFAULT ARRAY['USER']::"Role"[]; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 2e016f5ec..22955310b 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -13,6 +13,11 @@ enum IdentityProvider { GOOGLE } +enum Role { + ADMIN + USER +} + model User { id Int @id @default(autoincrement()) name String? @@ -21,6 +26,7 @@ model User { password String? source String? signature String? + roles Role[] @default([USER]) identityProvider IdentityProvider @default(DOCUMENSO) accounts Account[] sessions Session[] From 67571158e88912620e2aa33c537a91ea7da6443f Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 11:28:50 +0300 Subject: [PATCH 02/15] feat: add the admin page --- apps/web/src/app/(dashboard)/admin/layout.tsx | 23 ++++ apps/web/src/app/(dashboard)/admin/page.tsx | 107 ++++++++++++++++++ .../(dashboard)/layout/profile-dropdown.tsx | 4 +- .../lib/server-only/admin/get-documents.ts | 5 + .../lib/server-only/admin/get-recipients.ts | 20 ++++ packages/lib/server-only/admin/get-users.ts | 18 +++ 6 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/layout.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/page.tsx create mode 100644 packages/lib/server-only/admin/get-documents.ts create mode 100644 packages/lib/server-only/admin/get-recipients.ts create mode 100644 packages/lib/server-only/admin/get-users.ts diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx new file mode 100644 index 000000000..340605bc7 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -0,0 +1,23 @@ +import { redirect } from 'next/navigation'; + +import { isAdmin } from '@documenso/lib/'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; + +export type AdminLayoutProps = { + children: React.ReactNode; +}; + +export default async function AdminLayout({ children }: AdminLayoutProps) { + const user = await getRequiredServerComponentSession(); + const isUserAdmin = isAdmin(user); + + if (!user) { + redirect('/signin'); + } + + if (!isUserAdmin) { + redirect('/dashboard'); + } + + return
{children}
; +} diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx new file mode 100644 index 000000000..e4a62f725 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -0,0 +1,107 @@ +import { + Archive, + File, + FileX2, + LucideIcon, + User as LucideUser, + Mail, + MailOpen, + PenTool, + Send, + UserPlus2, + UserSquare2, +} from 'lucide-react'; + +import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents'; +import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients'; +import { + getUsersCount, + getUsersWithSubscriptionsCount, +} from '@documenso/lib/server-only/admin/get-users'; +import { + ReadStatus as InternalReadStatus, + SendStatus as InternalSendStatus, + SigningStatus as InternalSigningStatus, +} from '@documenso/prisma/client'; + +import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; + +type TCardData = { + icon: LucideIcon; + title: string; + status: + | 'TOTAL_RECIPIENTS' + | 'OPENED' + | 'NOT_OPENED' + | 'SIGNED' + | 'NOT_SIGNED' + | 'SENT' + | 'NOT_SENT'; +}[]; + +const CARD_DATA: TCardData = [ + { + icon: UserSquare2, + title: 'Total recipients in the database', + status: 'TOTAL_RECIPIENTS', + }, + { + icon: MailOpen, + title: 'Total recipients with opened count', + status: InternalReadStatus.OPENED, + }, + { + icon: Mail, + title: 'Total recipients with unopened count', + status: InternalReadStatus.NOT_OPENED, + }, + { + icon: Send, + title: 'Total recipients with sent count', + status: InternalSendStatus.SENT, + }, + { + icon: Archive, + title: 'Total recipients with unsent count', + status: InternalSendStatus.NOT_SENT, + }, + { + icon: PenTool, + title: 'Total recipients with signed count', + status: InternalSigningStatus.SIGNED, + }, + { + icon: FileX2, + title: 'Total recipients with unsigned count', + status: InternalSigningStatus.NOT_SIGNED, + }, +]; + +export default async function Admin() { + const [usersCount, usersWithSubscriptionsCount, docsCount, recipientsStats] = await Promise.all([ + getUsersCount(), + getUsersWithSubscriptionsCount(), + getDocsCount(), + getRecipientsStats(), + ]); + + return ( +
+

Documenso instance metrics

+
+ + + + {CARD_DATA.map((card) => ( +
+ +
+ ))} +
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 19a15564b..0bea64565 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -38,7 +38,7 @@ export type ProfileDropdownProps = { export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { const { theme, setTheme } = useTheme(); const { getFlag } = useFeatureFlags(); - const userIsAdmin = isAdmin(user); + const isUserAdmin = isAdmin(user); const isBillingEnabled = getFlag('app_billing'); @@ -69,7 +69,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
- {userIsAdmin && ( + {isUserAdmin && ( diff --git a/packages/lib/server-only/admin/get-documents.ts b/packages/lib/server-only/admin/get-documents.ts new file mode 100644 index 000000000..9100a886c --- /dev/null +++ b/packages/lib/server-only/admin/get-documents.ts @@ -0,0 +1,5 @@ +import { prisma } from '@documenso/prisma'; + +export const getDocsCount = async () => { + return await prisma.document.count(); +}; diff --git a/packages/lib/server-only/admin/get-recipients.ts b/packages/lib/server-only/admin/get-recipients.ts new file mode 100644 index 000000000..0be612e55 --- /dev/null +++ b/packages/lib/server-only/admin/get-recipients.ts @@ -0,0 +1,20 @@ +import { prisma } from '@documenso/prisma'; +import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client'; + +export const getRecipientsStats = async () => { + const results = await prisma.recipient.groupBy({ + by: ['readStatus', 'signingStatus', 'sendStatus'], + _count: true, + }); + + return { + TOTAL_RECIPIENTS: results.length, + [ReadStatus.OPENED]: results.filter((r) => r.readStatus === 'OPENED')?.[0]?._count ?? 0, + [ReadStatus.NOT_OPENED]: results.filter((r) => r.readStatus === 'NOT_OPENED')?.[0]?._count ?? 0, + [SigningStatus.SIGNED]: results.filter((r) => r.signingStatus === 'SIGNED')?.[0]?._count ?? 0, + [SigningStatus.NOT_SIGNED]: + results.filter((r) => r.signingStatus === 'NOT_SIGNED')?.[0]?._count ?? 0, + [SendStatus.SENT]: results.filter((r) => r.sendStatus === 'SENT')?.[0]?._count ?? 0, + [SendStatus.NOT_SENT]: results.filter((r) => r.sendStatus === 'NOT_SENT')?.[0]?._count ?? 0, + }; +}; diff --git a/packages/lib/server-only/admin/get-users.ts b/packages/lib/server-only/admin/get-users.ts new file mode 100644 index 000000000..09892171a --- /dev/null +++ b/packages/lib/server-only/admin/get-users.ts @@ -0,0 +1,18 @@ +import { prisma } from '@documenso/prisma'; +import { SubscriptionStatus } from '@documenso/prisma/client'; + +export const getUsersCount = async () => { + return await prisma.user.count(); +}; + +export const getUsersWithSubscriptionsCount = async () => { + return await prisma.user.count({ + where: { + Subscription: { + some: { + status: SubscriptionStatus.ACTIVE, + }, + }, + }, + }); +}; From 6cdba45396299cc1e06a7d185280acaddda0fb59 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 12:39:13 +0300 Subject: [PATCH 03/15] chore: implemented feedback --- apps/web/src/app/(dashboard)/admin/page.tsx | 4 +-- .../lib/server-only/admin/get-recipients.ts | 25 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index e4a62f725..e72d35dc3 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -37,9 +37,9 @@ type TCardData = { | 'NOT_SIGNED' | 'SENT' | 'NOT_SENT'; -}[]; +}; -const CARD_DATA: TCardData = [ +const CARD_DATA: TCardData[] = [ { icon: UserSquare2, title: 'Total recipients in the database', diff --git a/packages/lib/server-only/admin/get-recipients.ts b/packages/lib/server-only/admin/get-recipients.ts index 0be612e55..92c0c3527 100644 --- a/packages/lib/server-only/admin/get-recipients.ts +++ b/packages/lib/server-only/admin/get-recipients.ts @@ -7,14 +7,21 @@ export const getRecipientsStats = async () => { _count: true, }); - return { - TOTAL_RECIPIENTS: results.length, - [ReadStatus.OPENED]: results.filter((r) => r.readStatus === 'OPENED')?.[0]?._count ?? 0, - [ReadStatus.NOT_OPENED]: results.filter((r) => r.readStatus === 'NOT_OPENED')?.[0]?._count ?? 0, - [SigningStatus.SIGNED]: results.filter((r) => r.signingStatus === 'SIGNED')?.[0]?._count ?? 0, - [SigningStatus.NOT_SIGNED]: - results.filter((r) => r.signingStatus === 'NOT_SIGNED')?.[0]?._count ?? 0, - [SendStatus.SENT]: results.filter((r) => r.sendStatus === 'SENT')?.[0]?._count ?? 0, - [SendStatus.NOT_SENT]: results.filter((r) => r.sendStatus === 'NOT_SENT')?.[0]?._count ?? 0, + const stats = { + TOTAL_RECIPIENTS: 0, + [ReadStatus.OPENED]: 0, + [ReadStatus.NOT_OPENED]: 0, + [SigningStatus.SIGNED]: 0, + [SigningStatus.NOT_SIGNED]: 0, + [SendStatus.SENT]: 0, + [SendStatus.NOT_SENT]: 0, }; + results.forEach((result) => { + const { readStatus, signingStatus, sendStatus, _count } = result; + stats[readStatus] += _count; + stats[signingStatus] += _count; + stats[sendStatus] += _count; + stats.TOTAL_RECIPIENTS += _count; + }); + return stats; }; From 77058220a8975f01d4b9fda48c29bc6089a3bef0 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 12:42:14 +0300 Subject: [PATCH 04/15] chore: rename files --- apps/web/src/app/(dashboard)/admin/page.tsx | 6 +++--- .../admin/{get-documents.ts => get-documents-stats.ts} | 0 .../admin/{get-recipients.ts => get-recipients-stats.ts} | 0 .../server-only/admin/{get-users.ts => get-users-stats.ts} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/lib/server-only/admin/{get-documents.ts => get-documents-stats.ts} (100%) rename packages/lib/server-only/admin/{get-recipients.ts => get-recipients-stats.ts} (100%) rename packages/lib/server-only/admin/{get-users.ts => get-users-stats.ts} (100%) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index e72d35dc3..aabdbfa35 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -12,12 +12,12 @@ import { UserSquare2, } from 'lucide-react'; -import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents'; -import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients'; +import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents-stats'; +import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; import { getUsersCount, getUsersWithSubscriptionsCount, -} from '@documenso/lib/server-only/admin/get-users'; +} from '@documenso/lib/server-only/admin/get-users-stats'; import { ReadStatus as InternalReadStatus, SendStatus as InternalSendStatus, diff --git a/packages/lib/server-only/admin/get-documents.ts b/packages/lib/server-only/admin/get-documents-stats.ts similarity index 100% rename from packages/lib/server-only/admin/get-documents.ts rename to packages/lib/server-only/admin/get-documents-stats.ts diff --git a/packages/lib/server-only/admin/get-recipients.ts b/packages/lib/server-only/admin/get-recipients-stats.ts similarity index 100% rename from packages/lib/server-only/admin/get-recipients.ts rename to packages/lib/server-only/admin/get-recipients-stats.ts diff --git a/packages/lib/server-only/admin/get-users.ts b/packages/lib/server-only/admin/get-users-stats.ts similarity index 100% rename from packages/lib/server-only/admin/get-users.ts rename to packages/lib/server-only/admin/get-users-stats.ts From 660f5894a6f66baa5fd9394efec21306e640dc7b Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 12:56:44 +0300 Subject: [PATCH 05/15] chore: feedback improvements --- apps/web/src/app/(dashboard)/admin/layout.tsx | 2 +- .../src/components/(dashboard)/layout/profile-dropdown.tsx | 2 +- packages/lib/index.ts | 6 +----- packages/lib/next-auth/guards/is-admin.ts | 5 +++++ 4 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 packages/lib/next-auth/guards/is-admin.ts diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index 340605bc7..a221d92ba 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation'; -import { isAdmin } from '@documenso/lib/'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; export type AdminLayoutProps = { children: React.ReactNode; diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 0bea64565..e3fd4c6d6 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -16,7 +16,7 @@ import { import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; -import { isAdmin } from '@documenso/lib/'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; import { User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; diff --git a/packages/lib/index.ts b/packages/lib/index.ts index 2801305dd..cb0ff5c3b 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -1,5 +1 @@ -import { Role, User } from '@documenso/prisma/client'; - -const isAdmin = (user: User) => user.roles.includes(Role.ADMIN); - -export { isAdmin }; +export {}; diff --git a/packages/lib/next-auth/guards/is-admin.ts b/packages/lib/next-auth/guards/is-admin.ts new file mode 100644 index 000000000..2801305dd --- /dev/null +++ b/packages/lib/next-auth/guards/is-admin.ts @@ -0,0 +1,5 @@ +import { Role, User } from '@documenso/prisma/client'; + +const isAdmin = (user: User) => user.roles.includes(Role.ADMIN); + +export { isAdmin }; From 5969f148c861bbc1d3e05a68cf359542bdff481c Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Fri, 8 Sep 2023 14:51:55 +0300 Subject: [PATCH 06/15] chore: changed the cards titles --- apps/web/src/app/(dashboard)/admin/page.tsx | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index aabdbfa35..fdb54dc07 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -42,37 +42,37 @@ type TCardData = { const CARD_DATA: TCardData[] = [ { icon: UserSquare2, - title: 'Total recipients in the database', + title: 'Recipients in the database', status: 'TOTAL_RECIPIENTS', }, { icon: MailOpen, - title: 'Total recipients with opened count', + title: 'Opened documents', status: InternalReadStatus.OPENED, }, { icon: Mail, - title: 'Total recipients with unopened count', + title: 'Unopened documents', status: InternalReadStatus.NOT_OPENED, }, { icon: Send, - title: 'Total recipients with sent count', + title: 'Sent documents', status: InternalSendStatus.SENT, }, { icon: Archive, - title: 'Total recipients with unsent count', + title: 'Unsent documents', status: InternalSendStatus.NOT_SENT, }, { icon: PenTool, - title: 'Total recipients with signed count', + title: 'Signed documents', status: InternalSigningStatus.SIGNED, }, { icon: FileX2, - title: 'Total recipients with unsigned count', + title: 'Unsigned documents', status: InternalSigningStatus.NOT_SIGNED, }, ]; @@ -87,15 +87,22 @@ export default async function Admin() { return (
-

Documenso instance metrics

-
+

Instance metrics

+
+
+

Document metrics

+
+
+ +

Recipients metrics

+
{CARD_DATA.map((card) => (
From fbf32404a6b859355e5a189d98756fc9da2a466f Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 11 Sep 2023 16:58:41 +1000 Subject: [PATCH 07/15] feat: add avatar email fallback --- .../src/components/(dashboard)/avatar/stack-avatar.tsx | 4 ++-- .../(dashboard)/avatar/stack-avatars-with-tooltip.tsx | 10 +++++----- .../components/(dashboard)/avatar/stack-avatars.tsx | 4 ++-- .../components/(dashboard)/layout/profile-dropdown.tsx | 10 +++------- packages/lib/client-only/recipient-avatar-fallback.ts | 7 +++++++ packages/lib/client-only/recipient-initials.ts | 2 +- 6 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 packages/lib/client-only/recipient-avatar-fallback.ts diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx index 78814cd45..a2a81bb2a 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx @@ -15,7 +15,7 @@ export type StackAvatarProps = { type: 'unsigned' | 'waiting' | 'opened' | 'completed'; }; -export const StackAvatar = ({ first, zIndex, fallbackText, type }: StackAvatarProps) => { +export const StackAvatar = ({ first, zIndex, fallbackText = '', type }: StackAvatarProps) => { let classes = ''; let zIndexClass = ''; const firstClass = first ? '' : '-ml-3'; @@ -48,7 +48,7 @@ export const StackAvatar = ({ first, zIndex, fallbackText, type }: StackAvatarPr ${firstClass} dark:border-border h-10 w-10 border-2 border-solid border-white`} > - {fallbackText ?? 'UK'} + {fallbackText} ); }; diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx index 2a053a35a..3f6407029 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx @@ -1,4 +1,4 @@ -import { initials } from '@documenso/lib/client-only/recipient-initials'; +import { recipientAvatarFallback } from '@documenso/lib/client-only/recipient-avatar-fallback'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { Recipient } from '@documenso/prisma/client'; import { @@ -56,7 +56,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={initials(recipient.name)} + fallbackText={recipientAvatarFallback(recipient)} /> {recipient.email}
@@ -73,7 +73,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={initials(recipient.name)} + fallbackText={recipientAvatarFallback(recipient)} /> {recipient.email}
@@ -90,7 +90,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={initials(recipient.name)} + fallbackText={recipientAvatarFallback(recipient)} /> {recipient.email}
@@ -107,7 +107,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={initials(recipient.name)} + fallbackText={recipientAvatarFallback(recipient)} /> {recipient.email}
diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx index 97af9dc9e..678836ffd 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { initials } from '@documenso/lib/client-only/recipient-initials'; +import { recipientAvatarFallback } from '@documenso/lib/client-only/recipient-avatar-fallback'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; import { Recipient } from '@documenso/prisma/client'; @@ -26,7 +26,7 @@ export function StackAvatars({ recipients }: { recipients: Recipient[] }) { first={first} zIndex={String(zIndex - index * 10)} type={lastItemText && index === 4 ? 'unsigned' : getRecipientType(recipient)} - fallbackText={lastItemText ? lastItemText : initials(recipient.name)} + fallbackText={lastItemText ? lastItemText : recipientAvatarFallback(recipient)} /> ); }); diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index 02af86d70..e52d9b42f 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -15,6 +15,7 @@ import { import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; +import { initials } from '@documenso/lib/client-only/recipient-initials'; import { User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; @@ -40,19 +41,14 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { const isBillingEnabled = getFlag('app_billing'); - const initials = - user.name - ?.split(' ') - .map((name: string) => name.slice(0, 1).toUpperCase()) - .slice(0, 2) - .join('') ?? 'UK'; + const avatarFallback = user.name ? initials(user.name) : user.email.slice(0, 1).toUpperCase(); return ( diff --git a/packages/lib/client-only/recipient-avatar-fallback.ts b/packages/lib/client-only/recipient-avatar-fallback.ts new file mode 100644 index 000000000..7a296a5fa --- /dev/null +++ b/packages/lib/client-only/recipient-avatar-fallback.ts @@ -0,0 +1,7 @@ +import { Recipient } from '@documenso/prisma/client'; + +import { initials } from './recipient-initials'; + +export const recipientAvatarFallback = (recipient: Recipient) => { + return initials(recipient.name) || recipient.email.slice(0, 1).toUpperCase(); +}; diff --git a/packages/lib/client-only/recipient-initials.ts b/packages/lib/client-only/recipient-initials.ts index 0712ccd7d..403ed26e4 100644 --- a/packages/lib/client-only/recipient-initials.ts +++ b/packages/lib/client-only/recipient-initials.ts @@ -3,4 +3,4 @@ export const initials = (text: string) => ?.split(' ') .map((name: string) => name.slice(0, 1).toUpperCase()) .slice(0, 2) - .join('') ?? 'UK'; + .join(''); From 326743d8a1f3a2f363f5df4ebe30947cea2a476b Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Mon, 11 Sep 2023 10:59:50 +0300 Subject: [PATCH 08/15] chore: added app version --- apps/web/next.config.js | 4 +++- apps/web/src/app/(dashboard)/admin/page.tsx | 2 +- turbo.json | 16 +++++----------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 09760f806..1e98b98fc 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -18,7 +18,9 @@ const config = { '@documenso/ui', '@documenso/email', ], - env, + env: { + APP_VERSION: process.env.npm_package_version, + }, modularizeImports: { 'lucide-react': { transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index fdb54dc07..78358c95a 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -87,7 +87,7 @@ export default async function Admin() { return (
-

Instance metrics

+

Instance version: {process.env.APP_VERSION}

Date: Mon, 11 Sep 2023 11:34:10 +0300 Subject: [PATCH 09/15] chore: fix version in nextjs config --- apps/web/next.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 1e98b98fc..fa6c0d1ac 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const path = require('path'); +const { version } = require('./package.json'); const { parsed: env } = require('dotenv').config({ path: path.join(__dirname, '../../.env.local'), @@ -19,7 +20,8 @@ const config = { '@documenso/email', ], env: { - APP_VERSION: process.env.npm_package_version, + ...env, + APP_VERSION: version, }, modularizeImports: { 'lucide-react': { From 00574325b90e11d64f552cbd6b809acc98897ea2 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Mon, 11 Sep 2023 13:43:17 +0300 Subject: [PATCH 10/15] chore: implemented feedback --- apps/web/src/app/(dashboard)/admin/page.tsx | 8 ++++---- turbo.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index 78358c95a..073056478 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -3,11 +3,11 @@ import { File, FileX2, LucideIcon, - User as LucideUser, Mail, MailOpen, PenTool, Send, + User as UserIcon, UserPlus2, UserSquare2, } from 'lucide-react'; @@ -26,7 +26,7 @@ import { import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; -type TCardData = { +type CardData = { icon: LucideIcon; title: string; status: @@ -39,7 +39,7 @@ type TCardData = { | 'NOT_SENT'; }; -const CARD_DATA: TCardData[] = [ +const CARD_DATA: CardData[] = [ { icon: UserSquare2, title: 'Recipients in the database', @@ -89,7 +89,7 @@ export default async function Admin() {

Instance version: {process.env.APP_VERSION}

- + Date: Tue, 12 Sep 2023 10:37:47 +1000 Subject: [PATCH 11/15] fix: data table links for recipients --- .../documents/data-table-title.tsx | 56 +++++++++++++++++++ .../app/(dashboard)/documents/data-table.tsx | 19 +++---- 2 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/documents/data-table-title.tsx diff --git a/apps/web/src/app/(dashboard)/documents/data-table-title.tsx b/apps/web/src/app/(dashboard)/documents/data-table-title.tsx new file mode 100644 index 000000000..c04f9f13d --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/data-table-title.tsx @@ -0,0 +1,56 @@ +'use client'; + +import Link from 'next/link'; + +import { useSession } from 'next-auth/react'; +import { match } from 'ts-pattern'; + +import { Document, Recipient, User } from '@documenso/prisma/client'; + +export type DataTableTitleProps = { + row: Document & { + User: Pick; + Recipient: Recipient[]; + }; +}; + +export const DataTableTitle = ({ row }: DataTableTitleProps) => { + 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; + + return match({ + isOwner, + isRecipient, + }) + .with({ isOwner: true }, () => ( + + {row.title} + + )) + .with({ isRecipient: true }, () => ( + + {row.title} + + )) + .otherwise(() => ( + + {row.title} + + )); +}; diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index 1d6c08e73..245734a8e 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -2,9 +2,8 @@ import { useTransition } from 'react'; -import Link from 'next/link'; - import { Loader } from 'lucide-react'; +import { useSession } from 'next-auth/react'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { FindResultSet } from '@documenso/lib/types/find-result-set'; @@ -18,6 +17,7 @@ import { LocaleDate } from '~/components/formatter/locale-date'; import { DataTableActionButton } from './data-table-action-button'; import { DataTableActionDropdown } from './data-table-action-dropdown'; +import { DataTableTitle } from './data-table-title'; export type DocumentsDataTableProps = { results: FindResultSet< @@ -29,6 +29,7 @@ export type DocumentsDataTableProps = { }; export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { + const { data: session } = useSession(); const [isPending, startTransition] = useTransition(); const updateSearchParams = useUpdateSearchParams(); @@ -42,6 +43,10 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { }); }; + if (!session) { + return null; + } + return (
{ }, { header: 'Title', - cell: ({ row }) => ( - - {row.original.title} - - ), + cell: ({ row }) => , }, { header: 'Recipient', From e8796a7d86cdf9be56da6fc8b88cb12360b506ab Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 12 Sep 2023 12:33:04 +1000 Subject: [PATCH 12/15] refactor: organise recipient utils --- .../avatar/stack-avatars-with-tooltip.tsx | 10 +++++----- .../components/(dashboard)/avatar/stack-avatars.tsx | 4 ++-- .../(dashboard)/layout/profile-dropdown.tsx | 6 ++++-- .../lib/client-only/recipient-avatar-fallback.ts | 7 ------- packages/lib/client-only/recipient-initials.ts | 6 ------ packages/lib/utils/recipient-formatter.ts | 12 ++++++++++++ 6 files changed, 23 insertions(+), 22 deletions(-) delete mode 100644 packages/lib/client-only/recipient-avatar-fallback.ts delete mode 100644 packages/lib/client-only/recipient-initials.ts create mode 100644 packages/lib/utils/recipient-formatter.ts diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx index 3f6407029..e36415813 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx @@ -1,5 +1,5 @@ -import { recipientAvatarFallback } from '@documenso/lib/client-only/recipient-avatar-fallback'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; +import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import { Recipient } from '@documenso/prisma/client'; import { Tooltip, @@ -56,7 +56,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={recipientAvatarFallback(recipient)} + fallbackText={recipientAbbreviation(recipient)} /> {recipient.email}
@@ -73,7 +73,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={recipientAvatarFallback(recipient)} + fallbackText={recipientAbbreviation(recipient)} /> {recipient.email}
@@ -90,7 +90,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={recipientAvatarFallback(recipient)} + fallbackText={recipientAbbreviation(recipient)} /> {recipient.email}
@@ -107,7 +107,7 @@ export const StackAvatarsWithTooltip = ({ first={true} key={recipient.id} type={getRecipientType(recipient)} - fallbackText={recipientAvatarFallback(recipient)} + fallbackText={recipientAbbreviation(recipient)} /> {recipient.email}
diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx index 678836ffd..91f470f74 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { recipientAvatarFallback } from '@documenso/lib/client-only/recipient-avatar-fallback'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; +import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import { Recipient } from '@documenso/prisma/client'; import { StackAvatar } from './stack-avatar'; @@ -26,7 +26,7 @@ export function StackAvatars({ recipients }: { recipients: Recipient[] }) { first={first} zIndex={String(zIndex - index * 10)} type={lastItemText && index === 4 ? 'unsigned' : getRecipientType(recipient)} - fallbackText={lastItemText ? lastItemText : recipientAvatarFallback(recipient)} + fallbackText={lastItemText ? lastItemText : recipientAbbreviation(recipient)} /> ); }); diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index e52d9b42f..3b361e885 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -15,7 +15,7 @@ import { import { signOut } from 'next-auth/react'; import { useTheme } from 'next-themes'; -import { initials } from '@documenso/lib/client-only/recipient-initials'; +import { recipientInitials } from '@documenso/lib/utils/recipient-formatter'; import { User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; @@ -41,7 +41,9 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { const isBillingEnabled = getFlag('app_billing'); - const avatarFallback = user.name ? initials(user.name) : user.email.slice(0, 1).toUpperCase(); + const avatarFallback = user.name + ? recipientInitials(user.name) + : user.email.slice(0, 1).toUpperCase(); return ( diff --git a/packages/lib/client-only/recipient-avatar-fallback.ts b/packages/lib/client-only/recipient-avatar-fallback.ts deleted file mode 100644 index 7a296a5fa..000000000 --- a/packages/lib/client-only/recipient-avatar-fallback.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Recipient } from '@documenso/prisma/client'; - -import { initials } from './recipient-initials'; - -export const recipientAvatarFallback = (recipient: Recipient) => { - return initials(recipient.name) || recipient.email.slice(0, 1).toUpperCase(); -}; diff --git a/packages/lib/client-only/recipient-initials.ts b/packages/lib/client-only/recipient-initials.ts deleted file mode 100644 index 403ed26e4..000000000 --- a/packages/lib/client-only/recipient-initials.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const initials = (text: string) => - text - ?.split(' ') - .map((name: string) => name.slice(0, 1).toUpperCase()) - .slice(0, 2) - .join(''); diff --git a/packages/lib/utils/recipient-formatter.ts b/packages/lib/utils/recipient-formatter.ts new file mode 100644 index 000000000..da404830b --- /dev/null +++ b/packages/lib/utils/recipient-formatter.ts @@ -0,0 +1,12 @@ +import { Recipient } from '@documenso/prisma/client'; + +export const recipientInitials = (text: string) => + text + .split(' ') + .map((name: string) => name.slice(0, 1).toUpperCase()) + .slice(0, 2) + .join(''); + +export const recipientAbbreviation = (recipient: Recipient) => { + return recipientInitials(recipient.name) || recipient.email.slice(0, 1).toUpperCase(); +}; From 24a2e9e6d4fb254a03a6f300996b3518678e68ee Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 12 Sep 2023 14:29:27 +1000 Subject: [PATCH 13/15] feat: update document table layout (#371) * feat: update document table layout - Removed dashboard page - Removed redundant ID column - Moved date to first column - Added estimated locales for SSR dates --- .../src/app/(dashboard)/dashboard/page.tsx | 124 ------------------ .../documents/[id]/edit-document.tsx | 2 +- .../app/(dashboard)/documents/data-table.tsx | 12 +- .../src/app/(dashboard)/documents/page.tsx | 3 +- .../upload-document.tsx | 0 apps/web/src/app/layout.tsx | 26 ++-- .../src/components/formatter/locale-date.tsx | 23 +++- apps/web/src/components/forms/signin.tsx | 16 +-- packages/lib/client-only/providers/locale.tsx | 37 ++++++ .../ui/primitives/data-table-pagination.tsx | 33 ++++- 10 files changed, 117 insertions(+), 159 deletions(-) delete mode 100644 apps/web/src/app/(dashboard)/dashboard/page.tsx rename apps/web/src/app/(dashboard)/{dashboard => documents}/upload-document.tsx (100%) create mode 100644 packages/lib/client-only/providers/locale.tsx diff --git a/apps/web/src/app/(dashboard)/dashboard/page.tsx b/apps/web/src/app/(dashboard)/dashboard/page.tsx deleted file mode 100644 index 77b18b98c..000000000 --- a/apps/web/src/app/(dashboard)/dashboard/page.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import Link from 'next/link'; - -import { Clock, File, FileCheck } from 'lucide-react'; - -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 { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@documenso/ui/primitives/table'; - -import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; -import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; -import { DocumentStatus } from '~/components/formatter/document-status'; -import { LocaleDate } from '~/components/formatter/locale-date'; - -import { UploadDocument } from './upload-document'; - -const CARD_DATA = [ - { - icon: FileCheck, - title: 'Completed', - status: InternalDocumentStatus.COMPLETED, - }, - { - icon: File, - title: 'Drafts', - status: InternalDocumentStatus.DRAFT, - }, - { - icon: Clock, - title: 'Pending', - status: InternalDocumentStatus.PENDING, - }, -]; - -export default async function DashboardPage() { - const user = await getRequiredServerComponentSession(); - - const [stats, results] = await Promise.all([ - getStats({ - user, - }), - findDocuments({ - userId: user.id, - perPage: 10, - }), - ]); - - return ( -
-

Dashboard

- -
- {CARD_DATA.map((card) => ( - - - - ))} -
- -
- - -

Recent Documents

- -
- - - - ID - Title - Reciepient - Status - Created - - - - {results.data.map((document) => { - return ( - - {document.id} - - - {document.title} - - - - - - - - - - - - - - - ); - })} - {results.data.length === 0 && ( - - - No results. - - - )} - -
-
-
-
- ); -} diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index 7ed28feca..ba134ac58 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -136,7 +136,7 @@ export const EditDocumentForm = ({ duration: 5000, }); - router.push('/dashboard'); + router.push('/documents'); } catch (err) { console.error(err); diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index 245734a8e..b8c735b59 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -52,8 +52,9 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { , }, { header: 'Title', @@ -71,11 +72,6 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { accessorKey: 'status', cell: ({ row }) => , }, - { - header: 'Created', - accessorKey: 'created', - cell: ({ row }) => , - }, { header: 'Actions', cell: ({ row }) => ( @@ -92,7 +88,7 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { totalPages={results.totalPages} onPaginationChange={onPaginationChange} > - {(table) => } + {(table) => } {isPending && ( diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx index 4ea55936b..d1f558806 100644 --- a/apps/web/src/app/(dashboard)/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/page.tsx @@ -11,8 +11,8 @@ import { PeriodSelector } from '~/components/(dashboard)/period-selector/period- import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types'; import { DocumentStatus } from '~/components/formatter/document-status'; -import { UploadDocument } from '../dashboard/upload-document'; import { DocumentsDataTable } from './data-table'; +import { UploadDocument } from './upload-document'; export type DocumentsPageProps = { searchParams?: { @@ -81,6 +81,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage {value !== ExtendedDocumentStatus.ALL && ( {Math.min(stats[value], 99)} + {stats[value] > 99 && '+'} )} diff --git a/apps/web/src/app/(dashboard)/dashboard/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx similarity index 100% rename from apps/web/src/app/(dashboard)/dashboard/upload-document.tsx rename to apps/web/src/app/(dashboard)/documents/upload-document.tsx diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 1d1e056ae..2ce8744d4 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -2,6 +2,8 @@ import { Suspense } from 'react'; import { Caveat, Inter } from 'next/font/google'; +import { LocaleProvider } from '@documenso/lib/client-only/providers/locale'; +import { getLocale } from '@documenso/lib/server-only/headers/get-locale'; import { TrpcProvider } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Toaster } from '@documenso/ui/primitives/toaster'; @@ -45,6 +47,8 @@ export const metadata = { export default async function RootLayout({ children }: { children: React.ReactNode }) { const flags = await getServerComponentAllFlags(); + const locale = getLocale(); + return ( - - - - - {children} - - - - - + + + + + + {children} + + + + + + ); diff --git a/apps/web/src/components/formatter/locale-date.tsx b/apps/web/src/components/formatter/locale-date.tsx index 837c6aa38..ecefb1e3b 100644 --- a/apps/web/src/components/formatter/locale-date.tsx +++ b/apps/web/src/components/formatter/locale-date.tsx @@ -2,16 +2,31 @@ import { HTMLAttributes, useEffect, useState } from 'react'; +import { DateTime, DateTimeFormatOptions } from 'luxon'; + +import { useLocale } from '@documenso/lib/client-only/providers/locale'; + export type LocaleDateProps = HTMLAttributes & { date: string | number | Date; + format?: DateTimeFormatOptions; }; -export const LocaleDate = ({ className, date, ...props }: LocaleDateProps) => { - const [localeDate, setLocaleDate] = useState(() => new Date(date).toISOString()); +/** + * Formats the date based on the user locale. + * + * Will use the estimated locale from the user headers on SSR, then will use + * the client browser locale once mounted. + */ +export const LocaleDate = ({ className, date, format, ...props }: LocaleDateProps) => { + const { locale } = useLocale(); + + const [localeDate, setLocaleDate] = useState(() => + DateTime.fromJSDate(new Date(date)).setLocale(locale).toLocaleString(format), + ); useEffect(() => { - setLocaleDate(new Date(date).toLocaleString()); - }, [date]); + setLocaleDate(DateTime.fromJSDate(new Date(date)).toLocaleString(format)); + }, [date, format]); return ( diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index 5e44146ea..d9d727afc 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -18,13 +18,15 @@ import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; import { useToast } from '@documenso/ui/primitives/use-toast'; -const ErrorMessages = { +const ERROR_MESSAGES = { [ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect', [ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'The email or password provided is incorrect', [ErrorCode.USER_MISSING_PASSWORD]: 'This account appears to be using a social login method, please sign in using that method', }; +const LOGIN_REDIRECT_PATH = '/documents'; + export const ZSignInFormSchema = z.object({ email: z.string().email().min(1), password: z.string().min(6).max(72), @@ -37,9 +39,10 @@ export type SignInFormProps = { }; export const SignInForm = ({ className }: SignInFormProps) => { - const { toast } = useToast(); const searchParams = useSearchParams(); + const { toast } = useToast(); + const { register, handleSubmit, @@ -61,7 +64,7 @@ export const SignInForm = ({ className }: SignInFormProps) => { timeout = setTimeout(() => { toast({ variant: 'destructive', - description: ErrorMessages[errorCode] ?? 'An unknown error occurred', + description: ERROR_MESSAGES[errorCode] ?? 'An unknown error occurred', }); }, 0); } @@ -78,12 +81,10 @@ export const SignInForm = ({ className }: SignInFormProps) => { await signIn('credentials', { email, password, - callbackUrl: '/documents', + callbackUrl: LOGIN_REDIRECT_PATH, }).catch((err) => { console.error(err); }); - - // throw new Error('Not implemented'); } catch (err) { toast({ title: 'An unknown error occurred', @@ -95,8 +96,7 @@ export const SignInForm = ({ className }: SignInFormProps) => { const onSignInWithGoogleClick = async () => { try { - await signIn('google', { callbackUrl: '/dashboard' }); - // throw new Error('Not implemented'); + await signIn('google', { callbackUrl: LOGIN_REDIRECT_PATH }); } catch (err) { toast({ title: 'An unknown error occurred', diff --git a/packages/lib/client-only/providers/locale.tsx b/packages/lib/client-only/providers/locale.tsx new file mode 100644 index 000000000..ff8b03e5a --- /dev/null +++ b/packages/lib/client-only/providers/locale.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { createContext, useContext } from 'react'; + +export type LocaleContextValue = { + locale: string; +}; + +export const LocaleContext = createContext(null); + +export const useLocale = () => { + const context = useContext(LocaleContext); + + if (!context) { + throw new Error('useLocale must be used within a LocaleProvider'); + } + + return context; +}; + +export function LocaleProvider({ + children, + locale, +}: { + children: React.ReactNode; + locale: string; +}) { + return ( + + {children} + + ); +} diff --git a/packages/ui/primitives/data-table-pagination.tsx b/packages/ui/primitives/data-table-pagination.tsx index 0ff27ae11..8147c92fb 100644 --- a/packages/ui/primitives/data-table-pagination.tsx +++ b/packages/ui/primitives/data-table-pagination.tsx @@ -1,19 +1,46 @@ import { Table } from '@tanstack/react-table'; import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'; +import { match } from 'ts-pattern'; import { Button } from './button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; interface DataTablePaginationProps { table: Table; + + /** + * The type of information to show on the left hand side of the pagination. + * + * Defaults to 'VisibleCount'. + */ + additionalInformation?: 'SelectedCount' | 'VisibleCount' | 'None'; } -export function DataTablePagination({ table }: DataTablePaginationProps) { +export function DataTablePagination({ + table, + additionalInformation = 'VisibleCount', +}: DataTablePaginationProps) { return (
- {table.getFilteredSelectedRowModel().rows.length} of{' '} - {table.getFilteredRowModel().rows.length} row(s) selected. + {match(additionalInformation) + .with('SelectedCount', () => ( + + {table.getFilteredSelectedRowModel().rows.length} of{' '} + {table.getFilteredRowModel().rows.length} row(s) selected. + + )) + .with('VisibleCount', () => { + const visibleRows = table.getFilteredRowModel().rows.length; + + return ( + + Showing {visibleRows} result{visibleRows > 1 && 's'}. + + ); + }) + .with('None', () => null) + .exhaustive()}
From 581f08c59bf1d39bffb499f746b7942b7a9674c3 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Tue, 12 Sep 2023 07:25:44 +0000 Subject: [PATCH 14/15] fix: update layout and wording --- apps/web/src/app/(dashboard)/admin/layout.tsx | 28 ++--- apps/web/src/app/(dashboard)/admin/nav.tsx | 47 +++++++ apps/web/src/app/(dashboard)/admin/page.tsx | 115 +----------------- .../src/app/(dashboard)/admin/stats/page.tsx | 75 ++++++++++++ .../(dashboard)/layout/profile-dropdown.tsx | 22 ++-- .../(dashboard)/metric-card/metric-card.tsx | 6 +- .../server-only/admin/get-documents-stats.ts | 25 +++- 7 files changed, 176 insertions(+), 142 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/nav.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/stats/page.tsx diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index a221d92ba..a04c7b693 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -1,23 +1,19 @@ -import { redirect } from 'next/navigation'; +import React from 'react'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; -import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; +import { AdminNav } from './nav'; -export type AdminLayoutProps = { +export type AdminSectionLayoutProps = { children: React.ReactNode; }; -export default async function AdminLayout({ children }: AdminLayoutProps) { - const user = await getRequiredServerComponentSession(); - const isUserAdmin = isAdmin(user); +export default function AdminSectionLayout({ children }: AdminSectionLayoutProps) { + return ( +
+
+ - if (!user) { - redirect('/signin'); - } - - if (!isUserAdmin) { - redirect('/dashboard'); - } - - return
{children}
; +
{children}
+
+
+ ); } diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx new file mode 100644 index 000000000..3b87a9b13 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { HTMLAttributes } from 'react'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import { BarChart3, User2 } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +export type AdminNavProps = HTMLAttributes; + +export const AdminNav = ({ className, ...props }: AdminNavProps) => { + const pathname = usePathname(); + + return ( +
+ + + +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx index 073056478..5fe030685 100644 --- a/apps/web/src/app/(dashboard)/admin/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/page.tsx @@ -1,114 +1,5 @@ -import { - Archive, - File, - FileX2, - LucideIcon, - Mail, - MailOpen, - PenTool, - Send, - User as UserIcon, - UserPlus2, - UserSquare2, -} from 'lucide-react'; +import { redirect } from 'next/navigation'; -import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents-stats'; -import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; -import { - getUsersCount, - getUsersWithSubscriptionsCount, -} from '@documenso/lib/server-only/admin/get-users-stats'; -import { - ReadStatus as InternalReadStatus, - SendStatus as InternalSendStatus, - SigningStatus as InternalSigningStatus, -} from '@documenso/prisma/client'; - -import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; - -type CardData = { - icon: LucideIcon; - title: string; - status: - | 'TOTAL_RECIPIENTS' - | 'OPENED' - | 'NOT_OPENED' - | 'SIGNED' - | 'NOT_SIGNED' - | 'SENT' - | 'NOT_SENT'; -}; - -const CARD_DATA: CardData[] = [ - { - icon: UserSquare2, - title: 'Recipients in the database', - status: 'TOTAL_RECIPIENTS', - }, - { - icon: MailOpen, - title: 'Opened documents', - status: InternalReadStatus.OPENED, - }, - { - icon: Mail, - title: 'Unopened documents', - status: InternalReadStatus.NOT_OPENED, - }, - { - icon: Send, - title: 'Sent documents', - status: InternalSendStatus.SENT, - }, - { - icon: Archive, - title: 'Unsent documents', - status: InternalSendStatus.NOT_SENT, - }, - { - icon: PenTool, - title: 'Signed documents', - status: InternalSigningStatus.SIGNED, - }, - { - icon: FileX2, - title: 'Unsigned documents', - status: InternalSigningStatus.NOT_SIGNED, - }, -]; - -export default async function Admin() { - const [usersCount, usersWithSubscriptionsCount, docsCount, recipientsStats] = await Promise.all([ - getUsersCount(), - getUsersWithSubscriptionsCount(), - getDocsCount(), - getRecipientsStats(), - ]); - - return ( -
-

Instance version: {process.env.APP_VERSION}

-
- - -
-

Document metrics

-
- -
- -

Recipients metrics

-
- {CARD_DATA.map((card) => ( -
- -
- ))} -
-
- ); +export default function Admin() { + redirect('/admin/stats'); } diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx new file mode 100644 index 000000000..b93af5a03 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -0,0 +1,75 @@ +import { + File, + FileCheck, + FileClock, + FileEdit, + Mail, + MailOpen, + PenTool, + User as UserIcon, + UserPlus2, + UserSquare2, +} from 'lucide-react'; + +import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats'; +import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; +import { + getUsersCount, + getUsersWithSubscriptionsCount, +} from '@documenso/lib/server-only/admin/get-users-stats'; + +import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; + +export default async function AdminStatsPage() { + const [usersCount, usersWithSubscriptionsCount, docStats, recipientStats] = await Promise.all([ + getUsersCount(), + getUsersWithSubscriptionsCount(), + getDocumentStats(), + getRecipientsStats(), + ]); + + return ( +
+

Instance Stats

+ +
+ + + + +
+ +
+
+

Document metrics

+ +
+ + + + +
+
+ +
+

Recipients metrics

+ +
+ + + + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index e3fd4c6d6..3f7a02e60 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -62,6 +62,19 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { Account + {isUserAdmin && ( + <> + + + + Admin + + + + + + )} + @@ -69,15 +82,6 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { - {isUserAdmin && ( - - - - Admin - - - )} - diff --git a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx index f59d42096..a2248ccdc 100644 --- a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx +++ b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx @@ -18,10 +18,10 @@ export const CardMetric = ({ icon: Icon, title, value, className }: CardMetricPr )} >
-
- {Icon && } +
+ {Icon && } -

{title}

+

{title}

diff --git a/packages/lib/server-only/admin/get-documents-stats.ts b/packages/lib/server-only/admin/get-documents-stats.ts index 9100a886c..e0d53373f 100644 --- a/packages/lib/server-only/admin/get-documents-stats.ts +++ b/packages/lib/server-only/admin/get-documents-stats.ts @@ -1,5 +1,26 @@ import { prisma } from '@documenso/prisma'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; -export const getDocsCount = async () => { - return await prisma.document.count(); +export const getDocumentStats = async () => { + const counts = await prisma.document.groupBy({ + by: ['status'], + _count: { + _all: true, + }, + }); + + const stats: Record, number> = { + [ExtendedDocumentStatus.DRAFT]: 0, + [ExtendedDocumentStatus.PENDING]: 0, + [ExtendedDocumentStatus.COMPLETED]: 0, + [ExtendedDocumentStatus.ALL]: 0, + }; + + counts.forEach((stat) => { + stats[stat.status] = stat._count._all; + + stats.ALL += stat._count._all; + }); + + return stats; }; From 599e857a1e282141fba6add03b5cf7c6e3c110d0 Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 12 Sep 2023 17:53:38 +1000 Subject: [PATCH 15/15] fix: add removed layout guard --- apps/web/src/app/(dashboard)/admin/layout.tsx | 13 ++++++++++++- .../lib/server-only/admin/get-recipients-stats.ts | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index a04c7b693..3aa47d1a9 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -1,12 +1,23 @@ import React from 'react'; +import { redirect } from 'next/navigation'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; + import { AdminNav } from './nav'; export type AdminSectionLayoutProps = { children: React.ReactNode; }; -export default function AdminSectionLayout({ children }: AdminSectionLayoutProps) { +export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) { + const user = await getRequiredServerComponentSession(); + + if (!isAdmin(user)) { + redirect('/documents'); + } + return (

diff --git a/packages/lib/server-only/admin/get-recipients-stats.ts b/packages/lib/server-only/admin/get-recipients-stats.ts index 92c0c3527..f24d0b5a2 100644 --- a/packages/lib/server-only/admin/get-recipients-stats.ts +++ b/packages/lib/server-only/admin/get-recipients-stats.ts @@ -16,6 +16,7 @@ export const getRecipientsStats = async () => { [SendStatus.SENT]: 0, [SendStatus.NOT_SENT]: 0, }; + results.forEach((result) => { const { readStatus, signingStatus, sendStatus, _count } = result; stats[readStatus] += _count; @@ -23,5 +24,6 @@ export const getRecipientsStats = async () => { stats[sendStatus] += _count; stats.TOTAL_RECIPIENTS += _count; }); + return stats; };