feat: migrate templates and documents to envelope model

This commit is contained in:
David Nguyen
2025-09-11 18:23:38 +10:00
parent eec2307634
commit bf89bc781b
234 changed files with 8677 additions and 6054 deletions

View File

@ -1,4 +1,4 @@
import { DocumentStatus } from '@prisma/client';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
@ -7,18 +7,19 @@ import { addZeroMonth } from '../add-zero-month';
export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => {
const qb = kyselyPrisma.$kysely
.selectFrom('Document')
.selectFrom('Envelope')
.select(({ fn }) => [
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'Document.updatedAt']).as('month'),
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'Envelope.updatedAt']).as('month'),
fn.count('id').as('count'),
fn
.sum(fn.count('id'))
// Feels like a bug in the Kysely extension but I just can not do this orderBy in a type-safe manner
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'Document.updatedAt']) as any))
.over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'Envelope.updatedAt']) as any))
.as('cume_count'),
])
.where(() => sql`"Document"."status" = ${DocumentStatus.COMPLETED}::"DocumentStatus"`)
.where(() => sql`"Envelope"."status" = ${DocumentStatus.COMPLETED}::"DocumentStatus"`)
.where(() => sql`"Envelope"."type" = ${EnvelopeType.DOCUMENT}::"EnvelopeType"`)
.groupBy('month')
.orderBy('month', 'desc')
.limit(12);

View File

@ -3,7 +3,6 @@ import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Document } from '@prisma/client';
import { useNavigate } from 'react-router';
import { trpc } from '@documenso/trpc/react';
@ -22,10 +21,10 @@ import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type AdminDocumentDeleteDialogProps = {
document: Document;
envelopeId: string;
};
export const AdminDocumentDeleteDialog = ({ document }: AdminDocumentDeleteDialogProps) => {
export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
@ -42,7 +41,7 @@ export const AdminDocumentDeleteDialog = ({ document }: AdminDocumentDeleteDialo
return;
}
await deleteDocument({ id: document.id, reason });
await deleteDocument({ id: envelopeId, reason });
toast({
title: _(msg`Document deleted`),

View File

@ -81,7 +81,7 @@ export const DocumentMoveToFolderDialog = ({
},
);
const { mutateAsync: moveDocumentToFolder } = trpc.folder.moveDocumentToFolder.useMutation();
const { mutateAsync: updateDocument } = trpc.document.update.useMutation();
useEffect(() => {
if (!open) {
@ -94,9 +94,11 @@ export const DocumentMoveToFolderDialog = ({
const onSubmit = async (data: TMoveDocumentFormSchema) => {
try {
await moveDocumentToFolder({
await updateDocument({
documentId,
folderId: data.folderId ?? null,
data: {
folderId: data.folderId ?? null,
},
});
const documentsPath = formatDocumentsPath(team.url);

View File

@ -4,15 +4,15 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type Recipient, SigningStatus } from '@prisma/client';
import { type Recipient, SigningStatus, type Team, type User } from '@prisma/client';
import { History } from 'lucide-react';
import { useForm, useWatch } from 'react-hook-form';
import * as z from 'zod';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -43,7 +43,11 @@ import { StackAvatar } from '../general/stack-avatar';
const FORM_ID = 'resend-email';
export type DocumentResendDialogProps = {
document: TDocumentRow;
document: Pick<Document, 'id' | 'userId' | 'teamId' | 'status'> & {
user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
};
recipients: Recipient[];
};

View File

@ -4,14 +4,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import type { Template, TemplateDirectLink } from '@prisma/client';
import { TemplateType } from '@prisma/client';
import { type TemplateDirectLink, TemplateType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { CheckCircle2Icon, CircleIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { P, match } from 'ts-pattern';
import { z } from 'zod';
import { type Template } from '@documenso/prisma/types/template-legacy-schema';
import { trpc } from '@documenso/trpc/react';
import {
MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH,

View File

@ -54,7 +54,7 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) =>
try {
const response = await putPdfFile(file);
const { id } = await createTemplate({
const { legacyTemplateId: id } = await createTemplate({
title: file.name,
templateDocumentDataId: response.id,
folderId: folderId,

View File

@ -1,9 +1,10 @@
import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import type { Recipient, Template, TemplateDirectLink } from '@prisma/client';
import type { Recipient, TemplateDirectLink } from '@prisma/client';
import { LinkIcon } from 'lucide-react';
import type { Template } from '@documenso/prisma/types/template-legacy-schema';
import { Button } from '@documenso/ui/primitives/button';
import { TemplateDirectLinkDialog } from '~/components/dialogs/template-direct-link-dialog';

View File

@ -3,12 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import {
type Recipient,
RecipientRole,
type Template,
type TemplateDirectLink,
} from '@prisma/client';
import { type Recipient, RecipientRole, type TemplateDirectLink } from '@prisma/client';
import { CircleDotIcon, CircleIcon, ClipboardCopyIcon, InfoIcon, LoaderIcon } from 'lucide-react';
import { Link, useRevalidator } from 'react-router';
import { P, match } from 'ts-pattern';
@ -20,6 +15,7 @@ import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { DIRECT_TEMPLATE_DOCUMENTATION } from '@documenso/lib/constants/template';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import type { Template } from '@documenso/prisma/types/template-legacy-schema';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';

View File

@ -83,7 +83,7 @@ export function TemplateMoveToFolderDialog({
},
);
const { mutateAsync: moveTemplateToFolder } = trpc.folder.moveTemplateToFolder.useMutation();
const { mutateAsync: updateTemplate } = trpc.template.updateTemplate.useMutation();
useEffect(() => {
if (!isOpen) {
@ -96,9 +96,11 @@ export function TemplateMoveToFolderDialog({
const onSubmit = async (data: TMoveTemplateFormSchema) => {
try {
await moveTemplateToFolder({
await updateTemplate({
templateId,
folderId: data.folderId ?? null,
data: {
folderId: data.folderId ?? null,
},
});
toast({

View File

@ -118,6 +118,9 @@ export const ConfigureFieldsView = ({
sendStatus: signer.disabled ? SendStatus.SENT : SendStatus.NOT_SENT,
readStatus: signer.disabled ? ReadStatus.OPENED : ReadStatus.NOT_OPENED,
signingStatus: signer.disabled ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
// Todo: Envelopes - Dummy data
envelopeId: '',
}));
}, [configData.signers]);

View File

@ -15,12 +15,14 @@ import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { trpc } from '@documenso/trpc/react';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import {
type DocumentField,
DocumentReadOnlyFields,
} from '@documenso/ui/components/document/document-read-only-fields';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';

View File

@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { type Document, FieldType, type Passkey, type Recipient } from '@prisma/client';
import { type Envelope, FieldType, type Passkey, type Recipient } from '@prisma/client';
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
@ -26,9 +26,9 @@ type PasskeyData = {
export type DocumentSigningAuthContextValue = {
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;
documentAuthOptions: Document['authOptions'];
documentAuthOptions: Envelope['authOptions'];
documentAuthOption: TDocumentAuthOptions;
setDocumentAuthOptions: (_value: Document['authOptions']) => void;
setDocumentAuthOptions: (_value: Envelope['authOptions']) => void;
recipient: Recipient;
recipientAuthOption: TRecipientAuthOptions;
setRecipient: (_value: Recipient) => void;
@ -61,7 +61,7 @@ export const useRequiredDocumentSigningAuthContext = () => {
};
export interface DocumentSigningAuthProviderProps {
documentAuthOptions: Document['authOptions'];
documentAuthOptions: Envelope['authOptions'];
recipient: Recipient;
user?: SessionUser | null;
children: React.ReactNode;

View File

@ -3,12 +3,12 @@ import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import type { Document } from '@prisma/client';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { useSearchParams } from 'react-router';
import { z } from 'zod';
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {

View File

@ -4,6 +4,7 @@ import { Trans } from '@lingui/react/macro';
import type { DocumentData } from '@prisma/client';
import { DateTime } from 'luxon';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -23,6 +24,7 @@ export type DocumentCertificateQRViewProps = {
title: string;
documentData: DocumentData;
password?: string | null;
documentTeamUrl: string;
recipientCount?: number;
completedDate?: Date;
};
@ -32,29 +34,30 @@ export const DocumentCertificateQRView = ({
title,
documentData,
password,
documentTeamUrl,
recipientCount = 0,
completedDate,
}: DocumentCertificateQRViewProps) => {
const { data: documentUrl } = trpc.shareLink.getDocumentInternalUrlForQRCode.useQuery({
const { data: documentViaUser } = trpc.document.get.useQuery({
documentId,
});
const [isDialogOpen, setIsDialogOpen] = useState(() => !!documentUrl);
const [isDialogOpen, setIsDialogOpen] = useState(() => !!documentViaUser);
const formattedDate = completedDate
? DateTime.fromJSDate(completedDate).toLocaleString(DateTime.DATETIME_MED)
: '';
useEffect(() => {
if (documentUrl) {
if (documentViaUser) {
setIsDialogOpen(true);
}
}, [documentUrl]);
}, [documentViaUser]);
return (
<div className="mx-auto w-full max-w-screen-md">
{/* Dialog for internal document link */}
{documentUrl && (
{documentViaUser && (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader>
@ -72,7 +75,11 @@ export const DocumentCertificateQRView = ({
<DialogFooter className="flex flex-row justify-end gap-2">
<Button asChild>
<a href={documentUrl} target="_blank" rel="noopener noreferrer">
<a
href={`${formatDocumentsPath(documentTeamUrl)}/${documentViaUser.id}`}
target="_blank"
rel="noopener noreferrer"
>
<Trans>Go to document</Trans>
</a>
</Button>

View File

@ -64,7 +64,7 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
const response = await putPdfFile(file);
const { id } = await createDocument({
const { legacyDocumentId: id } = await createDocument({
title: file.name,
documentDataId: response.id,
timezone: userTimezone, // Note: When migrating to v2 document upload remember to pass this through as a 'userTimezone' field.

View File

@ -1,7 +1,7 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Document, Recipient, Team, User } from '@prisma/client';
import type { Recipient, Team, User } from '@prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
import { Link } from 'react-router';
@ -11,6 +11,7 @@ import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';

View File

@ -3,7 +3,7 @@ import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Document, Recipient, Team, User } from '@prisma/client';
import type { Recipient, Team, User } from '@prisma/client';
import { DocumentStatus } from '@prisma/client';
import {
Copy,
@ -19,6 +19,7 @@ import { Link, useNavigate } from 'react-router';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TDocument } from '@documenso/lib/types/document';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc as trpcClient } from '@documenso/trpc/client';
@ -39,7 +40,7 @@ import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/d
import { useCurrentTeam } from '~/providers/team';
export type DocumentPageViewDropdownProps = {
document: Document & {
document: TDocument & {
user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;

View File

@ -3,10 +3,11 @@ import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Document, Recipient, User } from '@prisma/client';
import type { Recipient, User } from '@prisma/client';
import { DateTime } from 'luxon';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
export type DocumentPageViewInformationProps = {
userId: number;

View File

@ -2,7 +2,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import type { Document, Recipient } from '@prisma/client';
import type { Recipient } from '@prisma/client';
import {
AlertTriangle,
CheckIcon,
@ -19,6 +19,7 @@ import { match } from 'ts-pattern';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';

View File

@ -75,7 +75,7 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
const response = await putPdfFile(file);
const { id } = await createDocument({
const { legacyDocumentId: id } = await createDocument({
title: file.name,
documentDataId: response.id,
timezone: userTimezone, // Note: When migrating to v2 document upload remember to pass this through as a 'userTimezone' field.

View File

@ -42,7 +42,7 @@ export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZon
const documentData = await putPdfFile(file);
const { id } = await createTemplate({
const { legacyTemplateId: id } = await createTemplate({
title: file.name,
templateDocumentDataId: documentData.id,
folderId: folderId ?? undefined,

View File

@ -3,10 +3,11 @@ import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Template, User } from '@prisma/client';
import type { User } from '@prisma/client';
import { DateTime } from 'luxon';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import type { Template } from '@documenso/prisma/types/template-legacy-schema';
export type TemplatePageViewInformationProps = {
userId: number;

View File

@ -1,13 +1,14 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Recipient, Template } from '@prisma/client';
import type { Recipient } from '@prisma/client';
import { PenIcon, PlusIcon } from 'lucide-react';
import { Link } from 'react-router';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { Template } from '@documenso/prisma/types/template-legacy-schema';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
export type TemplatePageViewRecipientsProps = {

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import type { Document, Role, Subscription } from '@prisma/client';
import type { Role, Subscription } from '@prisma/client';
import { Edit, Loader } from 'lucide-react';
import { Link } from 'react-router';
@ -20,7 +20,7 @@ type UserData = {
email: string;
roles: Role[];
subscriptions?: SubscriptionLite[] | null;
documents: DocumentLite[];
documentCount: number;
};
type SubscriptionLite = Pick<
@ -28,8 +28,6 @@ type SubscriptionLite = Pick<
'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd'
>;
type DocumentLite = Pick<Document, 'id'>;
type AdminDashboardUsersTableProps = {
users: UserData[];
totalPages: number;
@ -74,10 +72,7 @@ export const AdminDashboardUsersTable = ({
},
{
header: _(msg`Documents`),
accessorKey: 'documents',
cell: ({ row }) => {
return <div>{row.original.documents?.length}</div>;
},
accessorKey: 'documentCount',
},
{
header: '',

View File

@ -3,8 +3,7 @@ import { useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TemplateDirectLink } from '@prisma/client';
import { TemplateType } from '@prisma/client';
import { type TemplateDirectLink, TemplateType } from '@prisma/client';
import { EditIcon, FileIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';

View File

@ -1,11 +1,12 @@
import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import type { Recipient, Template, TemplateDirectLink } from '@prisma/client';
import type { Recipient, TemplateDirectLink } from '@prisma/client';
import { Copy, Edit, FolderIcon, MoreHorizontal, Share2Icon, Trash2, Upload } from 'lucide-react';
import { Link } from 'react-router';
import { useSession } from '@documenso/lib/client-only/providers/session';
import type { Template } from '@documenso/prisma/types/template-legacy-schema';
import {
DropdownMenu,
DropdownMenuContent,

View File

@ -1,11 +1,11 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { SigningStatus } from '@prisma/client';
import { EnvelopeType, SigningStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { Link, redirect } from 'react-router';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document';
import { trpc } from '@documenso/trpc/react';
import {
Accordion,
@ -36,13 +36,19 @@ export async function loader({ params }: Route.LoaderArgs) {
throw redirect('/admin/documents');
}
const document = await getEntireDocument({ id });
const envelope = await unsafeGetEntireEnvelope({
id: {
type: 'documentId',
id,
},
type: EnvelopeType.DOCUMENT,
});
return { document };
return { envelope };
}
export default function AdminDocumentDetailsPage({ loaderData }: Route.ComponentProps) {
const { document } = loaderData;
const { envelope } = loaderData;
const { _, i18n } = useLingui();
const { toast } = useToast();
@ -68,11 +74,11 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
<div>
<div className="flex items-start justify-between">
<div className="flex items-center gap-x-4">
<h1 className="text-2xl font-semibold">{document.title}</h1>
<DocumentStatus status={document.status} />
<h1 className="text-2xl font-semibold">{envelope.title}</h1>
<DocumentStatus status={envelope.status} />
</div>
{document.deletedAt && (
{envelope.deletedAt && (
<Badge size="large" variant="destructive">
<Trans>Deleted</Trans>
</Badge>
@ -81,11 +87,11 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
<div className="text-muted-foreground mt-4 text-sm">
<div>
<Trans>Created on</Trans>: {i18n.date(document.createdAt, DateTime.DATETIME_MED)}
<Trans>Created on</Trans>: {i18n.date(envelope.createdAt, DateTime.DATETIME_MED)}
</div>
<div>
<Trans>Last updated at</Trans>: {i18n.date(document.updatedAt, DateTime.DATETIME_MED)}
<Trans>Last updated at</Trans>: {i18n.date(envelope.updatedAt, DateTime.DATETIME_MED)}
</div>
</div>
@ -102,12 +108,12 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
<Button
variant="outline"
loading={isResealDocumentLoading}
disabled={document.recipients.some(
disabled={envelope.recipients.some(
(recipient) =>
recipient.signingStatus !== SigningStatus.SIGNED &&
recipient.signingStatus !== SigningStatus.REJECTED,
)}
onClick={() => resealDocument({ id: document.id })}
onClick={() => resealDocument({ id: envelope.id })}
>
<Trans>Reseal document</Trans>
</Button>
@ -123,7 +129,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
</TooltipProvider>
<Button variant="outline" asChild>
<Link to={`/admin/users/${document.userId}`}>
<Link to={`/admin/users/${envelope.userId}`}>
<Trans>Go to owner</Trans>
</Link>
</Button>
@ -136,7 +142,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
<div className="mt-4">
<Accordion type="multiple" className="space-y-4">
{document.recipients.map((recipient) => (
{envelope.recipients.map((recipient) => (
<AccordionItem
key={recipient.id}
value={recipient.id.toString()}
@ -161,7 +167,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
<hr className="my-4" />
{document && <AdminDocumentDeleteDialog document={document} />}
{envelope && <AdminDocumentDeleteDialog envelopeId={envelope.id} />}
</div>
);
}

View File

@ -12,7 +12,10 @@ import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { logDocumentAccess } from '@documenso/lib/utils/logger';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '@documenso/ui/components/document/document-read-only-fields';
import { Badge } from '@documenso/ui/primitives/badge';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
@ -54,7 +57,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
}
const document = await getDocumentWithDetailsById({
documentId,
id: {
type: 'documentId',
id: documentId,
},
userId: user.id,
teamId: team.id,
}).catch(() => null);
@ -171,7 +177,7 @@ export default function DocumentPage() {
{document.status !== DocumentStatus.COMPLETED && (
<DocumentReadOnlyFields
fields={document.fields}
fields={mapFieldsWithRecipients(document.fields, recipients)}
documentMeta={documentMeta || undefined}
showRecipientTooltip={true}
showRecipientColors={true}

View File

@ -42,7 +42,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
}
const document = await getDocumentWithDetailsById({
documentId,
id: {
type: 'documentId',
id: documentId,
},
userId: user.id,
teamId: team.id,
}).catch(() => null);

View File

@ -2,15 +2,16 @@ import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Recipient } from '@prisma/client';
import { EnvelopeType, type Recipient } from '@prisma/client';
import { ChevronLeft } from 'lucide-react';
import { DateTime } from 'luxon';
import { Link, redirect } from 'react-router';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { logDocumentAccess } from '@documenso/lib/utils/logger';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Card } from '@documenso/ui/primitives/card';
@ -40,13 +41,17 @@ export async function loader({ params, request }: Route.LoaderArgs) {
throw redirect(documentRootPath);
}
const document = await getDocumentById({
documentId,
const envelope = await getEnvelopeById({
id: {
type: 'documentId',
id: documentId,
},
type: EnvelopeType.DOCUMENT,
userId: user.id,
teamId: team?.id,
teamId: team.id,
}).catch(() => null);
if (!document || !document.documentData) {
if (!envelope) {
throw redirect(documentRootPath);
}
@ -63,7 +68,19 @@ export async function loader({ params, request }: Route.LoaderArgs) {
});
return {
document,
// Only return necessary data
document: {
id: mapSecondaryIdToDocumentId(envelope.secondaryId),
title: envelope.title,
status: envelope.status,
user: {
name: envelope.user.name,
email: envelope.user.email,
},
createdAt: envelope.createdAt,
updatedAt: envelope.updatedAt,
documentMeta: envelope.documentMeta,
},
recipients,
documentRootPath,
};

View File

@ -2,8 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type { TemplateDirectLink } from '@prisma/client';
import { TemplateType } from '@prisma/client';
import { type TemplateDirectLink, TemplateType } from '@prisma/client';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { useSession } from '@documenso/lib/client-only/providers/session';

View File

@ -1,14 +1,16 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { EnvelopeType } from '@prisma/client';
import { DateTime } from 'luxon';
import { redirect } from 'react-router';
import { DOCUMENT_STATUS } from '@documenso/lib/constants/document';
import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { getTranslations } from '@documenso/lib/utils/i18n';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -39,20 +41,24 @@ export async function loader({ request }: Route.LoaderArgs) {
const documentId = Number(rawDocumentId);
const document = await getEntireDocument({
id: documentId,
const envelope = await unsafeGetEntireEnvelope({
id: {
type: 'documentId',
id: documentId,
},
type: EnvelopeType.DOCUMENT,
}).catch(() => null);
if (!document) {
if (!envelope) {
throw redirect('/');
}
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
const documentLanguage = ZSupportedLanguageCodeSchema.parse(envelope.documentMeta?.language);
const { data: auditLogs } = await findDocumentAuditLogs({
documentId: documentId,
userId: document.userId,
teamId: document.teamId,
userId: envelope.userId,
teamId: envelope.teamId,
perPage: 100_000,
});
@ -60,7 +66,20 @@ export async function loader({ request }: Route.LoaderArgs) {
return {
auditLogs,
document,
document: {
id: mapSecondaryIdToDocumentId(envelope.secondaryId),
title: envelope.title,
status: envelope.status,
user: {
name: envelope.user.name,
email: envelope.user.email,
},
recipients: envelope.recipients,
createdAt: envelope.createdAt,
updatedAt: envelope.updatedAt,
deletedAt: envelope.deletedAt,
documentMeta: envelope.documentMeta,
},
documentLanguage,
messages,
};
@ -90,6 +109,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
<Card>
<CardContent className="grid grid-cols-2 gap-4 p-6 text-sm print:text-xs">
<p>
{/* Todo: Envelopes - Should we should envelope ID instead here? */}
<span className="font-medium">{_(msg`Document ID`)}</span>
<span className="mt-1 block break-words">{document.id}</span>

View File

@ -1,6 +1,6 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { FieldType, SigningStatus } from '@prisma/client';
import { EnvelopeType, FieldType, SigningStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { redirect } from 'react-router';
import { prop, sortBy } from 'remeda';
@ -14,12 +14,13 @@ import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_SIGNING_REASONS,
} from '@documenso/lib/constants/recipient-roles';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
import { getDocumentCertificateAuditLogs } from '@documenso/lib/server-only/document/get-document-certificate-audit-logs';
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { getTranslations } from '@documenso/lib/utils/i18n';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import {
@ -55,26 +56,45 @@ export async function loader({ request }: Route.LoaderArgs) {
const documentId = Number(rawDocumentId);
const document = await getEntireDocument({
id: documentId,
const envelope = await unsafeGetEntireEnvelope({
id: {
type: 'documentId',
id: documentId,
},
type: EnvelopeType.DOCUMENT,
}).catch(() => null);
if (!document) {
if (!envelope) {
throw redirect('/');
}
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: document.teamId });
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: envelope.teamId });
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
const documentLanguage = ZSupportedLanguageCodeSchema.parse(envelope.documentMeta?.language);
const auditLogs = await getDocumentCertificateAuditLogs({
id: documentId,
envelopeId: envelope.id,
});
const messages = await getTranslations(documentLanguage);
return {
document,
document: {
id: mapSecondaryIdToDocumentId(envelope.secondaryId),
title: envelope.title,
status: envelope.status,
user: {
name: envelope.user.name,
email: envelope.user.email,
},
qrToken: envelope.qrToken,
authOptions: envelope.authOptions,
recipients: envelope.recipients,
createdAt: envelope.createdAt,
updatedAt: envelope.updatedAt,
deletedAt: envelope.deletedAt,
documentMeta: envelope.documentMeta,
},
hidePoweredBy: organisationClaim.flags.hidePoweredBy,
documentLanguage,
auditLogs,

View File

@ -3,7 +3,7 @@ import { useEffect } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type Document, DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
import { CheckCircle2, Clock8, FileSearch } from 'lucide-react';
import { Link, useRevalidator } from 'react-router';
import { match } from 'ts-pattern';
@ -19,6 +19,7 @@ import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { env } from '@documenso/lib/utils/env';
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';

View File

@ -1,11 +1,11 @@
import { Trans } from '@lingui/react/macro';
import type { Team } from '@prisma/client';
import { DocumentStatus } from '@prisma/client';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { Link, redirect } from 'react-router';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
@ -40,12 +40,16 @@ export async function loader({ params, request }: Route.LoaderArgs) {
let team: Team | null = null;
if (user) {
isOwnerOrTeamMember = await getDocumentById({
documentId: document.id,
isOwnerOrTeamMember = await getEnvelopeById({
id: {
type: 'documentId',
id: document.id,
},
type: EnvelopeType.DOCUMENT,
userId: user.id,
teamId: document.teamId ?? undefined,
})
.then((document) => !!document)
.then((envelope) => !!envelope)
.catch(() => false);
if (document.teamId) {

View File

@ -81,9 +81,9 @@ export default function SharePage() {
<DocumentCertificateQRView
documentId={document.id}
title={document.title}
documentData={document.documentData}
password={document.documentMeta?.password}
recipientCount={document.recipients?.length ?? 0}
documentTeamUrl={document.documentTeamUrl}
documentData={document.documentData} // Todo: Envelopes
recipientCount={document.recipientCount}
completedDate={document.completedAt ?? undefined}
/>
);

View File

@ -54,7 +54,10 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
}
const document = await getDocumentWithDetailsById({
documentId,
id: {
type: 'documentId',
id: documentId,
},
userId: result?.userId,
teamId: result?.teamId ?? undefined,
}).catch(() => null);

View File

@ -1,6 +1,11 @@
import { EnvelopeType } from '@prisma/client';
import type { Context } from 'hono';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import {
mapDocumentIdToSecondaryId,
mapTemplateIdToSecondaryId,
} from '@documenso/lib/utils/envelope';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
@ -104,9 +109,10 @@ async function hasAccessToDocument(c: Context, documentId: number): Promise<stri
const userId = session.user.id;
const document = await prisma.document.findUnique({
const envelope = await prisma.envelope.findUnique({
where: {
id: documentId,
type: EnvelopeType.DOCUMENT,
secondaryId: mapDocumentIdToSecondaryId(documentId),
team: buildTeamWhereQuery({
userId,
teamId: undefined,
@ -121,7 +127,7 @@ async function hasAccessToDocument(c: Context, documentId: number): Promise<stri
},
});
return document ? document.team.url : null;
return envelope ? envelope.team.url : null;
}
async function hasAccessToFolder(c: Context, folderId: string): Promise<string | null> {
@ -154,9 +160,10 @@ async function hasAccessToTemplate(c: Context, templateId: number): Promise<stri
const userId = session.user.id;
const template = await prisma.template.findUnique({
const envelope = await prisma.envelope.findUnique({
where: {
id: templateId,
type: EnvelopeType.TEMPLATE,
secondaryId: mapTemplateIdToSecondaryId(templateId),
team: buildTeamWhereQuery({
userId,
teamId: undefined,
@ -171,5 +178,5 @@ async function hasAccessToTemplate(c: Context, templateId: number): Promise<stri
},
});
return template ? template.team.url : null;
return envelope ? envelope.team.url : null;
}