From 775de16d0af38918e5559deb60da5288d3560ccb Mon Sep 17 00:00:00 2001 From: pit Date: Thu, 21 Sep 2023 12:43:36 +0100 Subject: [PATCH 01/21] feat: admin ui for managing instance --- apps/web/src/app/(dashboard)/admin/nav.tsx | 8 +- .../app/(dashboard)/admin/users/[id]/page.tsx | 8 ++ .../admin/users/data-table-users.tsx | 135 ++++++++++++++++++ .../src/app/(dashboard)/admin/users/page.tsx | 35 +++++ .../lib/server-only/user/get-all-users.ts | 37 +++++ 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/users/page.tsx create mode 100644 packages/lib/server-only/user/get-all-users.ts diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx index 3b87a9b13..0b59335bf 100644 --- a/apps/web/src/app/(dashboard)/admin/nav.tsx +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -37,10 +37,12 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { 'justify-start md:w-full', pathname?.startsWith('/admin/users') && 'bg-secondary', )} - disabled + asChild > - - Users (Coming Soon) + + + Users + ); diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx new file mode 100644 index 000000000..3b8cfa287 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -0,0 +1,8 @@ +export default function UserPage() { + return ( +
+

Hey

+

Ho

+
+ ); +} diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx new file mode 100644 index 000000000..890d5cd48 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { useTransition } from 'react'; + +import Link from 'next/link'; + +import { Edit, Loader } from 'lucide-react'; + +import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; +import { Role } from '@documenso/prisma/client'; +import { Button } from '@documenso/ui/primitives/button'; +import { DataTable } from '@documenso/ui/primitives/data-table'; +import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; + +interface User { + id: number; + name: string | null; + email: string; + roles: Role[]; + Subscription: Subscription[]; +} + +interface Subscription { + id: number; + status: string; + planId: string | null; + priceId: string | null; + createdAt: Date | null; + periodEnd: Date | null; +} + +type UsersDataTableProps = { + users: User[]; + perPage: number; + page: number; + totalPages: number; +}; + +export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTableProps) => { + const [isPending, startTransition] = useTransition(); + const updateSearchParams = useUpdateSearchParams(); + console.log(users); + + const onPaginationChange = (page: number, perPage: number) => { + startTransition(() => { + updateSearchParams({ + page, + perPage, + }); + }); + }; + + return ( +
+
{row.original.id}
, + }, + { + header: 'Name', + accessorKey: 'name', + cell: ({ row }) =>
{row.original.name}
, + }, + { + header: 'Email', + accessorKey: 'email', + cell: ({ row }) =>
{row.original.email}
, + }, + { + header: 'Roles', + accessorKey: 'roles', + cell: ({ row }) => { + return ( + <> + {row.original.roles.map((role: string, idx: number) => { + return ( + + {role} {} + + ); + })} + + ); + }, + }, + { + header: 'Subscription status', + accessorKey: 'subscription', + cell: ({ row }) => { + return ( + <> + {row.original.Subscription.map((subscription: Subscription, idx: number) => { + return {subscription.status}; + })} + + ); + }, + }, + { + header: 'Edit', + accessorKey: 'edit', + cell: ({ row }) => { + return ( +
+ +
+ ); + }, + }, + ]} + data={users} + perPage={perPage} + currentPage={page} + totalPages={totalPages} + onPaginationChange={onPaginationChange} + > + {(table) => } +
+ + {isPending && ( +
+ +
+ )} +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/users/page.tsx b/apps/web/src/app/(dashboard)/admin/users/page.tsx new file mode 100644 index 000000000..7f8b9af47 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/users/page.tsx @@ -0,0 +1,35 @@ +import { findUsers } from '@documenso/lib/server-only/user/get-all-users'; + +/* +1. retrieve all users from the db +2. display them in a table +*/ +import { UsersDataTable } from './data-table-users'; + +type AdminManageUsersProps = { + searchParams?: { + page?: number; + perPage?: number; + }; +}; + +export default async function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) { + const page = Number(searchParams.page) || 1; + const perPage = Number(searchParams.perPage) || 10; + + const results = await findUsers({ page, perPage }); + + return ( +
+

Manage users

+
+ +
+
+ ); +} diff --git a/packages/lib/server-only/user/get-all-users.ts b/packages/lib/server-only/user/get-all-users.ts new file mode 100644 index 000000000..157a75d4a --- /dev/null +++ b/packages/lib/server-only/user/get-all-users.ts @@ -0,0 +1,37 @@ +import { prisma } from '@documenso/prisma'; + +type getAllUsersProps = { + page: number; + perPage: number; +}; + +export const findUsers = async ({ page = 1, perPage = 10 }: getAllUsersProps) => { + const [users, count] = await Promise.all([ + await prisma.user.findMany({ + select: { + id: true, + name: true, + email: true, + roles: true, + Subscription: { + select: { + id: true, + status: true, + planId: true, + priceId: true, + createdAt: true, + periodEnd: true, + }, + }, + }, + skip: Math.max(page - 1, 0) * perPage, + take: perPage, + }), + await prisma.user.count(), + ]); + + return { + users, + totalPages: Math.ceil(count / perPage), + }; +}; From 07bf780c3e52d252bd9a70399f620b4f150df195 Mon Sep 17 00:00:00 2001 From: pit Date: Thu, 21 Sep 2023 15:10:20 +0100 Subject: [PATCH 02/21] feat: build individual user page --- .../app/(dashboard)/admin/users/[id]/page.tsx | 85 ++++++++++++++++++- packages/trpc/server/profile-router/router.ts | 27 ++++++ packages/trpc/server/profile-router/schema.ts | 5 ++ 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index 3b8cfa287..ddeb52058 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -1,8 +1,87 @@ -export default function UserPage() { +'use client'; + +import { Loader } from 'lucide-react'; +import { Controller, useForm } from 'react-hook-form'; + +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { Input } from '@documenso/ui/primitives/input'; +import { Label } from '@documenso/ui/primitives/label'; +import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { FormErrorMessage } from '../../../../../components/form/form-error-message'; + +export default function UserPage({ params }: { params: { id: number } }) { + const toast = useToast(); + + const result = trpc.profile.getUser.useQuery( + { + id: Number(params.id), + }, + { + enabled: !!params.id, + }, + ); + + const { + register, + control, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm(); + + console.log(result.data); + + const onSubmit = async (data) => { + console.log(data); + }; + return (
-

Hey

-

Ho

+

Manage {result.data?.name}'s profile

+
+
+ + + +
+
+ + + +
+
+ + +
+ ( + onChange(v ?? '')} + /> + )} + /> + +
+
+
+ +
+
); } diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index bbeff675b..c5756c480 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -1,6 +1,8 @@ import { TRPCError } from '@trpc/server'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password'; +import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id'; import { resetPassword } from '@documenso/lib/server-only/user/reset-password'; import { updatePassword } from '@documenso/lib/server-only/user/update-password'; import { updateProfile } from '@documenso/lib/server-only/user/update-profile'; @@ -9,11 +11,36 @@ import { authenticatedProcedure, procedure, router } from '../trpc'; import { ZForgotPasswordFormSchema, ZResetPasswordFormSchema, + ZRetrieveUserByIdQuerySchema, ZUpdatePasswordMutationSchema, ZUpdateProfileMutationSchema, } from './schema'; export const profileRouter = router({ + getUser: authenticatedProcedure + .input(ZRetrieveUserByIdQuerySchema) + .query(async ({ input, ctx }) => { + const isUserAdmin = isAdmin(ctx.user); + + if (!isUserAdmin) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Not authorized to perform this action.', + }); + } + + try { + const { id } = input; + + return await getUserById({ id }); + } catch (err) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to retrieve the specified account. Please try again.', + }); + } + }), + updateProfile: authenticatedProcedure .input(ZUpdateProfileMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts index 641227684..a910ec3cc 100644 --- a/packages/trpc/server/profile-router/schema.ts +++ b/packages/trpc/server/profile-router/schema.ts @@ -1,5 +1,9 @@ import { z } from 'zod'; +export const ZRetrieveUserByIdQuerySchema = z.object({ + id: z.number().min(1), +}); + export const ZUpdateProfileMutationSchema = z.object({ name: z.string().min(1), signature: z.string(), @@ -18,6 +22,7 @@ export const ZResetPasswordFormSchema = z.object({ token: z.string().min(1), }); +export type TRetrieveUserByIdQuerySchema = z.infer; export type TUpdateProfileMutationSchema = z.infer; export type TUpdatePasswordMutationSchema = z.infer; export type TForgotPasswordFormSchema = z.infer; From f1bc772985b6feae7cf002b403e1db34aab8de1c Mon Sep 17 00:00:00 2001 From: pit Date: Fri, 29 Sep 2023 17:12:02 +0100 Subject: [PATCH 03/21] chore: improve the ui --- .../admin/users/data-table-users.tsx | 39 +++++++-- packages/lib/server-only/admin/update-user.ts | 30 +++++++ .../lib/server-only/user/get-all-users.ts | 5 ++ packages/ui/primitives/combobox.tsx | 84 +++++++++++++++++++ 4 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 packages/lib/server-only/admin/update-user.ts create mode 100644 packages/ui/primitives/combobox.tsx diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx index 890d5cd48..0329b6a17 100644 --- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -18,6 +18,7 @@ interface User { email: string; roles: Role[]; Subscription: Subscription[]; + Document: Document[]; } interface Subscription { @@ -36,10 +37,13 @@ type UsersDataTableProps = { totalPages: number; }; +type Document = { + id: number; +}; + export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTableProps) => { const [isPending, startTransition] = useTransition(); const updateSearchParams = useUpdateSearchParams(); - console.log(users); const onPaginationChange = (page: number, perPage: number) => { startTransition(() => { @@ -75,9 +79,9 @@ export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTa cell: ({ row }) => { return ( <> - {row.original.roles.map((role: string, idx: number) => { + {row.original.roles.map((role: string, i: number) => { return ( - + {role} {} ); @@ -87,15 +91,32 @@ export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTa }, }, { - header: 'Subscription status', + header: 'Subscription', accessorKey: 'subscription', + cell: ({ row }) => { + if (row.original.Subscription && row.original.Subscription.length > 0) { + return ( + <> + {row.original.Subscription.map((subscription: Subscription, i: number) => { + return {subscription.status}; + })} + + ); + } else { + return NONE; + } + }, + }, + { + header: 'Documents', + accessorKey: 'documents', cell: ({ row }) => { return ( - <> - {row.original.Subscription.map((subscription: Subscription, idx: number) => { - return {subscription.status}; - })} - +
+ + {row.original.Document.length} + +
); }, }, diff --git a/packages/lib/server-only/admin/update-user.ts b/packages/lib/server-only/admin/update-user.ts new file mode 100644 index 000000000..b10e6477d --- /dev/null +++ b/packages/lib/server-only/admin/update-user.ts @@ -0,0 +1,30 @@ +import { prisma } from '@documenso/prisma'; +import { Role } from '@documenso/prisma/client'; + +export type UpdateUserOptions = { + userId: number; + name: string; + email: string; + roles: Role[]; +}; + +export const updateUser = async ({ userId, name, email, roles }: UpdateUserOptions) => { + console.log('wtf'); + await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + }); + + const updatedUser = await prisma.user.update({ + where: { + id: userId, + }, + data: { + name, + email, + roles, + }, + }); + return updatedUser; +}; diff --git a/packages/lib/server-only/user/get-all-users.ts b/packages/lib/server-only/user/get-all-users.ts index 157a75d4a..35e165260 100644 --- a/packages/lib/server-only/user/get-all-users.ts +++ b/packages/lib/server-only/user/get-all-users.ts @@ -23,6 +23,11 @@ export const findUsers = async ({ page = 1, perPage = 10 }: getAllUsersProps) => periodEnd: true, }, }, + Document: { + select: { + id: true, + }, + }, }, skip: Math.max(page - 1, 0) * perPage, take: perPage, diff --git a/packages/ui/primitives/combobox.tsx b/packages/ui/primitives/combobox.tsx new file mode 100644 index 000000000..6e566e188 --- /dev/null +++ b/packages/ui/primitives/combobox.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; + +import { Check, ChevronsUpDown } from 'lucide-react'; + +import { Role } from '@documenso/prisma/client'; +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from '@documenso/ui/primitives/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; + +type ComboboxProps = { + listValues: string[]; + onChange: (values: string[]) => void; +}; + +const Combobox = ({ listValues, onChange }: ComboboxProps) => { + const [open, setOpen] = React.useState(false); + const [selectedValues, setSelectedValues] = React.useState([]); + const dbRoles = Object.values(Role); + + React.useEffect(() => { + setSelectedValues(listValues); + }, [listValues]); + + const allRoles = [...new Set([...dbRoles, ...selectedValues])]; + + const handleSelect = (currentValue: string) => { + let newSelectedValues; + if (selectedValues.includes(currentValue)) { + newSelectedValues = selectedValues.filter((value) => value !== currentValue); + } else { + newSelectedValues = [...selectedValues, currentValue]; + } + + setSelectedValues(newSelectedValues); + onChange(newSelectedValues); + setOpen(false); + }; + + return ( + <> + + + + + + + + No value found. + + {allRoles.map((value: string, i: number) => ( + handleSelect(value)}> + + {value} + + ))} + + + + + + ); +}; + +export { Combobox }; From c2cda0f06e907232d98e37f570dafd3194b09180 Mon Sep 17 00:00:00 2001 From: pit Date: Fri, 29 Sep 2023 17:26:37 +0100 Subject: [PATCH 04/21] feat: update user functionality --- .../app/(dashboard)/admin/users/[id]/page.tsx | 75 +++++++++++++++++-- packages/lib/server-only/admin/update-user.ts | 8 +- packages/trpc/server/admin-router/router.ts | 33 ++++++++ packages/trpc/server/admin-router/schema.ts | 13 ++++ packages/trpc/server/router.ts | 2 + 5 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 packages/trpc/server/admin-router/router.ts create mode 100644 packages/trpc/server/admin-router/schema.ts diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index ddeb52058..d6b0d9d25 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -1,10 +1,13 @@ 'use client'; +import { useRouter } from 'next/navigation'; + import { Loader } from 'lucide-react'; import { Controller, useForm } from 'react-hook-form'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; +import { Combobox } from '@documenso/ui/primitives/combobox'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; @@ -13,7 +16,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { FormErrorMessage } from '../../../../../components/form/form-error-message'; export default function UserPage({ params }: { params: { id: number } }) { - const toast = useToast(); + const { toast } = useToast(); + const router = useRouter(); const result = trpc.profile.getUser.useQuery( { @@ -24,6 +28,15 @@ export default function UserPage({ params }: { params: { id: number } }) { }, ); + const roles = result.data?.roles; + let rolesArr: string[] = []; + + if (roles) { + rolesArr = Object.values(roles); + } + + const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation(); + const { register, control, @@ -31,10 +44,44 @@ export default function UserPage({ params }: { params: { id: number } }) { formState: { errors, isSubmitting }, } = useForm(); - console.log(result.data); - const onSubmit = async (data) => { console.log(data); + + // const submittedRoles = data.roles + // .split(',') + // .map((role: string) => role.trim()) + // .map((role: string) => role.toUpperCase()); + + // const dbRoles = JSON.stringify(Role); + + // const roleExists = submittedRoles.some((role: string) => dbRoles.includes(role)); + // console.log('roleExists', roleExists); + + // console.log('db roles', dbRoles); + + try { + await updateUserMutation({ + id: Number(result.data?.id), + name: data.name, + email: data.email, + roles: data.roles, + }); + + router.refresh(); + + toast({ + title: 'Profile updated', + description: 'Your profile has been updated.', + duration: 5000, + }); + } catch (e) { + console.log(e); + toast({ + title: 'Error', + description: 'An error occurred while updating your profile.', + variant: 'destructive', + }); + } }; return ( @@ -45,17 +92,30 @@ export default function UserPage({ params }: { params: { id: number } }) { - +
- +
-
+
+ + ( + onChange(values)} /> + )} + /> + +
+ {/*
@@ -74,7 +134,8 @@ export default function UserPage({ params }: { params: { id: number } }) { />
-
+ */} +
+ )) + .with({ isRecipient: true, isPending: true, isSigned: false }, () => ( + + )) + .otherwise(() => ( + + )); +}; diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx new file mode 100644 index 000000000..72fdb4845 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx @@ -0,0 +1,124 @@ +'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 { getFile } from '@documenso/lib/universal/upload/get-file'; +import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client'; +import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; +import { trpc } from '@documenso/trpc/client'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from '@documenso/ui/primitives/dropdown-menu'; + +export type DataTableActionDropdownProps = { + row: Document & { + User: Pick; + 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 = async () => { + let document: DocumentWithData | null = null; + + if (!recipient) { + document = await trpc.document.getDocumentById.query({ + id: row.id, + }); + } else { + document = await trpc.document.getDocumentByToken.query({ + token: recipient.token, + }); + } + + const documentData = document?.documentData; + + if (!documentData) { + return; + } + + const documentBytes = await getFile(documentData); + + 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 ( + + + + + + + Action + + + + Download + + + + + Duplicate + + + + + Void + + + + + Delete + + + + + Resend + + + + ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-title.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-title.tsx new file mode 100644 index 000000000..c04f9f13d --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/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)/admin/documents/data-table.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx new file mode 100644 index 000000000..b1ab92e42 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx @@ -0,0 +1,115 @@ +'use client'; + +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'; +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'; + +import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; +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'; +import { DataTableTitle } from './data-table-title'; + +export type DocumentsDataTableProps = { + results: FindResultSet< + Document & { + Recipient: Recipient[]; + User: Pick; + } + >; +}; + +export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { + const { data: session } = useSession(); + const [isPending, startTransition] = useTransition(); + + const updateSearchParams = useUpdateSearchParams(); + + const onPaginationChange = (page: number, perPage: number) => { + startTransition(() => { + updateSearchParams({ + page, + perPage, + }); + }); + }; + + if (!session) { + return null; + } + + return ( +
+ , + }, + { + header: 'Title', + cell: ({ row }) => , + }, + { + header: 'Owner', + accessorKey: 'owner', + cell: ({ row }) => { + return ( + + + + ); + }, + }, + { + header: 'Status', + accessorKey: 'status', + cell: ({ row }) => , + }, + { + header: 'Actions', + cell: ({ row }) => ( +
+ + +
+ ), + }, + ]} + data={results.data} + perPage={results.perPage} + currentPage={results.currentPage} + totalPages={results.totalPages} + onPaginationChange={onPaginationChange} + > + {(table) => } +
+ + {isPending && ( +
+ +
+ )} +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/documents/page.tsx b/apps/web/src/app/(dashboard)/admin/documents/page.tsx new file mode 100644 index 000000000..d62d82ada --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/documents/page.tsx @@ -0,0 +1,36 @@ +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents'; + +import { DocumentsDataTable } from './data-table'; + +export type DocumentsPageProps = { + searchParams?: { + page?: string; + perPage?: string; + }; +}; + +export default async function Documents({ searchParams = {} }: DocumentsPageProps) { + const user = await getRequiredServerComponentSession(); + const page = Number(searchParams.page) || 1; + const perPage = Number(searchParams.perPage) || 20; + + const results = await findDocuments({ + userId: user.id, + orderBy: { + column: 'createdAt', + direction: 'desc', + }, + page, + perPage, + }); + + return ( +
+

Manage documents

+
+ +
+
+ ); +} diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx index 0b59335bf..8050f867a 100644 --- a/apps/web/src/app/(dashboard)/admin/nav.tsx +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -5,7 +5,7 @@ import { HTMLAttributes } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { BarChart3, User2 } from 'lucide-react'; +import { BarChart3, FileStack, User2, Wallet2 } from 'lucide-react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; @@ -44,6 +44,34 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { Users + + + +
); }; diff --git a/packages/lib/server-only/admin/get-all-documents.ts b/packages/lib/server-only/admin/get-all-documents.ts new file mode 100644 index 000000000..a1abdb186 --- /dev/null +++ b/packages/lib/server-only/admin/get-all-documents.ts @@ -0,0 +1,67 @@ +import { prisma } from '@documenso/prisma'; +import { Document } from '@documenso/prisma/client'; + +export interface FindDocumentsOptions { + term?: string; + page?: number; + perPage?: number; + orderBy?: { + column: keyof Omit; + direction: 'asc' | 'desc'; + }; +} + +export const findDocuments = async ({ + term, + page = 1, + perPage = 10, + orderBy, +}: FindDocumentsOptions) => { + const orderByColumn = orderBy?.column ?? 'createdAt'; + const orderByDirection = orderBy?.direction ?? 'desc'; + + const termFilters = !term + ? undefined + : ({ + title: { + contains: term, + mode: 'insensitive', + }, + } as const); + + const [data, count] = await Promise.all([ + prisma.document.findMany({ + where: { + ...termFilters, + }, + skip: Math.max(page - 1, 0) * perPage, + take: perPage, + orderBy: { + [orderByColumn]: orderByDirection, + }, + include: { + User: { + select: { + id: true, + name: true, + email: true, + }, + }, + Recipient: true, + }, + }), + prisma.document.count({ + where: { + ...termFilters, + }, + }), + ]); + + return { + data, + count, + currentPage: Math.max(page, 1), + perPage, + totalPages: Math.ceil(count / perPage), + }; +}; From 2b44e54d99e5f17b69d8cbf1ed507264da03e814 Mon Sep 17 00:00:00 2001 From: pit Date: Thu, 5 Oct 2023 18:35:12 +0300 Subject: [PATCH 07/21] feat: subscriptions and documents page --- .../(dashboard)/admin/subscriptions/page.tsx | 65 +++++++++++++++++++ .../admin/users/[id]/documents/page.tsx | 3 + .../app/(dashboard)/admin/users/[id]/page.tsx | 1 - .../admin/get-all-subscriptions.ts | 13 ++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/app/(dashboard)/admin/subscriptions/page.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/users/[id]/documents/page.tsx create mode 100644 packages/lib/server-only/admin/get-all-subscriptions.ts diff --git a/apps/web/src/app/(dashboard)/admin/subscriptions/page.tsx b/apps/web/src/app/(dashboard)/admin/subscriptions/page.tsx new file mode 100644 index 000000000..68ccf1ee4 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/subscriptions/page.tsx @@ -0,0 +1,65 @@ +import Link from 'next/link'; + +import { findSubscriptions } from '@documenso/lib/server-only/admin/get-all-subscriptions'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@documenso/ui/primitives/table'; + +export default async function Subscriptions() { + const subscriptions = await findSubscriptions(); + + return ( +
+

Manage subscriptions

+
+ + + + ID + Status + Created At + Ends On + User ID + + + + {subscriptions.map((subscription, index) => ( + + {subscription.id} + {subscription.status} + + {subscription.createdAt + ? new Date(subscription.createdAt).toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }) + : 'N/A'} + + + {subscription.periodEnd + ? new Date(subscription.periodEnd).toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }) + : 'N/A'} + + + {subscription.userId} + + + ))} + +
+
+
+ ); +} diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/documents/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/documents/page.tsx new file mode 100644 index 000000000..e17dc611b --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/documents/page.tsx @@ -0,0 +1,3 @@ +export default function UserDocuments() { + return

User docs

; +} diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index 5bbc7b340..802f2ec0c 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -74,7 +74,6 @@ export default function UserPage({ params }: { params: { id: number } }) { duration: 5000, }); } catch (e) { - console.log(e); toast({ title: 'Error', description: 'An error occurred while updating your profile.', diff --git a/packages/lib/server-only/admin/get-all-subscriptions.ts b/packages/lib/server-only/admin/get-all-subscriptions.ts new file mode 100644 index 000000000..5080c4c22 --- /dev/null +++ b/packages/lib/server-only/admin/get-all-subscriptions.ts @@ -0,0 +1,13 @@ +import { prisma } from '@documenso/prisma'; + +export const findSubscriptions = async () => { + return prisma.subscription.findMany({ + select: { + id: true, + status: true, + createdAt: true, + periodEnd: true, + userId: true, + }, + }); +}; From 5f14f87406d9003ccd32864d6ba228e08d8b59ad Mon Sep 17 00:00:00 2001 From: pit Date: Fri, 6 Oct 2023 15:48:05 +0300 Subject: [PATCH 08/21] feat: filter users by name or email --- .../admin/documents/data-table.tsx | 16 ++-- .../app/(dashboard)/admin/documents/page.tsx | 2 - .../admin/users/data-table-users.tsx | 26 +++---- .../src/app/(dashboard)/admin/users/page.tsx | 25 +++--- .../src/app/(dashboard)/admin/users/users.tsx | 78 +++++++++++++++++++ .../(dashboard)/admin/generic-data-table.tsx | 57 ++++++++++++++ .../lib/server-only/user/get-all-users.ts | 33 +++++++- 7 files changed, 192 insertions(+), 45 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/users/users.tsx create mode 100644 apps/web/src/components/(dashboard)/admin/generic-data-table.tsx diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx index b1ab92e42..1d121742a 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx @@ -10,10 +10,10 @@ 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'; import { Document, Recipient, User } from '@documenso/prisma/client'; +import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; -import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; import { DocumentStatus } from '~/components/formatter/document-status'; import { LocaleDate } from '~/components/formatter/locale-date'; @@ -68,15 +68,11 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { cell: ({ row }) => { return ( - + + + {row.original.User.name} + + ); }, diff --git a/apps/web/src/app/(dashboard)/admin/documents/page.tsx b/apps/web/src/app/(dashboard)/admin/documents/page.tsx index d62d82ada..6b5a0761c 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/page.tsx @@ -11,12 +11,10 @@ export type DocumentsPageProps = { }; export default async function Documents({ searchParams = {} }: DocumentsPageProps) { - const user = await getRequiredServerComponentSession(); const page = Number(searchParams.page) || 1; const perPage = Number(searchParams.perPage) || 20; const results = await findDocuments({ - userId: user.id, orderBy: { column: 'createdAt', direction: 'desc', diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx index 0329b6a17..a598fc605 100644 --- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -7,7 +7,7 @@ import Link from 'next/link'; import { Edit, Loader } from 'lucide-react'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; -import { Role } from '@documenso/prisma/client'; +import { Document, Role, Subscription } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; @@ -17,18 +17,16 @@ interface User { name: string | null; email: string; roles: Role[]; - Subscription: Subscription[]; - Document: Document[]; + Subscription: SubscriptionLite[]; + Document: DocumentLite[]; } -interface Subscription { - id: number; - status: string; - planId: string | null; - priceId: string | null; - createdAt: Date | null; - periodEnd: Date | null; -} +type SubscriptionLite = Pick< + Subscription, + 'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd' +>; + +type DocumentLite = Pick; type UsersDataTableProps = { users: User[]; @@ -37,10 +35,6 @@ type UsersDataTableProps = { totalPages: number; }; -type Document = { - id: number; -}; - export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTableProps) => { const [isPending, startTransition] = useTransition(); const updateSearchParams = useUpdateSearchParams(); @@ -97,7 +91,7 @@ export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTa if (row.original.Subscription && row.original.Subscription.length > 0) { return ( <> - {row.original.Subscription.map((subscription: Subscription, i: number) => { + {row.original.Subscription.map((subscription: SubscriptionLite, i: number) => { return {subscription.status}; })} diff --git a/apps/web/src/app/(dashboard)/admin/users/page.tsx b/apps/web/src/app/(dashboard)/admin/users/page.tsx index 7f8b9af47..67dc5563b 100644 --- a/apps/web/src/app/(dashboard)/admin/users/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/page.tsx @@ -1,10 +1,6 @@ import { findUsers } from '@documenso/lib/server-only/user/get-all-users'; -/* -1. retrieve all users from the db -2. display them in a table -*/ -import { UsersDataTable } from './data-table-users'; +import { Users } from './users'; type AdminManageUsersProps = { searchParams?: { @@ -13,23 +9,22 @@ type AdminManageUsersProps = { }; }; -export default async function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) { +export default function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) { const page = Number(searchParams.page) || 1; const perPage = Number(searchParams.perPage) || 10; - const results = await findUsers({ page, perPage }); + async function search(search: string) { + 'use server'; + + const results = await findUsers({ username: search, email: search, page, perPage }); + + return results; + } return (

Manage users

-
- -
+
); } diff --git a/apps/web/src/app/(dashboard)/admin/users/users.tsx b/apps/web/src/app/(dashboard)/admin/users/users.tsx new file mode 100644 index 000000000..e44772dbb --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/users/users.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import { Document, Role, Subscription } from '@documenso/prisma/client'; +import { Button } from '@documenso/ui/primitives/button'; +import { Input } from '@documenso/ui/primitives/input'; + +import { UsersDataTable } from './data-table-users'; + +export type SubscriptionLite = Pick< + Subscription, + 'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd' +>; +export type DocumentLite = Pick; + +export type User = { + id: number; + name: string | null; + email: string; + roles: Role[]; + Subscription: SubscriptionLite[]; + Document: DocumentLite[]; +}; + +export type UsersProps = { + search: (search: string) => Promise<{ users: User[]; totalPages: number }>; + perPage: number; + page: number; +}; + +export const Users = ({ search, perPage, page }: UsersProps) => { + const [data, setData] = useState([]); + const [totalPages, setTotalPages] = useState(0); + + const [searchString, setSearchString] = useState(''); + + useEffect(() => { + const fetchData = async () => { + try { + const result = await search(searchString); + setData(result.users); + setTotalPages(result.totalPages); + } catch (err) { + throw new Error(err); + } + }; + + fetchData(); + }, [searchString, search]); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await search(searchString); + setData(result.users); + }; + + const handleChange = (e: React.ChangeEvent) => { + setSearchString(e.target.value); + }; + + return ( + <> + + + + +
+ +
+ + ); +}; diff --git a/apps/web/src/components/(dashboard)/admin/generic-data-table.tsx b/apps/web/src/components/(dashboard)/admin/generic-data-table.tsx new file mode 100644 index 000000000..1dd3e01d3 --- /dev/null +++ b/apps/web/src/components/(dashboard)/admin/generic-data-table.tsx @@ -0,0 +1,57 @@ +import { useTransition } from 'react'; + +import { ColumnDef } from '@tanstack/react-table'; +import { Loader } from 'lucide-react'; + +import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; +import { DataTable } from '@documenso/ui/primitives/data-table'; +import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; + +type GenericDataTableProps = { + columns: ColumnDef[]; + data: TData[]; + perPage: number; + currentPage: number; + totalPages: number; +}; + +export function GenericDataTable({ + columns, + data, + perPage, + currentPage, + totalPages, +}: GenericDataTableProps) { + const [isPending, startTransition] = useTransition(); + const updateSearchParams = useUpdateSearchParams(); + + const onPaginationChange = (page: number, perPage: number) => { + startTransition(() => { + updateSearchParams({ + page: page.toString(), + perPage: perPage.toString(), + }); + }); + }; + + return ( +
+ + {(table) => } + + + {isPending && ( +
+ +
+ )} +
+ ); +} diff --git a/packages/lib/server-only/user/get-all-users.ts b/packages/lib/server-only/user/get-all-users.ts index 35e165260..babcc7ba1 100644 --- a/packages/lib/server-only/user/get-all-users.ts +++ b/packages/lib/server-only/user/get-all-users.ts @@ -1,11 +1,37 @@ +import { Prisma } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; type getAllUsersProps = { + username: string; + email: string; page: number; perPage: number; }; -export const findUsers = async ({ page = 1, perPage = 10 }: getAllUsersProps) => { +export const findUsers = async ({ + username = '', + email = '', + page = 1, + perPage = 10, +}: getAllUsersProps) => { + const whereClause = Prisma.validator()({ + OR: [ + { + name: { + contains: username, + mode: 'insensitive', + }, + }, + { + email: { + contains: email, + mode: 'insensitive', + }, + }, + ], + }); + const [users, count] = await Promise.all([ await prisma.user.findMany({ select: { @@ -29,10 +55,13 @@ export const findUsers = async ({ page = 1, perPage = 10 }: getAllUsersProps) => }, }, }, + where: whereClause, skip: Math.max(page - 1, 0) * perPage, take: perPage, }), - await prisma.user.count(), + await prisma.user.count({ + where: whereClause, + }), ]); return { From d4ae733e9e7462c1418cf308a33194259bc53895 Mon Sep 17 00:00:00 2001 From: pit Date: Mon, 9 Oct 2023 11:59:08 +0300 Subject: [PATCH 09/21] chore: add transition and check for empty users array --- .../src/app/(dashboard)/admin/users/users.tsx | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/users/users.tsx b/apps/web/src/app/(dashboard)/admin/users/users.tsx index e44772dbb..c27f71472 100644 --- a/apps/web/src/app/(dashboard)/admin/users/users.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/users.tsx @@ -1,11 +1,15 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useTransition } from 'react'; + +import { Loader } from 'lucide-react'; import { Document, Role, Subscription } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; import { Input } from '@documenso/ui/primitives/input'; +import { useDebouncedValue } from '~/hooks/use-debounced-value'; + import { UsersDataTable } from './data-table-users'; export type SubscriptionLite = Pick< @@ -24,7 +28,7 @@ export type User = { }; export type UsersProps = { - search: (search: string) => Promise<{ users: User[]; totalPages: number }>; + search: (_search: string) => Promise<{ users: User[]; totalPages: number }>; perPage: number; page: number; }; @@ -32,13 +36,14 @@ export type UsersProps = { export const Users = ({ search, perPage, page }: UsersProps) => { const [data, setData] = useState([]); const [totalPages, setTotalPages] = useState(0); - + const [isPending, startTransition] = useTransition(); const [searchString, setSearchString] = useState(''); + const debouncedSearchString = useDebouncedValue(searchString, 500); useEffect(() => { const fetchData = async () => { try { - const result = await search(searchString); + const result = await search(debouncedSearchString); setData(result.users); setTotalPages(result.totalPages); } catch (err) { @@ -47,12 +52,14 @@ export const Users = ({ search, perPage, page }: UsersProps) => { }; fetchData(); - }, [searchString, search]); + }, [debouncedSearchString, search]); - const onSubmit = async (e: React.FormEvent) => { + const onSubmit = (e: React.FormEvent) => { e.preventDefault(); - const result = await search(searchString); - setData(result.users); + startTransition(async () => { + const result = await search(debouncedSearchString); + setData(result.users); + }); }; const handleChange = (e: React.ChangeEvent) => { @@ -71,7 +78,13 @@ export const Users = ({ search, perPage, page }: UsersProps) => {
- + {data.length === 0 || isPending ? ( +
+ +
+ ) : ( + + )}
); From 4c518df60d94246d7375995a6548be4ad4a8caf9 Mon Sep 17 00:00:00 2001 From: pit Date: Mon, 9 Oct 2023 12:02:55 +0300 Subject: [PATCH 10/21] chore: remove generic data table --- .../(dashboard)/admin/generic-data-table.tsx | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 apps/web/src/components/(dashboard)/admin/generic-data-table.tsx diff --git a/apps/web/src/components/(dashboard)/admin/generic-data-table.tsx b/apps/web/src/components/(dashboard)/admin/generic-data-table.tsx deleted file mode 100644 index 1dd3e01d3..000000000 --- a/apps/web/src/components/(dashboard)/admin/generic-data-table.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useTransition } from 'react'; - -import { ColumnDef } from '@tanstack/react-table'; -import { Loader } from 'lucide-react'; - -import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; -import { DataTable } from '@documenso/ui/primitives/data-table'; -import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; - -type GenericDataTableProps = { - columns: ColumnDef[]; - data: TData[]; - perPage: number; - currentPage: number; - totalPages: number; -}; - -export function GenericDataTable({ - columns, - data, - perPage, - currentPage, - totalPages, -}: GenericDataTableProps) { - const [isPending, startTransition] = useTransition(); - const updateSearchParams = useUpdateSearchParams(); - - const onPaginationChange = (page: number, perPage: number) => { - startTransition(() => { - updateSearchParams({ - page: page.toString(), - perPage: perPage.toString(), - }); - }); - }; - - return ( -
- - {(table) => } - - - {isPending && ( -
- -
- )} -
- ); -} From a11440a7f36820bd9da6f89c1cf56a8bccca9ba4 Mon Sep 17 00:00:00 2001 From: pit Date: Mon, 9 Oct 2023 13:30:28 +0300 Subject: [PATCH 11/21] chore: tidy up --- .../documents/data-table-action-button.tsx | 52 ++++--------------- .../documents/data-table-action-dropdown.tsx | 16 +----- .../app/(dashboard)/admin/documents/page.tsx | 5 -- .../admin/users/[id]/documents/page.tsx | 3 -- .../admin/users/data-table-users.tsx | 8 +-- .../server-only/admin/get-all-documents.ts | 17 +----- packages/ui/primitives/combobox.tsx | 2 +- 7 files changed, 15 insertions(+), 88 deletions(-) delete mode 100644 apps/web/src/app/(dashboard)/admin/users/[id]/documents/page.tsx diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx index 7c1d42d2b..b1aa8efc5 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx @@ -2,11 +2,10 @@ import Link from 'next/link'; -import { Edit, Pencil, Share } from 'lucide-react'; +import { Edit } 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 { Document, Recipient, User } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; export type DataTableActionButtonProps = { @@ -23,43 +22,12 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => { 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 }, () => ( - - )) - .with({ isRecipient: true, isPending: true, isSigned: false }, () => ( - - )) - .otherwise(() => ( - - )); + return ( + + ); }; diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx index 72fdb4845..4788033b1 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx @@ -1,18 +1,6 @@ 'use client'; -import Link from 'next/link'; - -import { - Copy, - Download, - Edit, - History, - MoreHorizontal, - Pencil, - Share, - Trash2, - XCircle, -} from 'lucide-react'; +import { Copy, Download, History, MoreHorizontal, Trash2, XCircle } from 'lucide-react'; import { useSession } from 'next-auth/react'; import { getFile } from '@documenso/lib/universal/upload/get-file'; @@ -42,8 +30,6 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = } 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; diff --git a/apps/web/src/app/(dashboard)/admin/documents/page.tsx b/apps/web/src/app/(dashboard)/admin/documents/page.tsx index 6b5a0761c..2fbbcd4dc 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/page.tsx @@ -1,4 +1,3 @@ -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents'; import { DocumentsDataTable } from './data-table'; @@ -15,10 +14,6 @@ export default async function Documents({ searchParams = {} }: DocumentsPageProp const perPage = Number(searchParams.perPage) || 20; const results = await findDocuments({ - orderBy: { - column: 'createdAt', - direction: 'desc', - }, page, perPage, }); diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/documents/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/documents/page.tsx deleted file mode 100644 index e17dc611b..000000000 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/documents/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function UserDocuments() { - return

User docs

; -} diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx index a598fc605..7c1ec0520 100644 --- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -105,13 +105,7 @@ export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTa header: 'Documents', accessorKey: 'documents', cell: ({ row }) => { - return ( -
- - {row.original.Document.length} - -
- ); + return
{row.original.Document.length}
; }, }, { diff --git a/packages/lib/server-only/admin/get-all-documents.ts b/packages/lib/server-only/admin/get-all-documents.ts index a1abdb186..057b00afb 100644 --- a/packages/lib/server-only/admin/get-all-documents.ts +++ b/packages/lib/server-only/admin/get-all-documents.ts @@ -1,25 +1,12 @@ import { prisma } from '@documenso/prisma'; -import { Document } from '@documenso/prisma/client'; export interface FindDocumentsOptions { term?: string; page?: number; perPage?: number; - orderBy?: { - column: keyof Omit; - direction: 'asc' | 'desc'; - }; } -export const findDocuments = async ({ - term, - page = 1, - perPage = 10, - orderBy, -}: FindDocumentsOptions) => { - const orderByColumn = orderBy?.column ?? 'createdAt'; - const orderByDirection = orderBy?.direction ?? 'desc'; - +export const findDocuments = async ({ term, page = 1, perPage = 10 }: FindDocumentsOptions) => { const termFilters = !term ? undefined : ({ @@ -37,7 +24,7 @@ export const findDocuments = async ({ skip: Math.max(page - 1, 0) * perPage, take: perPage, orderBy: { - [orderByColumn]: orderByDirection, + createdAt: 'desc', }, include: { User: { diff --git a/packages/ui/primitives/combobox.tsx b/packages/ui/primitives/combobox.tsx index 6e566e188..90fdc7849 100644 --- a/packages/ui/primitives/combobox.tsx +++ b/packages/ui/primitives/combobox.tsx @@ -16,7 +16,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitive type ComboboxProps = { listValues: string[]; - onChange: (values: string[]) => void; + onChange: (_values: string[]) => void; }; const Combobox = ({ listValues, onChange }: ComboboxProps) => { From 1299aa51eebb291cc7956a2276beec13c234848f Mon Sep 17 00:00:00 2001 From: pit Date: Tue, 10 Oct 2023 11:44:16 +0300 Subject: [PATCH 12/21] chore: move fetching in data-table-users --- .../admin/users/data-table-users.tsx | 60 +++++++++++- .../admin/users/fetch-users.actions.ts | 9 ++ .../src/app/(dashboard)/admin/users/page.tsx | 14 +-- .../src/app/(dashboard)/admin/users/users.tsx | 91 ------------------- 4 files changed, 66 insertions(+), 108 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/users/fetch-users.actions.ts delete mode 100644 apps/web/src/app/(dashboard)/admin/users/users.tsx diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx index 7c1ec0520..456fda7cd 100644 --- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useTransition } from 'react'; +import { useEffect, useState, useTransition } from 'react'; import Link from 'next/link'; @@ -11,6 +11,12 @@ import { Document, Role, Subscription } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; +import { Input } from '@documenso/ui/primitives/input'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useDebouncedValue } from '~/hooks/use-debounced-value'; + +import { search } from './fetch-users.actions'; interface User { id: number; @@ -29,15 +35,19 @@ type SubscriptionLite = Pick< type DocumentLite = Pick; type UsersDataTableProps = { - users: User[]; perPage: number; page: number; - totalPages: number; }; -export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTableProps) => { +export const UsersDataTable = ({ perPage, page }: UsersDataTableProps) => { + const { toast } = useToast(); + const [isPending, startTransition] = useTransition(); const updateSearchParams = useUpdateSearchParams(); + const [data, setData] = useState([]); + const [searchString, setSearchString] = useState(''); + const [totalPages, setTotalPages] = useState(0); + const debouncedSearchString = useDebouncedValue(searchString, 500); const onPaginationChange = (page: number, perPage: number) => { startTransition(() => { @@ -48,8 +58,48 @@ export const UsersDataTable = ({ users, perPage, page, totalPages }: UsersDataTa }); }; + useEffect(() => { + const fetchData = async () => { + try { + const result = await search(debouncedSearchString, page, perPage); + setData(result.users); + setTotalPages(result.totalPages); + + if (result.totalPages < page) { + startTransition(() => { + updateSearchParams({ + page: 1, + perPage, + }); + }); + } + } catch (err) { + throw new Error(err); + } + }; + + fetchData().catch(() => { + toast({ + title: 'Something went wrong', + description: 'Please try again', + variant: 'destructive', + }); + }); + }, [debouncedSearchString, page, perPage]); + + const handleChange = (e: React.ChangeEvent) => { + setSearchString(e.target.value); + }; + return (
+

Manage users

- +
); } diff --git a/apps/web/src/app/(dashboard)/admin/users/users.tsx b/apps/web/src/app/(dashboard)/admin/users/users.tsx deleted file mode 100644 index c27f71472..000000000 --- a/apps/web/src/app/(dashboard)/admin/users/users.tsx +++ /dev/null @@ -1,91 +0,0 @@ -'use client'; - -import { useEffect, useState, useTransition } from 'react'; - -import { Loader } from 'lucide-react'; - -import { Document, Role, Subscription } from '@documenso/prisma/client'; -import { Button } from '@documenso/ui/primitives/button'; -import { Input } from '@documenso/ui/primitives/input'; - -import { useDebouncedValue } from '~/hooks/use-debounced-value'; - -import { UsersDataTable } from './data-table-users'; - -export type SubscriptionLite = Pick< - Subscription, - 'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd' ->; -export type DocumentLite = Pick; - -export type User = { - id: number; - name: string | null; - email: string; - roles: Role[]; - Subscription: SubscriptionLite[]; - Document: DocumentLite[]; -}; - -export type UsersProps = { - search: (_search: string) => Promise<{ users: User[]; totalPages: number }>; - perPage: number; - page: number; -}; - -export const Users = ({ search, perPage, page }: UsersProps) => { - const [data, setData] = useState([]); - const [totalPages, setTotalPages] = useState(0); - const [isPending, startTransition] = useTransition(); - const [searchString, setSearchString] = useState(''); - const debouncedSearchString = useDebouncedValue(searchString, 500); - - useEffect(() => { - const fetchData = async () => { - try { - const result = await search(debouncedSearchString); - setData(result.users); - setTotalPages(result.totalPages); - } catch (err) { - throw new Error(err); - } - }; - - fetchData(); - }, [debouncedSearchString, search]); - - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - startTransition(async () => { - const result = await search(debouncedSearchString); - setData(result.users); - }); - }; - - const handleChange = (e: React.ChangeEvent) => { - setSearchString(e.target.value); - }; - - return ( - <> -
- - -
-
- {data.length === 0 || isPending ? ( -
- -
- ) : ( - - )} -
- - ); -}; From 8f4ba6eb8aedd2cd30055893ca948e98622c02c0 Mon Sep 17 00:00:00 2001 From: pit Date: Tue, 10 Oct 2023 13:50:50 +0300 Subject: [PATCH 13/21] chore: self-review --- .../documents/data-table-action-button.tsx | 7 +-- .../documents/data-table-action-dropdown.tsx | 14 +---- .../admin/documents/data-table-title.tsx | 56 ------------------- .../admin/documents/data-table.tsx | 14 ++++- 4 files changed, 17 insertions(+), 74 deletions(-) delete mode 100644 apps/web/src/app/(dashboard)/admin/documents/data-table-title.tsx diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx index b1aa8efc5..a3fed63ec 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx @@ -5,14 +5,11 @@ import Link from 'next/link'; import { Edit } from 'lucide-react'; import { useSession } from 'next-auth/react'; -import { Document, Recipient, User } from '@documenso/prisma/client'; +import { Document } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; export type DataTableActionButtonProps = { - row: Document & { - User: Pick; - Recipient: Recipient[]; - }; + row: Pick; }; export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => { diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx index 4788033b1..84d7b063f 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx @@ -18,7 +18,6 @@ import { export type DataTableActionDropdownProps = { row: Document & { User: Pick; - Recipient: Recipient[]; }; }; @@ -29,7 +28,6 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = return null; } - const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); // const isRecipient = !!recipient; // const isDraft = row.status === DocumentStatus.DRAFT; // const isPending = row.status === DocumentStatus.PENDING; @@ -39,15 +37,9 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = const onDownloadClick = async () => { let document: DocumentWithData | null = null; - if (!recipient) { - document = await trpc.document.getDocumentById.query({ - id: row.id, - }); - } else { - document = await trpc.document.getDocumentByToken.query({ - token: recipient.token, - }); - } + document = await trpc.document.getDocumentById.query({ + id: row.id, + }); const documentData = document?.documentData; diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-title.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-title.tsx deleted file mode 100644 index c04f9f13d..000000000 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table-title.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'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)/admin/documents/data-table.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx index 1d121742a..cef49b3df 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx @@ -19,7 +19,6 @@ 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< @@ -60,7 +59,18 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { }, { header: 'Title', - cell: ({ row }) => , + accessorKey: 'title', + cell: ({ row }) => { + return ( + + {row.original.title} + + ); + }, }, { header: 'Owner', From 67629dd735dffc131f6243c81ddbb7ec54c5c03b Mon Sep 17 00:00:00 2001 From: pit Date: Tue, 10 Oct 2023 13:57:07 +0300 Subject: [PATCH 14/21] chore: fix eslint issues --- .../(dashboard)/admin/documents/data-table-action-dropdown.tsx | 2 +- apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx index 84d7b063f..21650ffc5 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx @@ -4,7 +4,7 @@ import { Copy, Download, History, MoreHorizontal, Trash2, XCircle } from 'lucide import { useSession } from 'next-auth/react'; import { getFile } from '@documenso/lib/universal/upload/get-file'; -import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client'; +import { Document, DocumentStatus, User } from '@documenso/prisma/client'; import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { trpc } from '@documenso/trpc/client'; import { diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx index 456fda7cd..737029a05 100644 --- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -6,6 +6,7 @@ import Link from 'next/link'; import { Edit, Loader } from 'lucide-react'; +import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { Document, Role, Subscription } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; @@ -14,8 +15,6 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat import { Input } from '@documenso/ui/primitives/input'; import { useToast } from '@documenso/ui/primitives/use-toast'; -import { useDebouncedValue } from '~/hooks/use-debounced-value'; - import { search } from './fetch-users.actions'; interface User { From 9e0d2818835da61312a78c0962be51aab81a6a90 Mon Sep 17 00:00:00 2001 From: pit Date: Tue, 10 Oct 2023 16:52:58 +0300 Subject: [PATCH 15/21] chore: feedback fix --- .../documents/data-table-action-button.tsx | 30 ------------------- .../documents/data-table-action-dropdown.tsx | 7 ----- .../admin/documents/data-table.tsx | 30 +++++++++++-------- 3 files changed, 17 insertions(+), 50 deletions(-) delete mode 100644 apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx deleted file mode 100644 index a3fed63ec..000000000 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-button.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import Link from 'next/link'; - -import { Edit } from 'lucide-react'; -import { useSession } from 'next-auth/react'; - -import { Document } from '@documenso/prisma/client'; -import { Button } from '@documenso/ui/primitives/button'; - -export type DataTableActionButtonProps = { - row: Pick; -}; - -export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => { - const { data: session } = useSession(); - - if (!session) { - return null; - } - - return ( - - ); -}; diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx index 21650ffc5..a4a4b5ee4 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx @@ -1,7 +1,6 @@ 'use client'; import { Copy, Download, History, MoreHorizontal, Trash2, XCircle } from 'lucide-react'; -import { useSession } from 'next-auth/react'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { Document, DocumentStatus, User } from '@documenso/prisma/client'; @@ -22,12 +21,6 @@ export type DataTableActionDropdownProps = { }; export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => { - const { data: session } = useSession(); - - if (!session) { - return null; - } - // const isRecipient = !!recipient; // const isDraft = row.status === DocumentStatus.DRAFT; // const isPending = row.status === DocumentStatus.PENDING; diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx index cef49b3df..8bda26ef4 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx @@ -4,33 +4,30 @@ import { useTransition } from 'react'; import Link from 'next/link'; -import { Loader } from 'lucide-react'; -import { useSession } from 'next-auth/react'; +import { Edit, 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 { Document, Recipient, User } from '@documenso/prisma/client'; +import { Document, User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; +import { Button } from '@documenso/ui/primitives/button'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; 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< Document & { - Recipient: Recipient[]; User: Pick; } >; }; export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { - const { data: session } = useSession(); const [isPending, startTransition] = useTransition(); const updateSearchParams = useUpdateSearchParams(); @@ -44,10 +41,6 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { }); }; - if (!session) { - return null; - } - return (
{ {row.original.title} @@ -90,13 +89,18 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { { header: 'Status', accessorKey: 'status', - cell: ({ row }) => , + cell: ({ row }) => , }, { header: 'Actions', cell: ({ row }) => (
- +
), From e02ab7d256863cf9bbc8e659be71d7641e4815ed Mon Sep 17 00:00:00 2001 From: pit Date: Wed, 11 Oct 2023 12:32:33 +0300 Subject: [PATCH 16/21] chore: implement pr feedback --- .../app/(dashboard)/admin/users/[id]/page.tsx | 129 ++++++++++-------- packages/lib/server-only/admin/update-user.ts | 3 +- .../lib/server-only/user/get-all-users.ts | 4 +- packages/trpc/server/admin-router/router.ts | 16 +-- packages/trpc/server/profile-router/router.ts | 36 ++--- packages/trpc/server/trpc.ts | 30 ++++ packages/ui/primitives/combobox.tsx | 66 +++++---- 7 files changed, 154 insertions(+), 130 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index 802f2ec0c..8c68f1270 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -3,21 +3,23 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader } from 'lucide-react'; -import { Controller, useForm } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { Combobox } from '@documenso/ui/primitives/combobox'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; -import { Label } from '@documenso/ui/primitives/label'; import { useToast } from '@documenso/ui/primitives/use-toast'; -import { FormErrorMessage } from '../../../../../components/form/form-error-message'; -import { - TUserFormSchema, - ZUserFormSchema, -} from '../../../../../providers/admin-user-profile-update.types'; +import { TUserFormSchema, ZUserFormSchema } from '~/providers/admin-user-profile-update.types'; export default function UserPage({ params }: { params: { id: number } }) { const { toast } = useToast(); @@ -34,21 +36,11 @@ export default function UserPage({ params }: { params: { id: number } }) { const user = result.data; - const roles = user?.roles; - let rolesArr: string[] = []; - - if (roles) { - rolesArr = Object.values(roles); - } + const roles = user?.roles ?? []; const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation(); - const { - register, - control, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ + const form = useForm({ resolver: zodResolver(ZUserFormSchema), values: { name: user?.name ?? '', @@ -85,42 +77,69 @@ export default function UserPage({ params }: { params: { id: number } }) { return (

Manage {user?.name}'s profile

-
-
- - - -
-
- - - -
-
- - ( - onChange(values)} /> - )} - /> - -
+ + +
+ ( + + + Name + + + + + + + )} + /> + ( + + + Email + + + + + + + )} + /> -
- -
- + ( + +
+ + Roles + + + onChange(values)} + /> + + +
+
+ )} + /> + +
+ +
+
+ +
); } diff --git a/packages/lib/server-only/admin/update-user.ts b/packages/lib/server-only/admin/update-user.ts index e4cc7b2bc..9013899a7 100644 --- a/packages/lib/server-only/admin/update-user.ts +++ b/packages/lib/server-only/admin/update-user.ts @@ -15,7 +15,7 @@ export const updateUser = async ({ id, name, email, roles }: UpdateUserOptions) }, }); - const updatedUser = await prisma.user.update({ + return await prisma.user.update({ where: { id, }, @@ -25,5 +25,4 @@ export const updateUser = async ({ id, name, email, roles }: UpdateUserOptions) roles, }, }); - return updatedUser; }; diff --git a/packages/lib/server-only/user/get-all-users.ts b/packages/lib/server-only/user/get-all-users.ts index babcc7ba1..a1ff2c929 100644 --- a/packages/lib/server-only/user/get-all-users.ts +++ b/packages/lib/server-only/user/get-all-users.ts @@ -2,7 +2,7 @@ import { Prisma } from '@prisma/client'; import { prisma } from '@documenso/prisma'; -type getAllUsersProps = { +type GetAllUsersProps = { username: string; email: string; page: number; @@ -14,7 +14,7 @@ export const findUsers = async ({ email = '', page = 1, perPage = 10, -}: getAllUsersProps) => { +}: GetAllUsersProps) => { const whereClause = Prisma.validator()({ OR: [ { diff --git a/packages/trpc/server/admin-router/router.ts b/packages/trpc/server/admin-router/router.ts index 67556a251..666e3f085 100644 --- a/packages/trpc/server/admin-router/router.ts +++ b/packages/trpc/server/admin-router/router.ts @@ -1,24 +1,14 @@ import { TRPCError } from '@trpc/server'; -import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; import { updateUser } from '@documenso/lib/server-only/admin/update-user'; -import { authenticatedProcedure, router } from '../trpc'; +import { adminProcedure, router } from '../trpc'; import { ZUpdateProfileMutationByAdminSchema } from './schema'; export const adminRouter = router({ - updateUser: authenticatedProcedure + updateUser: adminProcedure .input(ZUpdateProfileMutationByAdminSchema) - .mutation(async ({ input, ctx }) => { - const isUserAdmin = isAdmin(ctx.user); - - if (!isUserAdmin) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'Not authorized to perform this action.', - }); - } - + .mutation(async ({ input }) => { const { id, name, email, roles } = input; try { diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index 8d83528c0..0f6636650 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -1,13 +1,12 @@ import { TRPCError } from '@trpc/server'; -import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password'; import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id'; import { resetPassword } from '@documenso/lib/server-only/user/reset-password'; import { updatePassword } from '@documenso/lib/server-only/user/update-password'; import { updateProfile } from '@documenso/lib/server-only/user/update-profile'; -import { authenticatedProcedure, procedure, router } from '../trpc'; +import { adminProcedure, authenticatedProcedure, procedure, router } from '../trpc'; import { ZForgotPasswordFormSchema, ZResetPasswordFormSchema, @@ -17,29 +16,18 @@ import { } from './schema'; export const profileRouter = router({ - getUser: authenticatedProcedure - .input(ZRetrieveUserByIdQuerySchema) - .query(async ({ input, ctx }) => { - const isUserAdmin = isAdmin(ctx.user); + getUser: adminProcedure.input(ZRetrieveUserByIdQuerySchema).query(async ({ input }) => { + try { + const { id } = input; - if (!isUserAdmin) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'Not authorized to perform this action.', - }); - } - - try { - const { id } = input; - - return await getUserById({ id }); - } catch (err) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'We were unable to retrieve the specified account. Please try again.', - }); - } - }), + return await getUserById({ id }); + } catch (err) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to retrieve the specified account. Please try again.', + }); + } + }), updateProfile: authenticatedProcedure .input(ZUpdateProfileMutationSchema) diff --git a/packages/trpc/server/trpc.ts b/packages/trpc/server/trpc.ts index 91d2a239f..a382e3511 100644 --- a/packages/trpc/server/trpc.ts +++ b/packages/trpc/server/trpc.ts @@ -1,6 +1,8 @@ import { TRPCError, initTRPC } from '@trpc/server'; import SuperJSON from 'superjson'; +import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; + import { TrpcContext } from './context'; const t = initTRPC.context().create({ @@ -28,9 +30,37 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => { }); }); +export const adminMiddleware = t.middleware(async ({ ctx, next }) => { + if (!ctx.session || !ctx.user) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'You must be logged in to perform this action.', + }); + } + + const isUserAdmin = isAdmin(ctx.user); + + if (!isUserAdmin) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Not authorized to perform this action.', + }); + } + + return await next({ + ctx: { + ...ctx, + + user: ctx.user, + session: ctx.session, + }, + }); +}); + /** * Routers and Procedures */ export const router = t.router; export const procedure = t.procedure; export const authenticatedProcedure = t.procedure.use(authenticatedMiddleware); +export const adminProcedure = t.procedure.use(adminMiddleware); diff --git a/packages/ui/primitives/combobox.tsx b/packages/ui/primitives/combobox.tsx index 90fdc7849..899ccd61d 100644 --- a/packages/ui/primitives/combobox.tsx +++ b/packages/ui/primitives/combobox.tsx @@ -44,40 +44,38 @@ const Combobox = ({ listValues, onChange }: ComboboxProps) => { }; return ( - <> - - - - - - - - No value found. - - {allRoles.map((value: string, i: number) => ( - handleSelect(value)}> - - {value} - - ))} - - - - - + + + + + + + + No value found. + + {allRoles.map((value: string, i: number) => ( + handleSelect(value)}> + + {value} + + ))} + + + + ); }; From bc9a6fa50aa67124232c80bbe8a84178151f629c Mon Sep 17 00:00:00 2001 From: pit Date: Wed, 11 Oct 2023 16:20:04 +0300 Subject: [PATCH 17/21] chore: implemented feedback --- .../admin/users/data-table-users.tsx | 54 ++++++------------- .../src/app/(dashboard)/admin/users/page.tsx | 9 +++- 2 files changed, 22 insertions(+), 41 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx index 737029a05..b6b475ea2 100644 --- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -13,9 +13,6 @@ import { Button } from '@documenso/ui/primitives/button'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; import { Input } from '@documenso/ui/primitives/input'; -import { useToast } from '@documenso/ui/primitives/use-toast'; - -import { search } from './fetch-users.actions'; interface User { id: number; @@ -34,19 +31,27 @@ type SubscriptionLite = Pick< type DocumentLite = Pick; type UsersDataTableProps = { + users: User[]; + totalPages: number; perPage: number; page: number; }; -export const UsersDataTable = ({ perPage, page }: UsersDataTableProps) => { - const { toast } = useToast(); - +export const UsersDataTable = ({ users, totalPages, perPage, page }: UsersDataTableProps) => { const [isPending, startTransition] = useTransition(); const updateSearchParams = useUpdateSearchParams(); - const [data, setData] = useState([]); const [searchString, setSearchString] = useState(''); - const [totalPages, setTotalPages] = useState(0); - const debouncedSearchString = useDebouncedValue(searchString, 500); + const debouncedSearchString = useDebouncedValue(searchString, 1000); + + useEffect(() => { + startTransition(() => { + updateSearchParams({ + search: debouncedSearchString, + page: 1, + perPage, + }); + }); + }, [debouncedSearchString]); const onPaginationChange = (page: number, perPage: number) => { startTransition(() => { @@ -57,35 +62,6 @@ export const UsersDataTable = ({ perPage, page }: UsersDataTableProps) => { }); }; - useEffect(() => { - const fetchData = async () => { - try { - const result = await search(debouncedSearchString, page, perPage); - setData(result.users); - setTotalPages(result.totalPages); - - if (result.totalPages < page) { - startTransition(() => { - updateSearchParams({ - page: 1, - perPage, - }); - }); - } - } catch (err) { - throw new Error(err); - } - }; - - fetchData().catch(() => { - toast({ - title: 'Something went wrong', - description: 'Please try again', - variant: 'destructive', - }); - }); - }, [debouncedSearchString, page, perPage]); - const handleChange = (e: React.ChangeEvent) => { setSearchString(e.target.value); }; @@ -174,7 +150,7 @@ export const UsersDataTable = ({ perPage, page }: UsersDataTableProps) => { }, }, ]} - data={data} + data={users} perPage={perPage} currentPage={page} totalPages={totalPages} diff --git a/apps/web/src/app/(dashboard)/admin/users/page.tsx b/apps/web/src/app/(dashboard)/admin/users/page.tsx index b11d4a4b2..686ce7669 100644 --- a/apps/web/src/app/(dashboard)/admin/users/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/page.tsx @@ -1,20 +1,25 @@ import { UsersDataTable } from './data-table-users'; +import { search } from './fetch-users.actions'; type AdminManageUsersProps = { searchParams?: { + search?: string; page?: number; perPage?: number; }; }; -export default function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) { +export default async function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) { const page = Number(searchParams.page) || 1; const perPage = Number(searchParams.perPage) || 10; + const searchString = searchParams.search || ''; + + const { users, totalPages } = await search(searchString, page, perPage); return (

Manage users

- +
); } From 7927b87259ae7a5983e57e0cd4020dee1b0def2c Mon Sep 17 00:00:00 2001 From: pit Date: Thu, 12 Oct 2023 17:07:54 +0300 Subject: [PATCH 18/21] chore: polished code --- .../admin/documents/data-table.tsx | 40 ++++--------------- .../app/(dashboard)/admin/users/[id]/page.tsx | 12 ++---- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx index 8bda26ef4..3b098c2fd 100644 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx @@ -4,21 +4,18 @@ import { useTransition } from 'react'; import Link from 'next/link'; -import { Edit, Loader } from 'lucide-react'; +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 { Document, User } from '@documenso/prisma/client'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; -import { Button } from '@documenso/ui/primitives/button'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; import { DocumentStatus } from '~/components/formatter/document-status'; import { LocaleDate } from '~/components/formatter/locale-date'; -import { DataTableActionDropdown } from './data-table-action-dropdown'; - export type DocumentsDataTableProps = { results: FindResultSet< Document & { @@ -54,21 +51,7 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { header: 'Title', accessorKey: 'title', cell: ({ row }) => { - return ( - - {row.original.title} - - ); + return
{row.original.title}
; }, }, { @@ -86,25 +69,16 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { ); }, }, + { + header: 'Last updated', + accessorKey: 'updatedAt', + cell: ({ row }) => , + }, { header: 'Status', accessorKey: 'status', cell: ({ row }) => , }, - { - header: 'Actions', - cell: ({ row }) => ( -
- - -
- ), - }, ]} data={results.data} perPage={results.perPage} diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index 8c68f1270..e1535c16e 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -85,9 +85,7 @@ export default function UserPage({ params }: { params: { id: number } }) { name="name" render={({ field }) => ( - - Name - + Name @@ -100,9 +98,7 @@ export default function UserPage({ params }: { params: { id: number } }) { name="email" render={({ field }) => ( - - Email - + Email @@ -117,9 +113,7 @@ export default function UserPage({ params }: { params: { id: number } }) { render={({ field: { onChange } }) => (
- - Roles - + Roles Date: Fri, 13 Oct 2023 11:48:52 +0300 Subject: [PATCH 19/21] chore: implemented feedback --- .../documents/data-table-action-dropdown.tsx | 95 ------------------- .../server-only/admin/get-all-documents.ts | 7 +- .../lib/server-only/user/get-all-users.ts | 3 +- 3 files changed, 5 insertions(+), 100 deletions(-) delete mode 100644 apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx deleted file mode 100644 index a4a4b5ee4..000000000 --- a/apps/web/src/app/(dashboard)/admin/documents/data-table-action-dropdown.tsx +++ /dev/null @@ -1,95 +0,0 @@ -'use client'; - -import { Copy, Download, History, MoreHorizontal, Trash2, XCircle } from 'lucide-react'; - -import { getFile } from '@documenso/lib/universal/upload/get-file'; -import { Document, DocumentStatus, User } from '@documenso/prisma/client'; -import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; -import { trpc } from '@documenso/trpc/client'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from '@documenso/ui/primitives/dropdown-menu'; - -export type DataTableActionDropdownProps = { - row: Document & { - User: Pick; - }; -}; - -export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => { - // 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 = async () => { - let document: DocumentWithData | null = null; - - document = await trpc.document.getDocumentById.query({ - id: row.id, - }); - - const documentData = document?.documentData; - - if (!documentData) { - return; - } - - const documentBytes = await getFile(documentData); - - 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 ( - - - - - - - Action - - - - Download - - - - - Duplicate - - - - - Void - - - - - Delete - - - - - Resend - - - - ); -}; diff --git a/packages/lib/server-only/admin/get-all-documents.ts b/packages/lib/server-only/admin/get-all-documents.ts index 057b00afb..cca1935a3 100644 --- a/packages/lib/server-only/admin/get-all-documents.ts +++ b/packages/lib/server-only/admin/get-all-documents.ts @@ -1,4 +1,5 @@ import { prisma } from '@documenso/prisma'; +import { Prisma } from '@documenso/prisma/client'; export interface FindDocumentsOptions { term?: string; @@ -7,14 +8,14 @@ export interface FindDocumentsOptions { } export const findDocuments = async ({ term, page = 1, perPage = 10 }: FindDocumentsOptions) => { - const termFilters = !term + const termFilters: Prisma.DocumentWhereInput | undefined = !term ? undefined - : ({ + : { title: { contains: term, mode: 'insensitive', }, - } as const); + }; const [data, count] = await Promise.all([ prisma.document.findMany({ diff --git a/packages/lib/server-only/user/get-all-users.ts b/packages/lib/server-only/user/get-all-users.ts index a1ff2c929..f7d431fa3 100644 --- a/packages/lib/server-only/user/get-all-users.ts +++ b/packages/lib/server-only/user/get-all-users.ts @@ -1,6 +1,5 @@ -import { Prisma } from '@prisma/client'; - import { prisma } from '@documenso/prisma'; +import { Prisma } from '@documenso/prisma/client'; type GetAllUsersProps = { username: string; From 901e83af58809b2bc6a776a760d794fa1e481ff9 Mon Sep 17 00:00:00 2001 From: pit Date: Fri, 13 Oct 2023 12:16:07 +0300 Subject: [PATCH 20/21] chore: implemented feedback --- packages/lib/server-only/user/get-all-users.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/lib/server-only/user/get-all-users.ts b/packages/lib/server-only/user/get-all-users.ts index f7d431fa3..71e670e7d 100644 --- a/packages/lib/server-only/user/get-all-users.ts +++ b/packages/lib/server-only/user/get-all-users.ts @@ -33,21 +33,8 @@ export const findUsers = async ({ const [users, count] = await Promise.all([ await prisma.user.findMany({ - select: { - id: true, - name: true, - email: true, - roles: true, - Subscription: { - select: { - id: true, - status: true, - planId: true, - priceId: true, - createdAt: true, - periodEnd: true, - }, - }, + include: { + Subscription: true, Document: { select: { id: true, From a6e13faf7bb7c18ed7a38f918fe1c28c5f4cfa83 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 13 Oct 2023 13:08:39 +0000 Subject: [PATCH 21/21] fix: quick tweaks --- .../app/(dashboard)/admin/users/[id]/page.tsx | 4 +-- .../admin/users/data-table-users.tsx | 30 +++++-------------- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index e1535c16e..790177c8a 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -25,7 +25,7 @@ export default function UserPage({ params }: { params: { id: number } }) { const { toast } = useToast(); const router = useRouter(); - const result = trpc.profile.getUser.useQuery( + const { data: user } = trpc.profile.getUser.useQuery( { id: Number(params.id), }, @@ -34,8 +34,6 @@ export default function UserPage({ params }: { params: { id: number } }) { }, ); - const user = result.data; - const roles = user?.roles ?? []; const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation(); diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx index b6b475ea2..1840f5a44 100644 --- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -95,19 +95,7 @@ export const UsersDataTable = ({ users, totalPages, perPage, page }: UsersDataTa { header: 'Roles', accessorKey: 'roles', - cell: ({ row }) => { - return ( - <> - {row.original.roles.map((role: string, i: number) => { - return ( - - {role} {} - - ); - })} - - ); - }, + cell: ({ row }) => row.original.roles.join(', '), }, { header: 'Subscription', @@ -134,18 +122,16 @@ export const UsersDataTable = ({ users, totalPages, perPage, page }: UsersDataTa }, }, { - header: 'Edit', + header: '', accessorKey: 'edit', cell: ({ row }) => { return ( -
- -
+ ); }, },