From 8fd9730e2b0417bc486ed3f1de82c8758d78c198 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 7 Aug 2023 23:10:27 +1000 Subject: [PATCH 1/5] feat: add inbox --- apps/web/package.json | 2 + apps/web/src/app/(dashboard)/inbox/page.tsx | 14 + .../(dashboard)/inbox/inbox-content.tsx | 82 ++++ .../(dashboard)/inbox/inbox.actions.ts | 24 ++ .../components/(dashboard)/inbox/inbox.tsx | 352 ++++++++++++++++++ .../(dashboard)/inbox/inbox.utils.ts | 23 ++ .../(dashboard)/layout/desktop-nav.tsx | 22 +- apps/web/src/hooks/use-debounced-value.ts | 18 + apps/web/tailwind.config.js | 1 + package-lock.json | 2 + .../template-document-completed.tsx | 71 ++++ .../template-document-invite.tsx | 59 +++ .../template-document-pending.tsx | 52 +++ .../template-components/template-footer.tsx | 22 ++ .../email/templates/document-completed.tsx | 91 +---- packages/email/templates/document-invite.tsx | 73 ++-- packages/email/templates/document-pending.tsx | 66 +--- .../server-only/document/find-documents.ts | 111 +++++- packages/prisma/types/document.ts | 12 + packages/tailwind-config/index.cjs | 5 + .../trpc/server/document-router/router.ts | 28 ++ .../trpc/server/document-router/schema.ts | 13 + 22 files changed, 966 insertions(+), 177 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/inbox/page.tsx create mode 100644 apps/web/src/components/(dashboard)/inbox/inbox-content.tsx create mode 100644 apps/web/src/components/(dashboard)/inbox/inbox.actions.ts create mode 100644 apps/web/src/components/(dashboard)/inbox/inbox.tsx create mode 100644 apps/web/src/components/(dashboard)/inbox/inbox.utils.ts create mode 100644 apps/web/src/hooks/use-debounced-value.ts create mode 100644 packages/email/template-components/template-document-completed.tsx create mode 100644 packages/email/template-components/template-document-invite.tsx create mode 100644 packages/email/template-components/template-document-pending.tsx create mode 100644 packages/email/template-components/template-footer.tsx create mode 100644 packages/prisma/types/document.ts diff --git a/apps/web/package.json b/apps/web/package.json index c0a3035ea..22a75128f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,6 +21,7 @@ "formidable": "^2.1.1", "framer-motion": "^10.12.8", "lucide-react": "^0.214.0", + "luxon": "^3.4.0", "micro": "^10.0.1", "nanoid": "^4.0.2", "next": "13.4.12", @@ -43,6 +44,7 @@ }, "devDependencies": { "@types/formidable": "^2.0.6", + "@types/luxon": "^3.3.1", "@types/node": "20.1.0", "@types/react": "18.2.18", "@types/react-dom": "18.2.7" diff --git a/apps/web/src/app/(dashboard)/inbox/page.tsx b/apps/web/src/app/(dashboard)/inbox/page.tsx new file mode 100644 index 000000000..badb421c9 --- /dev/null +++ b/apps/web/src/app/(dashboard)/inbox/page.tsx @@ -0,0 +1,14 @@ +import Inbox from '~/components/(dashboard)/inbox/inbox'; + +export default function InboxPage() { + return ( +
+

Inbox

+

Documents which you have been requested to sign.

+ +
+ +
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/inbox/inbox-content.tsx b/apps/web/src/components/(dashboard)/inbox/inbox-content.tsx new file mode 100644 index 000000000..f7e263f1f --- /dev/null +++ b/apps/web/src/components/(dashboard)/inbox/inbox-content.tsx @@ -0,0 +1,82 @@ +import { TemplateDocumentCompleted } from '@documenso/email/template-components/template-document-completed'; +import { TemplateDocumentInvite } from '@documenso/email/template-components/template-document-invite'; +import { DocumentWithRecipientAndSender } from '@documenso/prisma/types/document'; +import { cn } from '@documenso/ui/lib/utils'; + +import { formatInboxDate } from './inbox.utils'; + +export type InboxContentProps = { + document: DocumentWithRecipientAndSender; +}; + +export default function InboxContent({ document }: InboxContentProps) { + const inboxDocumentStatusIndicator = ( + +
+ + {document.recipient.signingStatus === 'SIGNED' ? 'Signed' : 'Pending'} +
+ ); + + return ( +
+
+
+

{document.subject}

+

+ {document.sender.name} <{document.sender.email}> +

+
+ +
+ {/* Todo: This needs to be updated to when the document was sent to the recipient when that value is available. */} +

{formatInboxDate(document.created)}

+ + {inboxDocumentStatusIndicator} +
+
+ +
+ {inboxDocumentStatusIndicator} +
+ + {/* Todo: get correct URLs */} +
+ {document.recipient.signingStatus === 'NOT_SIGNED' && ( + + )} + + {document.recipient.signingStatus === 'SIGNED' && ( + + )} +
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/inbox/inbox.actions.ts b/apps/web/src/components/(dashboard)/inbox/inbox.actions.ts new file mode 100644 index 000000000..38b50a8b3 --- /dev/null +++ b/apps/web/src/components/(dashboard)/inbox/inbox.actions.ts @@ -0,0 +1,24 @@ +'use server'; + +import { z } from 'zod'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { prisma } from '@documenso/prisma'; + +export async function updateRecipientReadStatus(recipientId: number, documentId: number) { + z.number().parse(recipientId); + z.number().parse(documentId); + + const { email } = await getRequiredServerComponentSession(); + + await prisma.recipient.update({ + where: { + id: recipientId, + documentId, + email, + }, + data: { + readStatus: 'OPENED', + }, + }); +} diff --git a/apps/web/src/components/(dashboard)/inbox/inbox.tsx b/apps/web/src/components/(dashboard)/inbox/inbox.tsx new file mode 100644 index 000000000..c76ba3d94 --- /dev/null +++ b/apps/web/src/components/(dashboard)/inbox/inbox.tsx @@ -0,0 +1,352 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +import { Inbox as InboxIcon } from 'lucide-react'; +import { z } from 'zod'; + +import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; +import { SigningStatus } from '@documenso/prisma/client'; +import { DocumentWithRecipientAndSender } from '@documenso/prisma/types/document'; +import { trpc } from '@documenso/trpc/react'; +import { cn } from '@documenso/ui/lib/utils'; +import { Input } from '@documenso/ui/primitives/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; +import { Skeleton } from '@documenso/ui/primitives/skeleton'; + +import { useDebouncedValue } from '~/hooks/use-debounced-value'; + +import InboxContent from './inbox-content'; +import { updateRecipientReadStatus } from './inbox.actions'; +import { formatInboxDate } from './inbox.utils'; + +export const ZInboxSearchParamsSchema = z.object({ + filter: z + .union([z.literal('SIGNED'), z.literal('NOT_SIGNED'), z.undefined()]) + .catch(() => undefined), + id: z + .string() + .optional() + .catch(() => undefined), + query: z + .string() + .optional() + .catch(() => undefined), +}); + +export type InboxProps = { + className?: string; +}; + +const numberOfSkeletons = 3; + +export default function Inbox(props: InboxProps) { + const { className } = props; + + const pathname = usePathname(); + const searchParams = useSearchParams(); + const router = useRouter(); + const updateSearchParams = useUpdateSearchParams(); + + const parsedSearchParams = ZInboxSearchParamsSchema.parse(Object.fromEntries(searchParams ?? [])); + + const [searchQuery, setSearchQuery] = useState(() => parsedSearchParams.query || ''); + + const [readStatusState, setReadStatusState] = useState<{ + [documentId: string]: 'ERROR' | 'UPDATED' | 'UPDATING'; + }>({}); + + const [isInitialLoad, setIsInitialLoad] = useState(true); + + const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); + + const { + data, + error, + fetchNextPage, + fetchPreviousPage, + hasNextPage, + hasPreviousPage, + isFetching, + isFetchingNextPage, + isFetchingPreviousPage, + refetch, + } = trpc.document.searchInboxDocuments.useInfiniteQuery( + { + query: parsedSearchParams.query, + filter: parsedSearchParams.filter, + }, + { + getPreviousPageParam: (firstPage) => + firstPage.currentPage > 1 ? firstPage.currentPage - 1 : undefined, + getNextPageParam: (lastPage) => + lastPage.currentPage < lastPage.totalPages ? lastPage.currentPage + 1 : undefined, + }, + ); + + /** + * The current documents in the inbox after filters and queries have been applied. + */ + const inboxDocuments = (data?.pages ?? []).flatMap((page) => page.data); + + /** + * The currently selected document in the inbox. + */ + const selectedDocument: DocumentWithRecipientAndSender | null = + inboxDocuments.find((item) => item.id.toString() === parsedSearchParams.id) ?? null; + + /** + * Remove the ID from the query if it is not found in the result. + */ + useEffect(() => { + if (!selectedDocument && parsedSearchParams.id && data) { + updateSearchParams({ + id: null, + }); + } + }, [data, selectedDocument, parsedSearchParams.id]); + + /** + * Handle debouncing the seach query. + */ + useEffect(() => { + if (!pathname) { + return; + } + + const params = new URLSearchParams(searchParams?.toString()); + + params.set('query', debouncedSearchQuery); + + if (debouncedSearchQuery === '') { + params.delete('query'); + } + + router.push(`${pathname}?${params.toString()}`); + }, [debouncedSearchQuery]); + + useEffect(() => { + if (!isFetching) { + setIsInitialLoad(false); + } + }, [isFetching]); + + const updateReadStatusState = (documentId: number, value: (typeof readStatusState)[string]) => { + setReadStatusState({ + ...readStatusState, + [documentId]: value, + }); + }; + + /** + * Handle selecting the selected document to display and updating the read status if required. + */ + const onSelectedDocumentChange = (value: DocumentWithRecipientAndSender) => { + if (!pathname) { + return; + } + + // Update the read status. + if ( + value.recipient.readStatus === 'NOT_OPENED' && + readStatusState[value.id] !== 'UPDATED' && + readStatusState[value.id] !== 'UPDATING' + ) { + updateReadStatusState(value.id, 'UPDATING'); + + updateRecipientReadStatus(value.recipient.id, value.id) + .then(() => { + updateReadStatusState(value.id, 'UPDATED'); + }) + .catch(() => { + updateReadStatusState(value.id, 'ERROR'); + }); + } + + const params = new URLSearchParams(searchParams?.toString()); + + params.set('id', value.id.toString()); + + router.push(`${pathname}?${params.toString()}`); + }; + + if (error) { + return ( +
+

Something went wrong while loading your inbox.

+ +
+ ); + } + + return ( +
+
+ {/* Header with search and filter options. */} +
+ setSearchQuery(e.target.value)} + /> + +
+ +
+
+ +
+ {/* Handle rendering no items found. */} + {!isFetching && inboxDocuments.length === 0 && ( +
+

No documents found.

+
+ )} + + {hasPreviousPage && !isFetchingPreviousPage && ( + + )} + +
    + {/* Handle rendering skeleton on first load. */} + {isFetching && + isInitialLoad && + !data && + Array.from({ length: numberOfSkeletons }).map((_, i) => ( +
  • + + +
    +
    + + +
    + + + + +
    +
  • + ))} + + {/* Handle rendering list of inbox documents. */} + {inboxDocuments.map((item, i) => ( +
  • + + + {/* Mobile inbox content. */} + {selectedDocument?.id === item.id && ( +
    + +
    + )} +
  • + ))} +
+ + {hasNextPage && !isFetchingNextPage && ( + + )} +
+
+ + {/* Desktop inbox content. */} +
+ {selectedDocument ? ( + + ) : ( +
+ +
+ )} +
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/inbox/inbox.utils.ts b/apps/web/src/components/(dashboard)/inbox/inbox.utils.ts new file mode 100644 index 000000000..9a6aff9f3 --- /dev/null +++ b/apps/web/src/components/(dashboard)/inbox/inbox.utils.ts @@ -0,0 +1,23 @@ +import { DateTime } from 'luxon'; + +/** + * Format the provided date into a readable string for inboxes. + * + * @param dateValue The date or date string + * @returns The date in the current locale, or the date formatted as HH:MM AM/PM if the provided date is after 12:00AM of the current date + */ +export const formatInboxDate = (dateValue: string | Date): string => { + const date = + typeof dateValue === 'string' ? DateTime.fromISO(dateValue) : DateTime.fromJSDate(dateValue); + + const startOfTheDay = DateTime.now().startOf('day'); + + if (date >= startOfTheDay) { + return date.toFormat('h:mma'); + } + + return date.toLocaleString({ + ...DateTime.DATE_SHORT, + year: '2-digit', + }); +}; diff --git a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx index 9b64baf58..e4ce72738 100644 --- a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx +++ b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx @@ -2,15 +2,19 @@ import { HTMLAttributes } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + import { cn } from '@documenso/ui/lib/utils'; export type DesktopNavProps = HTMLAttributes; export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { + const pathname = usePathname(); + return ( ); }; diff --git a/apps/web/src/hooks/use-debounced-value.ts b/apps/web/src/hooks/use-debounced-value.ts new file mode 100644 index 000000000..3c57f4aa0 --- /dev/null +++ b/apps/web/src/hooks/use-debounced-value.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from 'react'; + +export function useDebouncedValue(value: T, delay: number) { + // State and setters for debounced value + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index c7f6f1a19..fcc9f7032 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -7,5 +7,6 @@ module.exports = { content: [ ...baseConfig.content, `${path.join(require.resolve('@documenso/ui'), '..')}/**/*.{ts,tsx}`, + `${path.join(require.resolve('@documenso/email'), '..')}/**/*.{ts,tsx}`, ], }; diff --git a/package-lock.json b/package-lock.json index 8f3b7d79f..e220eff53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,6 +78,7 @@ "formidable": "^2.1.1", "framer-motion": "^10.12.8", "lucide-react": "^0.214.0", + "luxon": "^3.4.0", "micro": "^10.0.1", "nanoid": "^4.0.2", "next": "13.4.12", @@ -100,6 +101,7 @@ }, "devDependencies": { "@types/formidable": "^2.0.6", + "@types/luxon": "^3.3.1", "@types/node": "20.1.0", "@types/react": "18.2.18", "@types/react-dom": "18.2.7" diff --git a/packages/email/template-components/template-document-completed.tsx b/packages/email/template-components/template-document-completed.tsx new file mode 100644 index 000000000..b64b13cff --- /dev/null +++ b/packages/email/template-components/template-document-completed.tsx @@ -0,0 +1,71 @@ +import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; + +import * as config from '@documenso/tailwind-config'; + +export interface TemplateDocumentCompletedProps { + downloadLink: string; + reviewLink: string; + documentName: string; + assetBaseUrl: string; +} + +export const TemplateDocumentCompleted = ({ + downloadLink, + reviewLink, + documentName, + assetBaseUrl, +}: TemplateDocumentCompletedProps) => { + const getAssetUrl = (path: string) => { + return new URL(path, assetBaseUrl).toString(); + }; + + return ( + +
+
+ Documenso +
+ + + + Completed + + + + “{documentName}” was signed by all signers + + + + Continue by downloading or reviewing the document. + + +
+ + +
+
+
+ ); +}; + +export default TemplateDocumentCompleted; diff --git a/packages/email/template-components/template-document-invite.tsx b/packages/email/template-components/template-document-invite.tsx new file mode 100644 index 000000000..bf2fb905e --- /dev/null +++ b/packages/email/template-components/template-document-invite.tsx @@ -0,0 +1,59 @@ +import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; + +import * as config from '@documenso/tailwind-config'; + +export interface TemplateDocumentInviteProps { + inviterName: string; + inviterEmail: string; + documentName: string; + signDocumentLink: string; + assetBaseUrl: string; +} + +export const TemplateDocumentInvite = ({ + inviterName, + documentName, + signDocumentLink, + assetBaseUrl, +}: TemplateDocumentInviteProps) => { + const getAssetUrl = (path: string) => { + return new URL(path, assetBaseUrl).toString(); + }; + + return ( + +
+
+ Documenso +
+ + + {inviterName} has invited you to sign "{documentName}" + + + + Continue by signing the document. + + +
+ +
+
+
+ ); +}; + +export default TemplateDocumentInvite; diff --git a/packages/email/template-components/template-document-pending.tsx b/packages/email/template-components/template-document-pending.tsx new file mode 100644 index 000000000..80387b783 --- /dev/null +++ b/packages/email/template-components/template-document-pending.tsx @@ -0,0 +1,52 @@ +import { Img, Section, Tailwind, Text } from '@react-email/components'; + +import * as config from '@documenso/tailwind-config'; + +export interface TemplateDocumentPendingProps { + documentName: string; + assetBaseUrl: string; +} + +export const TemplateDocumentPending = ({ + documentName, + assetBaseUrl, +}: TemplateDocumentPendingProps) => { + const getAssetUrl = (path: string) => { + return new URL(path, assetBaseUrl).toString(); + }; + + return ( + +
+
+ Documenso +
+ + + + Waiting for others + + + + “{documentName}” has been signed + + + + We're still waiting for other signers to sign this document. +
+ We'll notify you as soon as it's ready. +
+
+
+ ); +}; + +export default TemplateDocumentPending; diff --git a/packages/email/template-components/template-footer.tsx b/packages/email/template-components/template-footer.tsx new file mode 100644 index 000000000..ee395a1e9 --- /dev/null +++ b/packages/email/template-components/template-footer.tsx @@ -0,0 +1,22 @@ +import { Link, Section, Text } from '@react-email/components'; + +export const TemplateFooter = () => { + return ( +
+ + This document was sent using{' '} + + Documenso. + + + + + Documenso +
+ 2261 Market Street, #5211, San Francisco, CA 94114, USA +
+
+ ); +}; + +export default TemplateFooter; diff --git a/packages/email/templates/document-completed.tsx b/packages/email/templates/document-completed.tsx index 0d4f70601..9152d5822 100644 --- a/packages/email/templates/document-completed.tsx +++ b/packages/email/templates/document-completed.tsx @@ -1,25 +1,23 @@ import { Body, - Button, Container, Head, Html, Img, - Link, Preview, Section, Tailwind, - Text, } from '@react-email/components'; import config from '@documenso/tailwind-config'; -interface DocumentCompletedEmailTemplateProps { - downloadLink?: string; - reviewLink?: string; - documentName?: string; - assetBaseUrl?: string; -} +import { + TemplateDocumentCompleted, + TemplateDocumentCompletedProps, +} from '../template-components/template-document-completed'; +import TemplateFooter from '../template-components/template-footer'; + +export type DocumentCompletedEmailTemplateProps = Partial; export const DocumentCompletedEmailTemplate = ({ downloadLink = 'https://documenso.com', @@ -50,74 +48,23 @@ export const DocumentCompletedEmailTemplate = ({
- Documenso Logo + Documenso Logo -
-
- Documenso -
- - - - Completed - - - - “{documentName}” was signed by all signers - - - - Continue by downloading or reviewing the document. - - -
- - -
-
+
-
- - This document was sent using{' '} - - Documenso. - - - - - Documenso -
- 2261 Market Street, #5211, San Francisco, CA 94114, USA -
-
+
diff --git a/packages/email/templates/document-invite.tsx b/packages/email/templates/document-invite.tsx index 90a32b7bf..465685649 100644 --- a/packages/email/templates/document-invite.tsx +++ b/packages/email/templates/document-invite.tsx @@ -1,6 +1,5 @@ import { Body, - Button, Container, Head, Hr, @@ -15,13 +14,13 @@ import { import config from '@documenso/tailwind-config'; -interface DocumentInviteEmailTemplateProps { - inviterName?: string; - inviterEmail?: string; - documentName?: string; - signDocumentLink?: string; - assetBaseUrl?: string; -} +import { + TemplateDocumentInvite, + TemplateDocumentInviteProps, +} from '../template-components/template-document-invite'; +import TemplateFooter from '../template-components/template-footer'; + +export type DocumentInviteEmailTemplateProps = Partial; export const DocumentInviteEmailTemplate = ({ inviterName = 'Lucas Smith', @@ -51,36 +50,21 @@ export const DocumentInviteEmailTemplate = ({ >
- -
- Documenso Logo + +
+ Documenso Logo -
-
- Documenso -
- - - {inviterName} has invited you to sign "{documentName}" - - - - Continue by signing the document. - - -
- -
-
+
@@ -102,20 +86,7 @@ export const DocumentInviteEmailTemplate = ({
-
- - This document was sent using{' '} - - Documenso. - - - - - Documenso -
- 2261 Market Street, #5211, San Francisco, CA 94114, USA -
-
+
diff --git a/packages/email/templates/document-pending.tsx b/packages/email/templates/document-pending.tsx index 03a69554f..0ed768747 100644 --- a/packages/email/templates/document-pending.tsx +++ b/packages/email/templates/document-pending.tsx @@ -4,19 +4,20 @@ import { Head, Html, Img, - Link, Preview, Section, Tailwind, - Text, } from '@react-email/components'; import config from '@documenso/tailwind-config'; -interface DocumentPendingEmailTemplateProps { - documentName?: string; - assetBaseUrl?: string; -} +import { + TemplateDocumentPending, + TemplateDocumentPendingProps, +} from '../template-components/template-document-pending'; +import { TemplateFooter } from '../template-components/template-footer'; + +export type DocumentPendingEmailTemplateProps = Partial; export const DocumentPendingEmailTemplate = ({ documentName = 'Open Source Pledge.pdf', @@ -43,55 +44,20 @@ export const DocumentPendingEmailTemplate = ({ >
- -
- Documenso Logo + +
+ Documenso Logo -
-
- Documenso -
- - - - Waiting for others - - - - “{documentName}” has been signed - - - - We're still waiting for other signers to sign this document. -
- We'll notify you as soon as it's ready. -
-
+
-
- - This document was sent using{' '} - - Documenso. - - - - - Documenso -
- 2261 Market Street, #5211, San Francisco, CA 94114, USA -
-
+
diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 41e9c858a..4b209f611 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -1,5 +1,6 @@ import { prisma } from '@documenso/prisma'; -import { Document, DocumentStatus, Prisma } from '@documenso/prisma/client'; +import { Document, DocumentStatus, Prisma, SigningStatus } from '@documenso/prisma/client'; +import { DocumentWithRecipientAndSender } from '@documenso/prisma/types/document'; import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient'; import { FindResultSet } from '../../types/find-result-set'; @@ -68,3 +69,111 @@ export const findDocuments = async ({ totalPages: Math.ceil(count / perPage), }; }; + +export interface FindDocumentsWithRecipientAndSenderOptions { + email: string; + query?: string; + signingStatus?: SigningStatus; + page?: number; + perPage?: number; + orderBy?: { + column: keyof Omit; + direction: 'asc' | 'desc'; + }; +} + +export const findDocumentsWithRecipientAndSender = async ({ + email, + query, + signingStatus, + page = 1, + perPage = 20, + orderBy, +}: FindDocumentsWithRecipientAndSenderOptions): Promise< + FindResultSet +> => { + const orderByColumn = orderBy?.column ?? 'created'; + const orderByDirection = orderBy?.direction ?? 'desc'; + + const filters: Prisma.DocumentWhereInput = { + Recipient: { + some: { + email, + signingStatus, + }, + }, + }; + + if (query) { + filters.OR = [ + { + User: { + email: { + contains: query, + mode: 'insensitive', + }, + }, + // Todo: Add filter for `Subject`. + }, + ]; + } + + const [data, count] = await Promise.all([ + prisma.document.findMany({ + select: { + id: true, + created: true, + title: true, + status: true, + userId: true, + User: { + select: { + id: true, + name: true, + email: true, + }, + }, + Recipient: { + where: { + email, + signingStatus, + }, + }, + }, + where: { + ...filters, + }, + skip: Math.max(page - 1, 0) * perPage, + take: perPage, + orderBy: { + [orderByColumn]: orderByDirection, + }, + }), + prisma.document.count({ + where: { + ...filters, + }, + }), + ]); + + return { + data: data.map((item) => { + const { User, Recipient, ...rest } = item; + + const subject = undefined; // Todo. + const description = undefined; // Todo. + + return { + ...rest, + sender: User, + recipient: Recipient[0], + subject: subject ?? 'Please sign this document', + description: description ?? `${User.name} has invited you to sign "${item.title}"`, + }; + }), + count, + currentPage: Math.max(page, 1), + perPage, + totalPages: Math.ceil(count / perPage), + }; +}; diff --git a/packages/prisma/types/document.ts b/packages/prisma/types/document.ts new file mode 100644 index 000000000..35a6a33b5 --- /dev/null +++ b/packages/prisma/types/document.ts @@ -0,0 +1,12 @@ +import { Document, Recipient } from '@documenso/prisma/client'; + +export type DocumentWithRecipientAndSender = Omit & { + recipient: Recipient; + sender: { + id: number; + name: string | null; + email: string; + }; + subject: string; + description: string; +}; diff --git a/packages/tailwind-config/index.cjs b/packages/tailwind-config/index.cjs index d2892f69f..1564454d8 100644 --- a/packages/tailwind-config/index.cjs +++ b/packages/tailwind-config/index.cjs @@ -115,6 +115,11 @@ module.exports = { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out', }, + screens: { + '3xl': '1920px', + '4xl': '2560px', + '5xl': '3840px', + }, }, }, plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')], diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index f20643327..1f790dc24 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -1,17 +1,45 @@ import { TRPCError } from '@trpc/server'; +import { findDocumentsWithRecipientAndSender } from '@documenso/lib/server-only/document/find-documents'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; import { authenticatedProcedure, router } from '../trpc'; import { + ZSearchInboxDocumentsParamsSchema, ZSendDocumentMutationSchema, ZSetFieldsForDocumentMutationSchema, ZSetRecipientsForDocumentMutationSchema, } from './schema'; export const documentRouter = router({ + searchInboxDocuments: authenticatedProcedure + .input(ZSearchInboxDocumentsParamsSchema) + .query(async ({ input, ctx }) => { + try { + const { filter, query, cursor: page } = input; + + return await findDocumentsWithRecipientAndSender({ + email: ctx.session.email, + query, + signingStatus: filter, + orderBy: { + column: 'created', + direction: 'desc', + }, + page, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Something went wrong. Please try again later.', + }); + } + }), + setRecipientsForDocument: authenticatedProcedure .input(ZSetRecipientsForDocumentMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 18c3a93ae..8ea107de3 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -2,6 +2,19 @@ import { z } from 'zod'; import { FieldType } from '@documenso/prisma/client'; +export const ZSearchInboxDocumentsParamsSchema = z.object({ + filter: z + .union([z.literal('SIGNED'), z.literal('NOT_SIGNED'), z.undefined()]) + .catch(() => undefined), + cursor: z.number().default(1), + query: z + .string() + .optional() + .catch(() => undefined), +}); + +export type TSearchInboxDocumentsParamsSchema = z.infer; + export const ZSetRecipientsForDocumentMutationSchema = z.object({ documentId: z.number(), recipients: z.array( From 1f8d5e45e156aac12e518acb55ff429c73abd3f2 Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 24 Aug 2023 16:50:40 +1000 Subject: [PATCH 2/5] feat: onepage inbox --- .../app/(dashboard)/documents/data-table.tsx | 72 +++++++++++- .../src/app/(dashboard)/documents/page.tsx | 81 +++++-------- .../components/formatter/document-status.tsx | 30 +++-- .../server-only/document/find-documents.ts | 110 +++++++++++++++--- .../lib/server-only/document/get-stats.ts | 92 ++++++++++++--- .../guards/is-extended-document-status.ts | 11 ++ .../prisma/types/extended-document-status.ts | 10 ++ packages/ui/icons/signature.tsx | 28 +++++ 8 files changed, 341 insertions(+), 93 deletions(-) create mode 100644 packages/prisma/guards/is-extended-document-status.ts create mode 100644 packages/prisma/types/extended-document-status.ts create mode 100644 packages/ui/icons/signature.tsx diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index 35fdfb4b1..012038f4e 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -4,13 +4,32 @@ import { useTransition } from 'react'; import Link from 'next/link'; -import { Loader } from 'lucide-react'; +import { + Copy, + Download, + Edit, + History, + Loader, + MoreHorizontal, + Pencil, + Share, + Trash2, + XCircle, +} from 'lucide-react'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { FindResultSet } from '@documenso/lib/types/find-result-set'; import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient'; +import { Button } from '@documenso/ui/primitives/button'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from '@documenso/ui/primitives/dropdown-menu'; import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; import { DocumentStatus } from '~/components/formatter/document-status'; @@ -67,6 +86,57 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { accessorKey: 'created', cell: ({ row }) => , }, + { + header: 'Actions', + cell: ({ row: _row }) => ( +
+ + + + + + + + Action + + + Sign + + + + Edit + + + + Download + + + + Duplicate + + + + Void + + + + Delete + + + Share + + + Resend + + + + Share + + + +
+ ), + }, ]} data={results.data} perPage={results.perPage} diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx index 76675f573..4ea55936b 100644 --- a/apps/web/src/app/(dashboard)/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/page.tsx @@ -3,8 +3,8 @@ import Link from 'next/link'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; import { getStats } from '@documenso/lib/server-only/document/get-stats'; -import { isDocumentStatus } from '@documenso/lib/types/is-document-status'; -import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client'; +import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector'; @@ -16,7 +16,7 @@ import { DocumentsDataTable } from './data-table'; export type DocumentsPageProps = { searchParams?: { - status?: InternalDocumentStatus | 'ALL'; + status?: ExtendedDocumentStatus; period?: PeriodSelectorValue; page?: string; perPage?: string; @@ -24,22 +24,20 @@ export type DocumentsPageProps = { }; export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) { - const session = await getRequiredServerComponentSession(); + const user = await getRequiredServerComponentSession(); const stats = await getStats({ - userId: session.id, + user, }); - const status = isDocumentStatus(searchParams.status) ? searchParams.status : 'ALL'; + const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL'; // const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : ''; const page = Number(searchParams.page) || 1; const perPage = Number(searchParams.perPage) || 20; - const shouldDefaultToPending = status === 'ALL' && stats.PENDING > 0; - const results = await findDocuments({ - userId: session.id, - status: status === 'ALL' ? undefined : status, + userId: user.id, + status, orderBy: { column: 'created', direction: 'desc', @@ -57,10 +55,6 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage params.delete('page'); } - if (value === 'ALL') { - params.delete('status'); - } - return `/documents?${params.toString()}`; }; @@ -70,47 +64,28 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage

Documents

-
- +
+ - - - + {[ + ExtendedDocumentStatus.INBOX, + ExtendedDocumentStatus.PENDING, + ExtendedDocumentStatus.COMPLETED, + ExtendedDocumentStatus.DRAFT, + ExtendedDocumentStatus.ALL, + ].map((value) => ( + + + - - {Math.min(stats.PENDING, 99)} - - - - - - - - - - {Math.min(stats.COMPLETED, 99)} - - - - - - - - - - {Math.min(stats.DRAFT, 99)} - - - - - - - All - - + {value !== ExtendedDocumentStatus.ALL && ( + + {Math.min(stats[value], 99)} + + )} + + + ))} diff --git a/apps/web/src/components/formatter/document-status.tsx b/apps/web/src/components/formatter/document-status.tsx index 4e1ccf742..126a52f4f 100644 --- a/apps/web/src/components/formatter/document-status.tsx +++ b/apps/web/src/components/formatter/document-status.tsx @@ -3,16 +3,17 @@ import { HTMLAttributes } from 'react'; import { CheckCircle2, Clock, File } from 'lucide-react'; import type { LucideIcon } from 'lucide-react/dist/lucide-react'; -import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; +import { SignatureIcon } from '@documenso/ui/icons/signature'; import { cn } from '@documenso/ui/lib/utils'; type FriendlyStatus = { label: string; - icon: LucideIcon; + icon?: LucideIcon; color: string; }; -const FRIENDLY_STATUS_MAP: Record = { +const FRIENDLY_STATUS_MAP: Record = { PENDING: { label: 'Pending', icon: Clock, @@ -28,10 +29,19 @@ const FRIENDLY_STATUS_MAP: Record = { icon: File, color: 'text-yellow-500', }, + INBOX: { + label: 'Inbox', + icon: SignatureIcon, + color: 'text-muted-foreground', + }, + ALL: { + label: 'All', + color: 'text-muted-foreground', + }, }; export type DocumentStatusProps = HTMLAttributes & { - status: InternalDocumentStatus; + status: ExtendedDocumentStatus; inheritColor?: boolean; }; @@ -45,11 +55,13 @@ export const DocumentStatus = ({ return ( - + {Icon && ( + + )} {label} ); diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 4b209f611..93b1e2089 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -1,14 +1,17 @@ +import { match } from 'ts-pattern'; + import { prisma } from '@documenso/prisma'; -import { Document, DocumentStatus, Prisma, SigningStatus } from '@documenso/prisma/client'; +import { Document, Prisma, SigningStatus } from '@documenso/prisma/client'; import { DocumentWithRecipientAndSender } from '@documenso/prisma/types/document'; import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { FindResultSet } from '../../types/find-result-set'; export interface FindDocumentsOptions { userId: number; term?: string; - status?: DocumentStatus; + status?: ExtendedDocumentStatus; page?: number; perPage?: number; orderBy?: { @@ -20,29 +23,102 @@ export interface FindDocumentsOptions { export const findDocuments = async ({ userId, term, - status, + status = ExtendedDocumentStatus.ALL, page = 1, perPage = 10, orderBy, }: FindDocumentsOptions): Promise> => { + const user = await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + }); + const orderByColumn = orderBy?.column ?? 'created'; const orderByDirection = orderBy?.direction ?? 'desc'; - const filters: Prisma.DocumentWhereInput = { - status, - userId, - }; + const termFilters = !term + ? undefined + : ({ + title: { + contains: term, + mode: 'insensitive', + }, + } as const); - if (term) { - filters.title = { - contains: term, - mode: 'insensitive', - }; - } + const filters = match(status) + .with(ExtendedDocumentStatus.ALL, () => ({ + OR: [ + { + userId, + }, + { + status: { + not: ExtendedDocumentStatus.DRAFT, + }, + Recipient: { + some: { + email: user.email, + }, + }, + }, + ], + })) + .with(ExtendedDocumentStatus.INBOX, () => ({ + status: { + not: ExtendedDocumentStatus.DRAFT, + }, + Recipient: { + some: { + email: user.email, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }, + })) + .with(ExtendedDocumentStatus.DRAFT, () => ({ + userId, + status: ExtendedDocumentStatus.DRAFT, + })) + .with(ExtendedDocumentStatus.PENDING, () => ({ + OR: [ + { + userId, + status: ExtendedDocumentStatus.PENDING, + }, + { + status: ExtendedDocumentStatus.PENDING, + + Recipient: { + some: { + email: user.email, + signingStatus: SigningStatus.SIGNED, + }, + }, + }, + ], + })) + .with(ExtendedDocumentStatus.COMPLETED, () => ({ + OR: [ + { + userId, + status: ExtendedDocumentStatus.COMPLETED, + }, + { + status: ExtendedDocumentStatus.COMPLETED, + Recipient: { + some: { + email: user.email, + }, + }, + }, + ], + })) + .exhaustive(); const [data, count] = await Promise.all([ prisma.document.findMany({ where: { + ...termFilters, ...filters, }, skip: Math.max(page - 1, 0) * perPage, @@ -51,11 +127,19 @@ export const findDocuments = async ({ [orderByColumn]: orderByDirection, }, include: { + User: { + select: { + id: true, + name: true, + email: true, + }, + }, Recipient: true, }, }), prisma.document.count({ where: { + ...termFilters, ...filters, }, }), diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts index 25754d7bc..6e875f9be 100644 --- a/packages/lib/server-only/document/get-stats.ts +++ b/packages/lib/server-only/document/get-stats.ts @@ -1,30 +1,88 @@ import { prisma } from '@documenso/prisma'; -import { DocumentStatus } from '@documenso/prisma/client'; +import { SigningStatus, User } from '@documenso/prisma/client'; +import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; export type GetStatsInput = { - userId: number; + user: User; }; -export const getStats = async ({ userId }: GetStatsInput) => { - const result = await prisma.document.groupBy({ - by: ['status'], - _count: { - _all: true, - }, - where: { - userId, - }, - }); +export const getStats = async ({ user }: GetStatsInput) => { + const [ownerCounts, notSignedCounts, hasSignedCounts] = await Promise.all([ + prisma.document.groupBy({ + by: ['status'], + _count: { + _all: true, + }, + where: { + userId: user.id, + }, + }), + prisma.document.groupBy({ + by: ['status'], + _count: { + _all: true, + }, + where: { + status: ExtendedDocumentStatus.PENDING, + Recipient: { + some: { + email: user.email, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }, + }, + }), + prisma.document.groupBy({ + by: ['status'], + _count: { + _all: true, + }, + where: { + status: { + not: ExtendedDocumentStatus.DRAFT, + }, + Recipient: { + some: { + email: user.email, + signingStatus: SigningStatus.SIGNED, + }, + }, + }, + }), + ]); - const stats: Record = { - [DocumentStatus.DRAFT]: 0, - [DocumentStatus.PENDING]: 0, - [DocumentStatus.COMPLETED]: 0, + const stats: Record = { + [ExtendedDocumentStatus.DRAFT]: 0, + [ExtendedDocumentStatus.PENDING]: 0, + [ExtendedDocumentStatus.COMPLETED]: 0, + [ExtendedDocumentStatus.INBOX]: 0, + [ExtendedDocumentStatus.ALL]: 0, }; - result.forEach((stat) => { + ownerCounts.forEach((stat) => { stats[stat.status] = stat._count._all; }); + notSignedCounts.forEach((stat) => { + stats[ExtendedDocumentStatus.INBOX] += stat._count._all; + }); + + hasSignedCounts.forEach((stat) => { + if (stat.status === ExtendedDocumentStatus.COMPLETED) { + stats[ExtendedDocumentStatus.COMPLETED] += stat._count._all; + } + + if (stat.status === ExtendedDocumentStatus.PENDING) { + stats[ExtendedDocumentStatus.PENDING] += stat._count._all; + } + }); + + Object.keys(stats).forEach((key) => { + if (key !== ExtendedDocumentStatus.ALL && isExtendedDocumentStatus(key)) { + stats[ExtendedDocumentStatus.ALL] += stats[key]; + } + }); + return stats; }; diff --git a/packages/prisma/guards/is-extended-document-status.ts b/packages/prisma/guards/is-extended-document-status.ts new file mode 100644 index 000000000..43be73f64 --- /dev/null +++ b/packages/prisma/guards/is-extended-document-status.ts @@ -0,0 +1,11 @@ +import { ExtendedDocumentStatus } from '../types/extended-document-status'; + +export const isExtendedDocumentStatus = (value: unknown): value is ExtendedDocumentStatus => { + if (typeof value !== 'string') { + return false; + } + + // We're using the assertion for a type-guard so it's safe to ignore the eslint warning + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return Object.values(ExtendedDocumentStatus).includes(value as ExtendedDocumentStatus); +}; diff --git a/packages/prisma/types/extended-document-status.ts b/packages/prisma/types/extended-document-status.ts new file mode 100644 index 000000000..a3576750d --- /dev/null +++ b/packages/prisma/types/extended-document-status.ts @@ -0,0 +1,10 @@ +import { DocumentStatus } from '@prisma/client'; + +export const ExtendedDocumentStatus = { + ...DocumentStatus, + INBOX: 'INBOX', + ALL: 'ALL', +} as const; + +export type ExtendedDocumentStatus = + (typeof ExtendedDocumentStatus)[keyof typeof ExtendedDocumentStatus]; diff --git a/packages/ui/icons/signature.tsx b/packages/ui/icons/signature.tsx new file mode 100644 index 000000000..b91998bb5 --- /dev/null +++ b/packages/ui/icons/signature.tsx @@ -0,0 +1,28 @@ +import type { LucideIcon } from 'lucide-react/dist/lucide-react'; + +export const SignatureIcon: LucideIcon = ({ + size = 24, + color = 'currentColor', + strokeWidth = 1.33, + absoluteStrokeWidth, + ...props +}) => { + return ( + + + + ); +}; From 68a5a9da1e12f2bd6ea23fd2f59228e4c93738a0 Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 29 Aug 2023 14:30:57 +1000 Subject: [PATCH 3/5] feat: add data table actions --- .../documents/data-table-action-button.tsx | 65 +++++++++ .../documents/data-table-action-dropdown.tsx | 133 ++++++++++++++++++ .../app/(dashboard)/documents/data-table.tsx | 87 +++--------- .../server-only/document/find-documents.ts | 15 +- packages/lib/types/find-result-set.ts | 2 +- .../prisma/types/document-with-recipient.ts | 2 +- 6 files changed, 230 insertions(+), 74 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx create mode 100644 apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx new file mode 100644 index 000000000..7c1d42d2b --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx @@ -0,0 +1,65 @@ +'use client'; + +import Link from 'next/link'; + +import { Edit, Pencil, Share } from 'lucide-react'; +import { useSession } from 'next-auth/react'; +import { match } from 'ts-pattern'; + +import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client'; +import { Button } from '@documenso/ui/primitives/button'; + +export type DataTableActionButtonProps = { + row: Document & { + User: Pick; + Recipient: Recipient[]; + }; +}; + +export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => { + const { data: session } = useSession(); + + if (!session) { + return null; + } + + const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); + + const isOwner = row.User.id === session.user.id; + const isRecipient = !!recipient; + const isDraft = row.status === DocumentStatus.DRAFT; + const isPending = row.status === DocumentStatus.PENDING; + const isComplete = row.status === DocumentStatus.COMPLETED; + const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; + + return match({ + isOwner, + isRecipient, + isDraft, + isPending, + isComplete, + isSigned, + }) + .with({ isOwner: true, isDraft: true }, () => ( + + )) + .with({ isRecipient: true, isPending: true, isSigned: false }, () => ( + + )) + .otherwise(() => ( + + )); +}; diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx new file mode 100644 index 000000000..b1d5832f8 --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx @@ -0,0 +1,133 @@ +'use client'; + +import Link from 'next/link'; + +import { + Copy, + Download, + Edit, + History, + MoreHorizontal, + Pencil, + Share, + Trash2, + XCircle, +} from 'lucide-react'; +import { useSession } from 'next-auth/react'; + +import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from '@documenso/ui/primitives/dropdown-menu'; + +export type DataTableActionDropdownProps = { + row: Document & { + User: Pick; + Recipient: Recipient[]; + }; +}; + +export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => { + const { data: session } = useSession(); + + if (!session) { + return null; + } + + const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); + + const isOwner = row.User.id === session.user.id; + // const isRecipient = !!recipient; + // const isDraft = row.status === DocumentStatus.DRAFT; + // const isPending = row.status === DocumentStatus.PENDING; + const isComplete = row.status === DocumentStatus.COMPLETED; + // const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; + + const onDownloadClick = () => { + let decodedDocument = row.document; + + try { + decodedDocument = atob(decodedDocument); + } catch (err) { + // We're just going to ignore this error and try to download the document + console.error(err); + } + + const documentBytes = Uint8Array.from(decodedDocument.split('').map((c) => c.charCodeAt(0))); + + const blob = new Blob([documentBytes], { + type: 'application/pdf', + }); + + const link = window.document.createElement('a'); + + link.href = window.URL.createObjectURL(blob); + link.download = row.title || 'document.pdf'; + + link.click(); + + window.URL.revokeObjectURL(link.href); + }; + + return ( + + + + + + + Action + + + + + Sign + + + + + + + Edit + + + + + + Download + + + + + Duplicate + + + + + Void + + + + + Delete + + + Share + + + + Resend + + + + + Share + + + + ); +}; diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index 012038f4e..1d6c08e73 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -4,39 +4,28 @@ import { useTransition } from 'react'; import Link from 'next/link'; -import { - Copy, - Download, - Edit, - History, - Loader, - MoreHorizontal, - Pencil, - Share, - Trash2, - XCircle, -} 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 { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient'; -import { Button } from '@documenso/ui/primitives/button'; +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 { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from '@documenso/ui/primitives/dropdown-menu'; 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'; + export type DocumentsDataTableProps = { - results: FindResultSet; + results: FindResultSet< + Document & { + Recipient: Recipient[]; + User: Pick; + } + >; }; export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { @@ -64,7 +53,11 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { { header: 'Title', cell: ({ row }) => ( - + {row.original.title} ), @@ -88,52 +81,10 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { }, { header: 'Actions', - cell: ({ row: _row }) => ( + cell: ({ row }) => (
- - - - - - - - Action - - - Sign - - - - Edit - - - - Download - - - - Duplicate - - - - Void - - - - Delete - - - Share - - - Resend - - - - Share - - - + +
), }, diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 93b1e2089..5a2d695ae 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -3,7 +3,6 @@ import { match } from 'ts-pattern'; import { prisma } from '@documenso/prisma'; import { Document, Prisma, SigningStatus } from '@documenso/prisma/client'; import { DocumentWithRecipientAndSender } from '@documenso/prisma/types/document'; -import { DocumentWithReciepient } from '@documenso/prisma/types/document-with-recipient'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { FindResultSet } from '../../types/find-result-set'; @@ -27,7 +26,7 @@ export const findDocuments = async ({ page = 1, perPage = 10, orderBy, -}: FindDocumentsOptions): Promise> => { +}: FindDocumentsOptions) => { const user = await prisma.user.findFirstOrThrow({ where: { id: userId, @@ -145,13 +144,21 @@ export const findDocuments = async ({ }), ]); + const maskedData = data.map((doc) => ({ + ...doc, + Recipient: doc.Recipient.map((recipient) => ({ + ...recipient, + token: recipient.email === user.email ? recipient.token : '', + })), + })); + return { - data, + data: maskedData, count, currentPage: Math.max(page, 1), perPage, totalPages: Math.ceil(count / perPage), - }; + } satisfies FindResultSet; }; export interface FindDocumentsWithRecipientAndSenderOptions { diff --git a/packages/lib/types/find-result-set.ts b/packages/lib/types/find-result-set.ts index 13eab7bbd..81b16f1ca 100644 --- a/packages/lib/types/find-result-set.ts +++ b/packages/lib/types/find-result-set.ts @@ -1,5 +1,5 @@ export type FindResultSet = { - data: T[]; + data: T extends Array ? T : T[]; count: number; currentPage: number; perPage: number; diff --git a/packages/prisma/types/document-with-recipient.ts b/packages/prisma/types/document-with-recipient.ts index 208fb2b68..4ba6a9776 100644 --- a/packages/prisma/types/document-with-recipient.ts +++ b/packages/prisma/types/document-with-recipient.ts @@ -1,5 +1,5 @@ import { Document, Recipient } from '@documenso/prisma/client'; -export type DocumentWithReciepient = Document & { +export type DocumentWithRecipient = Document & { Recipient: Recipient[]; }; From 9f93af613490f223683bf0277d97e773fd081745 Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 29 Aug 2023 17:26:19 +1000 Subject: [PATCH 4/5] fix: remove unused code --- .../src/app/(dashboard)/dashboard/page.tsx | 6 +- apps/web/src/app/(dashboard)/inbox/page.tsx | 14 - .../(dashboard)/inbox/inbox-content.tsx | 82 ---- .../(dashboard)/inbox/inbox.actions.ts | 24 -- .../components/(dashboard)/inbox/inbox.tsx | 352 ------------------ .../(dashboard)/inbox/inbox.utils.ts | 23 -- .../(dashboard)/layout/desktop-nav.tsx | 22 +- .../components/(dashboard)/layout/header.tsx | 9 +- .../server-only/document/find-documents.ts | 109 ------ .../trpc/server/document-router/router.ts | 28 -- 10 files changed, 10 insertions(+), 659 deletions(-) delete mode 100644 apps/web/src/app/(dashboard)/inbox/page.tsx delete mode 100644 apps/web/src/components/(dashboard)/inbox/inbox-content.tsx delete mode 100644 apps/web/src/components/(dashboard)/inbox/inbox.actions.ts delete mode 100644 apps/web/src/components/(dashboard)/inbox/inbox.tsx delete mode 100644 apps/web/src/components/(dashboard)/inbox/inbox.utils.ts diff --git a/apps/web/src/app/(dashboard)/dashboard/page.tsx b/apps/web/src/app/(dashboard)/dashboard/page.tsx index a9d650eb6..8e242b2fd 100644 --- a/apps/web/src/app/(dashboard)/dashboard/page.tsx +++ b/apps/web/src/app/(dashboard)/dashboard/page.tsx @@ -22,14 +22,14 @@ import { LocaleDate } from '~/components/formatter/locale-date'; import { UploadDocument } from './upload-document'; export default async function DashboardPage() { - const session = await getRequiredServerComponentSession(); + const user = await getRequiredServerComponentSession(); const [stats, results] = await Promise.all([ getStats({ - userId: session.id, + user, }), findDocuments({ - userId: session.id, + userId: user.id, perPage: 10, }), ]); diff --git a/apps/web/src/app/(dashboard)/inbox/page.tsx b/apps/web/src/app/(dashboard)/inbox/page.tsx deleted file mode 100644 index badb421c9..000000000 --- a/apps/web/src/app/(dashboard)/inbox/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import Inbox from '~/components/(dashboard)/inbox/inbox'; - -export default function InboxPage() { - return ( -
-

Inbox

-

Documents which you have been requested to sign.

- -
- -
-
- ); -} diff --git a/apps/web/src/components/(dashboard)/inbox/inbox-content.tsx b/apps/web/src/components/(dashboard)/inbox/inbox-content.tsx deleted file mode 100644 index f7e263f1f..000000000 --- a/apps/web/src/components/(dashboard)/inbox/inbox-content.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { TemplateDocumentCompleted } from '@documenso/email/template-components/template-document-completed'; -import { TemplateDocumentInvite } from '@documenso/email/template-components/template-document-invite'; -import { DocumentWithRecipientAndSender } from '@documenso/prisma/types/document'; -import { cn } from '@documenso/ui/lib/utils'; - -import { formatInboxDate } from './inbox.utils'; - -export type InboxContentProps = { - document: DocumentWithRecipientAndSender; -}; - -export default function InboxContent({ document }: InboxContentProps) { - const inboxDocumentStatusIndicator = ( - -
- - {document.recipient.signingStatus === 'SIGNED' ? 'Signed' : 'Pending'} -
- ); - - return ( -
-
-
-

{document.subject}

-

- {document.sender.name} <{document.sender.email}> -

-
- -
- {/* Todo: This needs to be updated to when the document was sent to the recipient when that value is available. */} -

{formatInboxDate(document.created)}

- - {inboxDocumentStatusIndicator} -
-
- -
- {inboxDocumentStatusIndicator} -
- - {/* Todo: get correct URLs */} -
- {document.recipient.signingStatus === 'NOT_SIGNED' && ( - - )} - - {document.recipient.signingStatus === 'SIGNED' && ( - - )} -
-
- ); -} diff --git a/apps/web/src/components/(dashboard)/inbox/inbox.actions.ts b/apps/web/src/components/(dashboard)/inbox/inbox.actions.ts deleted file mode 100644 index 38b50a8b3..000000000 --- a/apps/web/src/components/(dashboard)/inbox/inbox.actions.ts +++ /dev/null @@ -1,24 +0,0 @@ -'use server'; - -import { z } from 'zod'; - -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; -import { prisma } from '@documenso/prisma'; - -export async function updateRecipientReadStatus(recipientId: number, documentId: number) { - z.number().parse(recipientId); - z.number().parse(documentId); - - const { email } = await getRequiredServerComponentSession(); - - await prisma.recipient.update({ - where: { - id: recipientId, - documentId, - email, - }, - data: { - readStatus: 'OPENED', - }, - }); -} diff --git a/apps/web/src/components/(dashboard)/inbox/inbox.tsx b/apps/web/src/components/(dashboard)/inbox/inbox.tsx deleted file mode 100644 index c76ba3d94..000000000 --- a/apps/web/src/components/(dashboard)/inbox/inbox.tsx +++ /dev/null @@ -1,352 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; - -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; - -import { Inbox as InboxIcon } from 'lucide-react'; -import { z } from 'zod'; - -import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; -import { SigningStatus } from '@documenso/prisma/client'; -import { DocumentWithRecipientAndSender } from '@documenso/prisma/types/document'; -import { trpc } from '@documenso/trpc/react'; -import { cn } from '@documenso/ui/lib/utils'; -import { Input } from '@documenso/ui/primitives/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@documenso/ui/primitives/select'; -import { Skeleton } from '@documenso/ui/primitives/skeleton'; - -import { useDebouncedValue } from '~/hooks/use-debounced-value'; - -import InboxContent from './inbox-content'; -import { updateRecipientReadStatus } from './inbox.actions'; -import { formatInboxDate } from './inbox.utils'; - -export const ZInboxSearchParamsSchema = z.object({ - filter: z - .union([z.literal('SIGNED'), z.literal('NOT_SIGNED'), z.undefined()]) - .catch(() => undefined), - id: z - .string() - .optional() - .catch(() => undefined), - query: z - .string() - .optional() - .catch(() => undefined), -}); - -export type InboxProps = { - className?: string; -}; - -const numberOfSkeletons = 3; - -export default function Inbox(props: InboxProps) { - const { className } = props; - - const pathname = usePathname(); - const searchParams = useSearchParams(); - const router = useRouter(); - const updateSearchParams = useUpdateSearchParams(); - - const parsedSearchParams = ZInboxSearchParamsSchema.parse(Object.fromEntries(searchParams ?? [])); - - const [searchQuery, setSearchQuery] = useState(() => parsedSearchParams.query || ''); - - const [readStatusState, setReadStatusState] = useState<{ - [documentId: string]: 'ERROR' | 'UPDATED' | 'UPDATING'; - }>({}); - - const [isInitialLoad, setIsInitialLoad] = useState(true); - - const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); - - const { - data, - error, - fetchNextPage, - fetchPreviousPage, - hasNextPage, - hasPreviousPage, - isFetching, - isFetchingNextPage, - isFetchingPreviousPage, - refetch, - } = trpc.document.searchInboxDocuments.useInfiniteQuery( - { - query: parsedSearchParams.query, - filter: parsedSearchParams.filter, - }, - { - getPreviousPageParam: (firstPage) => - firstPage.currentPage > 1 ? firstPage.currentPage - 1 : undefined, - getNextPageParam: (lastPage) => - lastPage.currentPage < lastPage.totalPages ? lastPage.currentPage + 1 : undefined, - }, - ); - - /** - * The current documents in the inbox after filters and queries have been applied. - */ - const inboxDocuments = (data?.pages ?? []).flatMap((page) => page.data); - - /** - * The currently selected document in the inbox. - */ - const selectedDocument: DocumentWithRecipientAndSender | null = - inboxDocuments.find((item) => item.id.toString() === parsedSearchParams.id) ?? null; - - /** - * Remove the ID from the query if it is not found in the result. - */ - useEffect(() => { - if (!selectedDocument && parsedSearchParams.id && data) { - updateSearchParams({ - id: null, - }); - } - }, [data, selectedDocument, parsedSearchParams.id]); - - /** - * Handle debouncing the seach query. - */ - useEffect(() => { - if (!pathname) { - return; - } - - const params = new URLSearchParams(searchParams?.toString()); - - params.set('query', debouncedSearchQuery); - - if (debouncedSearchQuery === '') { - params.delete('query'); - } - - router.push(`${pathname}?${params.toString()}`); - }, [debouncedSearchQuery]); - - useEffect(() => { - if (!isFetching) { - setIsInitialLoad(false); - } - }, [isFetching]); - - const updateReadStatusState = (documentId: number, value: (typeof readStatusState)[string]) => { - setReadStatusState({ - ...readStatusState, - [documentId]: value, - }); - }; - - /** - * Handle selecting the selected document to display and updating the read status if required. - */ - const onSelectedDocumentChange = (value: DocumentWithRecipientAndSender) => { - if (!pathname) { - return; - } - - // Update the read status. - if ( - value.recipient.readStatus === 'NOT_OPENED' && - readStatusState[value.id] !== 'UPDATED' && - readStatusState[value.id] !== 'UPDATING' - ) { - updateReadStatusState(value.id, 'UPDATING'); - - updateRecipientReadStatus(value.recipient.id, value.id) - .then(() => { - updateReadStatusState(value.id, 'UPDATED'); - }) - .catch(() => { - updateReadStatusState(value.id, 'ERROR'); - }); - } - - const params = new URLSearchParams(searchParams?.toString()); - - params.set('id', value.id.toString()); - - router.push(`${pathname}?${params.toString()}`); - }; - - if (error) { - return ( -
-

Something went wrong while loading your inbox.

- -
- ); - } - - return ( -
-
- {/* Header with search and filter options. */} -
- setSearchQuery(e.target.value)} - /> - -
- -
-
- -
- {/* Handle rendering no items found. */} - {!isFetching && inboxDocuments.length === 0 && ( -
-

No documents found.

-
- )} - - {hasPreviousPage && !isFetchingPreviousPage && ( - - )} - -
    - {/* Handle rendering skeleton on first load. */} - {isFetching && - isInitialLoad && - !data && - Array.from({ length: numberOfSkeletons }).map((_, i) => ( -
  • - - -
    -
    - - -
    - - - - -
    -
  • - ))} - - {/* Handle rendering list of inbox documents. */} - {inboxDocuments.map((item, i) => ( -
  • - - - {/* Mobile inbox content. */} - {selectedDocument?.id === item.id && ( -
    - -
    - )} -
  • - ))} -
- - {hasNextPage && !isFetchingNextPage && ( - - )} -
-
- - {/* Desktop inbox content. */} -
- {selectedDocument ? ( - - ) : ( -
- -
- )} -
-
- ); -} diff --git a/apps/web/src/components/(dashboard)/inbox/inbox.utils.ts b/apps/web/src/components/(dashboard)/inbox/inbox.utils.ts deleted file mode 100644 index 9a6aff9f3..000000000 --- a/apps/web/src/components/(dashboard)/inbox/inbox.utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DateTime } from 'luxon'; - -/** - * Format the provided date into a readable string for inboxes. - * - * @param dateValue The date or date string - * @returns The date in the current locale, or the date formatted as HH:MM AM/PM if the provided date is after 12:00AM of the current date - */ -export const formatInboxDate = (dateValue: string | Date): string => { - const date = - typeof dateValue === 'string' ? DateTime.fromISO(dateValue) : DateTime.fromJSDate(dateValue); - - const startOfTheDay = DateTime.now().startOf('day'); - - if (date >= startOfTheDay) { - return date.toFormat('h:mma'); - } - - return date.toLocaleString({ - ...DateTime.DATE_SHORT, - year: '2-digit', - }); -}; diff --git a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx index e4ce72738..fb02ef3ef 100644 --- a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx +++ b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx @@ -2,19 +2,17 @@ import { HTMLAttributes } from 'react'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; - import { cn } from '@documenso/ui/lib/utils'; export type DesktopNavProps = HTMLAttributes; export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { - const pathname = usePathname(); + // const pathname = usePathname(); return ( ); }; diff --git a/apps/web/src/components/(dashboard)/layout/header.tsx b/apps/web/src/components/(dashboard)/layout/header.tsx index c10fa9e5e..88dc5d7a4 100644 --- a/apps/web/src/components/(dashboard)/layout/header.tsx +++ b/apps/web/src/components/(dashboard)/layout/header.tsx @@ -4,11 +4,8 @@ import { HTMLAttributes } from 'react'; import Link from 'next/link'; -import { Menu } from 'lucide-react'; - import { User } from '@documenso/prisma/client'; import { cn } from '@documenso/ui/lib/utils'; -import { Button } from '@documenso/ui/primitives/button'; import { Logo } from '~/components/branding/logo'; @@ -23,7 +20,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => { return (
{
- + */}
diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 5a2d695ae..c9c8eaf6c 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -2,7 +2,6 @@ import { match } from 'ts-pattern'; import { prisma } from '@documenso/prisma'; import { Document, Prisma, SigningStatus } from '@documenso/prisma/client'; -import { DocumentWithRecipientAndSender } from '@documenso/prisma/types/document'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { FindResultSet } from '../../types/find-result-set'; @@ -160,111 +159,3 @@ export const findDocuments = async ({ totalPages: Math.ceil(count / perPage), } satisfies FindResultSet; }; - -export interface FindDocumentsWithRecipientAndSenderOptions { - email: string; - query?: string; - signingStatus?: SigningStatus; - page?: number; - perPage?: number; - orderBy?: { - column: keyof Omit; - direction: 'asc' | 'desc'; - }; -} - -export const findDocumentsWithRecipientAndSender = async ({ - email, - query, - signingStatus, - page = 1, - perPage = 20, - orderBy, -}: FindDocumentsWithRecipientAndSenderOptions): Promise< - FindResultSet -> => { - const orderByColumn = orderBy?.column ?? 'created'; - const orderByDirection = orderBy?.direction ?? 'desc'; - - const filters: Prisma.DocumentWhereInput = { - Recipient: { - some: { - email, - signingStatus, - }, - }, - }; - - if (query) { - filters.OR = [ - { - User: { - email: { - contains: query, - mode: 'insensitive', - }, - }, - // Todo: Add filter for `Subject`. - }, - ]; - } - - const [data, count] = await Promise.all([ - prisma.document.findMany({ - select: { - id: true, - created: true, - title: true, - status: true, - userId: true, - User: { - select: { - id: true, - name: true, - email: true, - }, - }, - Recipient: { - where: { - email, - signingStatus, - }, - }, - }, - where: { - ...filters, - }, - skip: Math.max(page - 1, 0) * perPage, - take: perPage, - orderBy: { - [orderByColumn]: orderByDirection, - }, - }), - prisma.document.count({ - where: { - ...filters, - }, - }), - ]); - - return { - data: data.map((item) => { - const { User, Recipient, ...rest } = item; - - const subject = undefined; // Todo. - const description = undefined; // Todo. - - return { - ...rest, - sender: User, - recipient: Recipient[0], - subject: subject ?? 'Please sign this document', - description: description ?? `${User.name} has invited you to sign "${item.title}"`, - }; - }), - count, - currentPage: Math.max(page, 1), - perPage, - totalPages: Math.ceil(count / perPage), - }; -}; diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 1f790dc24..f20643327 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -1,45 +1,17 @@ import { TRPCError } from '@trpc/server'; -import { findDocumentsWithRecipientAndSender } from '@documenso/lib/server-only/document/find-documents'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; import { authenticatedProcedure, router } from '../trpc'; import { - ZSearchInboxDocumentsParamsSchema, ZSendDocumentMutationSchema, ZSetFieldsForDocumentMutationSchema, ZSetRecipientsForDocumentMutationSchema, } from './schema'; export const documentRouter = router({ - searchInboxDocuments: authenticatedProcedure - .input(ZSearchInboxDocumentsParamsSchema) - .query(async ({ input, ctx }) => { - try { - const { filter, query, cursor: page } = input; - - return await findDocumentsWithRecipientAndSender({ - email: ctx.session.email, - query, - signingStatus: filter, - orderBy: { - column: 'created', - direction: 'desc', - }, - page, - }); - } catch (err) { - console.error(err); - - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Something went wrong. Please try again later.', - }); - } - }), - setRecipientsForDocument: authenticatedProcedure .input(ZSetRecipientsForDocumentMutationSchema) .mutation(async ({ input, ctx }) => { From 8c4120f0a2abf0b61cb609535d5eab9a92992ab9 Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 29 Aug 2023 18:12:46 +1000 Subject: [PATCH 5/5] fix: remove further unused code --- packages/trpc/server/document-router/schema.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 8ea107de3..18c3a93ae 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -2,19 +2,6 @@ import { z } from 'zod'; import { FieldType } from '@documenso/prisma/client'; -export const ZSearchInboxDocumentsParamsSchema = z.object({ - filter: z - .union([z.literal('SIGNED'), z.literal('NOT_SIGNED'), z.undefined()]) - .catch(() => undefined), - cursor: z.number().default(1), - query: z - .string() - .optional() - .catch(() => undefined), -}); - -export type TSearchInboxDocumentsParamsSchema = z.infer; - export const ZSetRecipientsForDocumentMutationSchema = z.object({ documentId: z.number(), recipients: z.array(