mirror of
https://github.com/documenso/documenso.git
synced 2026-07-14 14:57:12 +10:00
154 lines
6.4 KiB
TypeScript
154 lines
6.4 KiB
TypeScript
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
|
import { trpc } from '@documenso/trpc/react';
|
|
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
|
import { cn } from '@documenso/ui/lib/utils';
|
|
import { msg } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react';
|
|
import { Trans } from '@lingui/react/macro';
|
|
import { AlertTriangle, CheckCheckIcon, CheckIcon, Loader, MailOpen } from 'lucide-react';
|
|
import { DateTime } from 'luxon';
|
|
import { useMemo } from 'react';
|
|
import { match } from 'ts-pattern';
|
|
|
|
export type DocumentPageViewRecentActivityProps = {
|
|
documentId: number;
|
|
userId: number;
|
|
};
|
|
|
|
export const DocumentPageViewRecentActivity = ({ documentId, userId }: DocumentPageViewRecentActivityProps) => {
|
|
const { _, i18n } = useLingui();
|
|
|
|
const { data, isLoading, isLoadingError, refetch, hasNextPage, fetchNextPage, isFetchingNextPage } =
|
|
trpc.document.auditLog.find.useInfiniteQuery(
|
|
{
|
|
documentId,
|
|
filterForRecentActivity: true,
|
|
orderByColumn: 'createdAt',
|
|
orderByDirection: 'asc',
|
|
perPage: 10,
|
|
},
|
|
{
|
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
},
|
|
);
|
|
|
|
const documentAuditLogs = useMemo(() => (data?.pages ?? []).flatMap((page) => page.data), [data]);
|
|
|
|
return (
|
|
<section className="flex flex-col rounded-xl border border-border bg-widget dark:bg-background">
|
|
<div className="flex flex-row items-center justify-between border-b px-4 py-3">
|
|
<h1 className="font-medium text-foreground">
|
|
<Trans>Recent activity</Trans>
|
|
</h1>
|
|
|
|
{/* Can add dropdown menu here for additional options. */}
|
|
</div>
|
|
|
|
{isLoading && (
|
|
<div className="flex h-full items-center justify-center py-16">
|
|
<Loader className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
)}
|
|
|
|
{isLoadingError && (
|
|
<div className="flex h-full flex-col items-center justify-center py-16">
|
|
<p className="text-foreground/80 text-sm">
|
|
<Trans>Unable to load document history</Trans>
|
|
</p>
|
|
<button
|
|
onClick={async () => refetch()}
|
|
className="mt-2 text-foreground/70 text-sm hover:text-muted-foreground"
|
|
>
|
|
<Trans>Click here to retry</Trans>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<AnimateGenericFadeInOut>
|
|
{data && (
|
|
<ul role="list" className="space-y-6 p-4">
|
|
{hasNextPage && (
|
|
<li className="relative flex gap-x-4">
|
|
<div className="absolute top-0 -bottom-6 left-0 flex w-6 justify-center">
|
|
<div className="w-px bg-border" />
|
|
</div>
|
|
|
|
<div className="relative flex h-6 w-6 flex-none items-center justify-center bg-widget">
|
|
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-gray-300 dark:ring-neutral-600" />
|
|
</div>
|
|
|
|
<button
|
|
onClick={async () => fetchNextPage()}
|
|
className="text-foreground/70 text-xs hover:text-muted-foreground"
|
|
>
|
|
{isFetchingNextPage ? _(msg`Loading...`) : _(msg`Load older activity`)}
|
|
</button>
|
|
</li>
|
|
)}
|
|
|
|
{documentAuditLogs.length === 0 && (
|
|
<div className="flex items-center justify-center py-4">
|
|
<p className="text-muted-foreground/70 text-sm">
|
|
<Trans>No recent activity</Trans>
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{documentAuditLogs.map((auditLog, auditLogIndex) => (
|
|
<li key={auditLog.id} className="relative flex gap-x-4">
|
|
<div
|
|
className={cn(
|
|
auditLogIndex === documentAuditLogs.length - 1 ? 'h-6' : '-bottom-6',
|
|
'absolute top-0 left-0 flex w-6 justify-center',
|
|
)}
|
|
>
|
|
<div className="w-px bg-border" />
|
|
</div>
|
|
|
|
<div className="relative flex h-6 w-6 flex-none items-center justify-center bg-widget text-foreground/40">
|
|
{match(auditLog.type)
|
|
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED, () => (
|
|
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
|
|
<CheckCheckIcon className="h-3 w-3" aria-hidden="true" />
|
|
</div>
|
|
))
|
|
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => (
|
|
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
|
|
<CheckIcon className="h-3 w-3" aria-hidden="true" />
|
|
</div>
|
|
))
|
|
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => (
|
|
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
|
|
<AlertTriangle className="h-3 w-3" aria-hidden="true" />
|
|
</div>
|
|
))
|
|
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED, () => (
|
|
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
|
|
<MailOpen className="h-3 w-3" aria-hidden="true" />
|
|
</div>
|
|
))
|
|
.otherwise(() => (
|
|
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-gray-300 dark:ring-neutral-600" />
|
|
))}
|
|
</div>
|
|
|
|
<p
|
|
className="flex-auto truncate py-0.5 text-muted-foreground text-xs leading-5 dark:text-muted-foreground/70"
|
|
title={formatDocumentAuditLogAction(i18n, auditLog, userId).description}
|
|
>
|
|
{formatDocumentAuditLogAction(i18n, auditLog, userId).description}
|
|
</p>
|
|
|
|
<time className="flex-none py-0.5 text-muted-foreground text-xs leading-5 dark:text-muted-foreground/70">
|
|
{DateTime.fromJSDate(auditLog.createdAt).toRelative({ style: 'short' })}
|
|
</time>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</AnimateGenericFadeInOut>
|
|
</section>
|
|
);
|
|
};
|