diff --git a/apps/remix/app/components/general/document/document-status.tsx b/apps/remix/app/components/general/document/document-status.tsx index eacebaec1..df4613505 100644 --- a/apps/remix/app/components/general/document/document-status.tsx +++ b/apps/remix/app/components/general/document/document-status.tsx @@ -1,5 +1,4 @@ import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; -import type { TInboxStatus } from '@documenso/trpc/server/document-router/find-inbox.types'; import { SignatureIcon } from '@documenso/ui/icons/signature'; import { cn } from '@documenso/ui/lib/utils'; import type { MessageDescriptor } from '@lingui/core'; @@ -16,7 +15,7 @@ type FriendlyStatus = { color: string; }; -export const FRIENDLY_STATUS_MAP: Record = { +export const FRIENDLY_STATUS_MAP: Record = { PENDING: { label: msg`Pending`, labelExtended: msg`Document pending`, @@ -73,7 +72,7 @@ export const FRIENDLY_STATUS_MAP: Record & { - status: ExtendedDocumentStatus | TInboxStatus; + status: ExtendedDocumentStatus; inheritColor?: boolean; }; diff --git a/apps/remix/app/components/tables/documents-table-status-filter.tsx b/apps/remix/app/components/tables/documents-table-status-filter.tsx index e97ab8e08..4e312582f 100644 --- a/apps/remix/app/components/tables/documents-table-status-filter.tsx +++ b/apps/remix/app/components/tables/documents-table-status-filter.tsx @@ -1,32 +1,29 @@ import { useOptionalCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { STATS_COUNT_CAP } from '@documenso/lib/constants/document'; -import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; -import type { TFindDocumentsInternalResponse } from '@documenso/trpc/server/document-router/find-documents-internal.types'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { OrganisationType } from '@prisma/client'; import { ListFilterIcon } from 'lucide-react'; -import { parseAsString, useQueryStates } from 'nuqs'; +import { useQueryStates } from 'nuqs'; import { useMemo } from 'react'; -import type { DocumentStatusProps } from '~/components/general/document/document-status'; import { DocumentStatus, FRIENDLY_STATUS_MAP } from '~/components/general/document/document-status'; import { FilterPill } from '~/components/general/filter-pill'; import { documentsSearchParams } from '~/utils/documents-search-params'; type DocumentsTableStatusFilterProps = { /** - * Per-status document counts, shown next to each option. When omitted no - * counts are rendered. + * Per-status document counts, shown next to each option. When omitted, or + * when a status has no count, no count is rendered. */ - stats?: TFindDocumentsInternalResponse['stats']; + stats?: Partial>; /** * The statuses available for selection. Defaults to every status that * makes sense for the documents page. */ - statuses?: readonly DocumentStatusProps['status'][]; + statuses?: readonly ExtendedDocumentStatus[]; }; export const DocumentsTableStatusFilter = ({ @@ -37,11 +34,9 @@ export const DocumentsTableStatusFilter = ({ const organisation = useOptionalCurrentOrganisation(); - // Any string is read here since each page offers its own statuses, which - // are matched against the selectable statuses below. const [{ status }, setSearchParams] = useQueryStates( { - status: parseAsString, + status: documentsSearchParams.status, page: documentsSearchParams.page, }, { history: 'push' }, @@ -82,7 +77,7 @@ export const DocumentsTableStatusFilter = ({ options={selectableStatuses.map((value) => ({ value, label: , - trailing: stats && isExtendedDocumentStatus(value) ? formatStatsCount(stats[value]) : undefined, + trailing: formatStatsCount(stats?.[value]), }))} testId="documents-table-status-filter" /> @@ -90,7 +85,7 @@ export const DocumentsTableStatusFilter = ({ {/* Visually hidden document counts, for screen readers and tests. */} {stats && ( - {[...selectableStatuses, ExtendedDocumentStatus.ALL].filter(isExtendedDocumentStatus).map((value) => ( + {[...selectableStatuses, ExtendedDocumentStatus.ALL].map((value) => ( {_(FRIENDLY_STATUS_MAP[value].label)}:{' '} {stats[value]} @@ -112,6 +107,10 @@ const SELECTABLE_STATUSES: ExtendedDocumentStatus[] = [ ExtendedDocumentStatus.EXPIRED, ]; -const formatStatsCount = (count: number) => { +const formatStatsCount = (count: number | undefined) => { + if (count === undefined) { + return undefined; + } + return count >= STATS_COUNT_CAP ? `${STATS_COUNT_CAP.toLocaleString()}+` : count.toString(); }; diff --git a/apps/remix/app/components/tables/inbox-table.tsx b/apps/remix/app/components/tables/inbox-table.tsx index 83a0fbcee..beec4724a 100644 --- a/apps/remix/app/components/tables/inbox-table.tsx +++ b/apps/remix/app/components/tables/inbox-table.tsx @@ -1,6 +1,7 @@ import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { useSession } from '@documenso/lib/client-only/providers/session'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { trpc } from '@documenso/trpc/react'; import type { TFindInboxResponse } from '@documenso/trpc/server/document-router/find-inbox.types'; import { Button } from '@documenso/ui/primitives/button'; @@ -131,7 +132,7 @@ export const InboxTable = () => { .with({ status: DocumentStatusEnum.COMPLETED }, () => ( Documents that you have completed will appear here )) - .with({ status: 'PARTIALLY_APPROVED' }, () => ( + .with({ status: ExtendedDocumentStatus.PARTIALLY_APPROVED }, () => ( Documents that are waiting on other recipients will appear here )) .with({ status: DocumentStatusEnum.REJECTED }, () => ( @@ -278,5 +279,5 @@ const getInboxStatus = (row: DocumentsTableRow, email: string) => { recipient.signingStatus !== SigningStatus.NOT_SIGNED, ); - return isWaitingOnOthers ? 'PARTIALLY_APPROVED' : row.status; + return isWaitingOnOthers ? ExtendedDocumentStatus.PARTIALLY_APPROVED : row.status; }; diff --git a/packages/app-tests/e2e/documents/inbox.spec.ts b/packages/app-tests/e2e/documents/inbox.spec.ts index ec67b261d..826c39c12 100644 --- a/packages/app-tests/e2e/documents/inbox.spec.ts +++ b/packages/app-tests/e2e/documents/inbox.spec.ts @@ -8,6 +8,7 @@ import { } from '@documenso/prisma/seed/documents'; import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams'; import { seedUser } from '@documenso/prisma/seed/users'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; import { DocumentStatus, RecipientRole, SigningStatus, TeamMemberRole } from '@prisma/client'; @@ -37,7 +38,7 @@ const searchInbox = async (page: Page, query: string) => { // spelling "Canceled" for the `Cancelled` source string. const INBOX_STATUS_LABELS = { [DocumentStatus.PENDING]: 'Pending', - PARTIALLY_APPROVED: 'Partially Approved', + [ExtendedDocumentStatus.PARTIALLY_APPROVED]: 'Partially Approved', [DocumentStatus.COMPLETED]: 'Completed', [DocumentStatus.REJECTED]: 'Rejected', [DocumentStatus.CANCELLED]: 'Canceled', @@ -154,7 +155,7 @@ test.describe('Inbox - Search & Status Filter', () => { await expect(inboxRow(page, 'Inbox Unsigned Document')).toBeVisible(); await expect(inboxRow(page, 'Inbox Signed Document')).not.toBeVisible(); - await selectInboxStatus(page, 'PARTIALLY_APPROVED'); + await selectInboxStatus(page, ExtendedDocumentStatus.PARTIALLY_APPROVED); await expect(page.getByTestId('documents-table-status-filter')).toContainText('Partially Approved'); await expect(inboxRow(page, 'Inbox Signed Document')).toBeVisible(); diff --git a/packages/lib/server-only/admin/get-documents-stats.ts b/packages/lib/server-only/admin/get-documents-stats.ts index 15fed8ff7..c604efa9c 100644 --- a/packages/lib/server-only/admin/get-documents-stats.ts +++ b/packages/lib/server-only/admin/get-documents-stats.ts @@ -13,7 +13,7 @@ export const getDocumentStats = async () => { }, }); - const stats: Record, number> = { + const stats: Record, number> = { [ExtendedDocumentStatus.DRAFT]: 0, [ExtendedDocumentStatus.PENDING]: 0, [ExtendedDocumentStatus.COMPLETED]: 0, diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 031ddfb5b..098339507 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -326,6 +326,7 @@ export const findDocuments = async ({ ]), ), ) + .with(ExtendedDocumentStatus.PARTIALLY_APPROVED, () => null) // Only the inbox offers this status. .exhaustive(); }; @@ -488,6 +489,7 @@ export const findDocuments = async ({ return eb.and([teamDeletedFilter(eb), visibilityFilter(eb), hasExpiredRecipient(eb), eb.or(accessBranches)]); }), ) + .with(ExtendedDocumentStatus.PARTIALLY_APPROVED, () => null) // Only the inbox offers this status. .exhaustive(); }; diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts index e3a27534d..16143e26c 100644 --- a/packages/lib/server-only/document/get-stats.ts +++ b/packages/lib/server-only/document/get-stats.ts @@ -301,7 +301,7 @@ export const getStats = async ({ userId, teamId, period, search = '', folderId, // `expired` is intentionally excluded from `all` — it overlaps PENDING. const all = Math.min(draft + pending + completed + rejected + cancelled + inbox, STATS_COUNT_CAP); - const stats: Record = { + const stats: Record, number> = { [ExtendedDocumentStatus.DRAFT]: draft, [ExtendedDocumentStatus.PENDING]: pending, [ExtendedDocumentStatus.COMPLETED]: completed, diff --git a/packages/prisma/types/extended-document-status.ts b/packages/prisma/types/extended-document-status.ts index ddd9f693e..3e5b38d3e 100644 --- a/packages/prisma/types/extended-document-status.ts +++ b/packages/prisma/types/extended-document-status.ts @@ -5,6 +5,7 @@ export const ExtendedDocumentStatus = { INBOX: 'INBOX', ALL: 'ALL', EXPIRED: 'EXPIRED', + PARTIALLY_APPROVED: 'PARTIALLY_APPROVED', } as const; export type ExtendedDocumentStatus = (typeof ExtendedDocumentStatus)[keyof typeof ExtendedDocumentStatus]; diff --git a/packages/trpc/server/document-router/find-inbox.ts b/packages/trpc/server/document-router/find-inbox.ts index 9d2a7efcc..563639184 100644 --- a/packages/trpc/server/document-router/find-inbox.ts +++ b/packages/trpc/server/document-router/find-inbox.ts @@ -1,6 +1,7 @@ import type { FindResultResponse } from '@documenso/lib/types/search-params'; import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document'; import { prisma } from '@documenso/prisma'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import type { Envelope, Prisma } from '@prisma/client'; import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client'; import { match, P } from 'ts-pattern'; @@ -85,7 +86,7 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, query = '', st status: DocumentStatus.PENDING, recipients: { some: unsignedUserRecipient }, })) - .with('PARTIALLY_APPROVED', () => ({ + .with(ExtendedDocumentStatus.PARTIALLY_APPROVED, () => ({ status: DocumentStatus.PENDING, recipients: { some: userRecipient, none: unsignedUserRecipient }, })) diff --git a/packages/trpc/server/document-router/find-inbox.types.ts b/packages/trpc/server/document-router/find-inbox.types.ts index 0b26f1d9b..54e1ea4e0 100644 --- a/packages/trpc/server/document-router/find-inbox.types.ts +++ b/packages/trpc/server/document-router/find-inbox.types.ts @@ -2,7 +2,7 @@ import { ZDocumentManySchema } from '@documenso/lib/types/document'; import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params'; -import { DocumentStatus } from '@prisma/client'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { z } from 'zod'; /** @@ -16,11 +16,11 @@ import { z } from 'zod'; * that wait on other recipients. */ export const INBOX_STATUSES = [ - DocumentStatus.PENDING, - 'PARTIALLY_APPROVED', - DocumentStatus.COMPLETED, - DocumentStatus.REJECTED, - DocumentStatus.CANCELLED, + ExtendedDocumentStatus.PENDING, + ExtendedDocumentStatus.PARTIALLY_APPROVED, + ExtendedDocumentStatus.COMPLETED, + ExtendedDocumentStatus.REJECTED, + ExtendedDocumentStatus.CANCELLED, ] as const; export const ZInboxStatusSchema = z.enum(INBOX_STATUSES);