mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
3 Commits
v1.10.3
...
feat/copy-
| Author | SHA1 | Date | |
|---|---|---|---|
| de31769415 | |||
| cc3995ce74 | |||
| 8b95b9a7c0 |
@ -4,7 +4,7 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { AlertTriangle, CheckCheckIcon, CheckIcon, Loader, MailOpen } from 'lucide-react';
|
import { AlertTriangle, CheckCheckIcon, CheckIcon, Copy, Loader, MailOpen } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@ -141,6 +141,11 @@ export const DocumentPageViewRecentActivity = ({
|
|||||||
<MailOpen className="h-3 w-3" aria-hidden="true" />
|
<MailOpen className="h-3 w-3" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_LINK_COPIED, () => (
|
||||||
|
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
|
||||||
|
<Copy className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
.otherwise(() => (
|
.otherwise(() => (
|
||||||
<div className="bg-widget h-1.5 w-1.5 rounded-full ring-1 ring-gray-300 dark:ring-neutral-600" />
|
<div className="bg-widget h-1.5 w-1.5 rounded-full ring-1 ring-gray-300 dark:ring-neutral-600" />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -17,8 +17,9 @@ import { match } from 'ts-pattern';
|
|||||||
|
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
import type { Document, Recipient } from '@documenso/prisma/client';
|
import type { Document, Recipient } from '@documenso/prisma/client';
|
||||||
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||||
@ -40,8 +41,29 @@ export const DocumentPageViewRecipients = ({
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { mutateAsync: createAuditLog } = trpc.document.createAuditLog.useMutation();
|
||||||
|
|
||||||
const recipients = document.recipients;
|
const recipients = document.recipients;
|
||||||
|
|
||||||
|
const onCopyLink = (recipient: Recipient) => {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Copied to clipboard`),
|
||||||
|
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||||
|
});
|
||||||
|
|
||||||
|
void createAuditLog({
|
||||||
|
documentId: document.id,
|
||||||
|
type: 'DOCUMENT_SIGNING_LINK_COPIED',
|
||||||
|
data: {
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
isBulkCopy: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">
|
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">
|
||||||
<div className="flex flex-row items-center justify-between px-4 py-3">
|
<div className="flex flex-row items-center justify-between px-4 py-3">
|
||||||
@ -157,12 +179,7 @@ export const DocumentPageViewRecipients = ({
|
|||||||
recipient.role !== RecipientRole.CC && (
|
recipient.role !== RecipientRole.CC && (
|
||||||
<CopyTextButton
|
<CopyTextButton
|
||||||
value={formatSigningLink(recipient.token)}
|
value={formatSigningLink(recipient.token)}
|
||||||
onCopySuccess={() => {
|
onCopySuccess={() => void onCopyLink(recipient)}
|
||||||
toast({
|
|
||||||
title: _(msg`Copied to clipboard`),
|
|
||||||
description: _(msg`The signing link has been copied to your clipboard.`),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,9 +15,8 @@ import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/g
|
|||||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
import type { Team, TeamEmail } from '@documenso/prisma/client';
|
import type { Team, TeamEmail } from '@documenso/prisma/client';
|
||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
import { Badge } from '@documenso/ui/primitives/badge';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@ -137,7 +136,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||||
{document.status === DocumentStatus.PENDING && (
|
{document.status === DocumentStatus.PENDING && (
|
||||||
<DocumentRecipientLinkCopyDialog recipients={recipients} />
|
<DocumentRecipientLinkCopyDialog recipients={recipients} documentId={document.id} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
||||||
@ -169,6 +168,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
documentStatus={document.status}
|
documentStatus={document.status}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
|
documentId={document.id}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<Trans>{recipients.length} Recipient(s)</Trans>
|
<Trans>{recipients.length} Recipient(s)</Trans>
|
||||||
|
|||||||
@ -13,8 +13,7 @@ import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
|||||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import type { Team } from '@documenso/prisma/client';
|
import type { Team } from '@documenso/prisma/client';
|
||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
import { DocumentStatus as InternalDocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
|
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
|
||||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||||
@ -127,6 +126,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
|||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
documentStatus={document.status}
|
documentStatus={document.status}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
|
documentId={document.id}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<Plural one="1 Recipient" other="# Recipients" value={recipients.length} />
|
<Plural one="1 Recipient" other="# Recipients" value={recipients.length} />
|
||||||
|
|||||||
@ -23,8 +23,8 @@ import { useSession } from 'next-auth/react';
|
|||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
|
|
||||||
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
||||||
|
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import {
|
import {
|
||||||
@ -190,6 +190,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
{canManageDocument && (
|
{canManageDocument && (
|
||||||
<DocumentRecipientLinkCopyDialog
|
<DocumentRecipientLinkCopyDialog
|
||||||
recipients={row.recipients}
|
recipients={row.recipients}
|
||||||
|
documentId={row.id}
|
||||||
trigger={
|
trigger={
|
||||||
<DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export const DocumentsDataTable = ({
|
|||||||
<StackAvatarsWithTooltip
|
<StackAvatarsWithTooltip
|
||||||
recipients={row.original.recipients}
|
recipients={row.original.recipients}
|
||||||
documentStatus={row.original.status}
|
documentStatus={row.original.status}
|
||||||
|
documentId={row.original.id}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -117,6 +117,7 @@ export const TemplatePageViewDocumentsTable = ({
|
|||||||
<StackAvatarsWithTooltip
|
<StackAvatarsWithTooltip
|
||||||
recipients={row.original.recipients}
|
recipients={row.original.recipients}
|
||||||
documentStatus={row.original.status}
|
documentStatus={row.original.status}
|
||||||
|
documentId={row.original.id}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
|
||||||
@ -12,6 +10,7 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
|
|||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@ -20,14 +19,21 @@ import { StackAvatar } from './stack-avatar';
|
|||||||
export type AvatarWithRecipientProps = {
|
export type AvatarWithRecipientProps = {
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
documentStatus: DocumentStatus;
|
documentStatus: DocumentStatus;
|
||||||
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRecipientProps) {
|
export function AvatarWithRecipient({
|
||||||
|
recipient,
|
||||||
|
documentStatus,
|
||||||
|
documentId,
|
||||||
|
}: AvatarWithRecipientProps) {
|
||||||
const [, copy] = useCopyToClipboard();
|
const [, copy] = useCopyToClipboard();
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { mutateAsync: createAuditLog } = trpc.document.createAuditLog.useMutation();
|
||||||
|
|
||||||
const signingToken = documentStatus === DocumentStatus.PENDING ? recipient.token : null;
|
const signingToken = documentStatus === DocumentStatus.PENDING ? recipient.token : null;
|
||||||
|
|
||||||
const onRecipientClick = () => {
|
const onRecipientClick = () => {
|
||||||
@ -40,6 +46,18 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
|
|||||||
title: _(msg`Copied to clipboard`),
|
title: _(msg`Copied to clipboard`),
|
||||||
description: _(msg`The signing link has been copied to your clipboard.`),
|
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void createAuditLog({
|
||||||
|
documentId,
|
||||||
|
type: 'DOCUMENT_SIGNING_LINK_COPIED',
|
||||||
|
data: {
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
isBulkCopy: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export type StackAvatarsWithTooltipProps = {
|
|||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
position?: 'top' | 'bottom';
|
position?: 'top' | 'bottom';
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StackAvatarsWithTooltip = ({
|
export const StackAvatarsWithTooltip = ({
|
||||||
@ -27,6 +28,7 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
recipients,
|
recipients,
|
||||||
position,
|
position,
|
||||||
children,
|
children,
|
||||||
|
documentId,
|
||||||
}: StackAvatarsWithTooltipProps) => {
|
}: StackAvatarsWithTooltipProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
@ -129,6 +131,7 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
key={recipient.id}
|
key={recipient.id}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
documentStatus={documentStatus}
|
documentStatus={documentStatus}
|
||||||
|
documentId={documentId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -144,6 +147,7 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
key={recipient.id}
|
key={recipient.id}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
documentStatus={documentStatus}
|
documentStatus={documentStatus}
|
||||||
|
documentId={documentId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -159,6 +163,7 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
key={recipient.id}
|
key={recipient.id}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
documentStatus={documentStatus}
|
documentStatus={documentStatus}
|
||||||
|
documentId={documentId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
|
|||||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -32,11 +33,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
export type DocumentRecipientLinkCopyDialogProps = {
|
export type DocumentRecipientLinkCopyDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentRecipientLinkCopyDialog = ({
|
export const DocumentRecipientLinkCopyDialog = ({
|
||||||
trigger,
|
trigger,
|
||||||
recipients,
|
recipients,
|
||||||
|
documentId,
|
||||||
}: DocumentRecipientLinkCopyDialogProps) => {
|
}: DocumentRecipientLinkCopyDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -50,6 +53,29 @@ export const DocumentRecipientLinkCopyDialog = ({
|
|||||||
|
|
||||||
const actionSearchParam = searchParams?.get('action');
|
const actionSearchParam = searchParams?.get('action');
|
||||||
|
|
||||||
|
const { mutateAsync: createAuditLog } = trpc.document.createAuditLog.useMutation();
|
||||||
|
|
||||||
|
const onCopyLink = async (recipient: Recipient) => {
|
||||||
|
await copy(formatSigningLink(recipient.token)).then(() => {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Copied to clipboard`),
|
||||||
|
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||||
|
});
|
||||||
|
|
||||||
|
void createAuditLog({
|
||||||
|
documentId,
|
||||||
|
type: 'DOCUMENT_SIGNING_LINK_COPIED',
|
||||||
|
data: {
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
isBulkCopy: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onBulkCopy = async () => {
|
const onBulkCopy = async () => {
|
||||||
const generatedString = recipients
|
const generatedString = recipients
|
||||||
.filter((recipient) => recipient.role !== RecipientRole.CC)
|
.filter((recipient) => recipient.role !== RecipientRole.CC)
|
||||||
@ -61,6 +87,24 @@ export const DocumentRecipientLinkCopyDialog = ({
|
|||||||
title: _(msg`Copied to clipboard`),
|
title: _(msg`Copied to clipboard`),
|
||||||
description: _(msg`All signing links have been copied to your clipboard.`),
|
description: _(msg`All signing links have been copied to your clipboard.`),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void Promise.all(
|
||||||
|
recipients
|
||||||
|
.filter((recipient) => recipient.role !== RecipientRole.CC)
|
||||||
|
.map(async (recipient) =>
|
||||||
|
createAuditLog({
|
||||||
|
documentId,
|
||||||
|
type: 'DOCUMENT_SIGNING_LINK_COPIED',
|
||||||
|
data: {
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
isBulkCopy: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -112,12 +156,7 @@ export const DocumentRecipientLinkCopyDialog = ({
|
|||||||
{recipient.role !== RecipientRole.CC && (
|
{recipient.role !== RecipientRole.CC && (
|
||||||
<CopyTextButton
|
<CopyTextButton
|
||||||
value={formatSigningLink(recipient.token)}
|
value={formatSigningLink(recipient.token)}
|
||||||
onCopySuccess={() => {
|
onCopySuccess={async () => onCopyLink(recipient)}
|
||||||
toast({
|
|
||||||
title: _(msg`Copied to clipboard`),
|
|
||||||
description: _(msg`The signing link has been copied to your clipboard.`),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
badgeContentUncopied={
|
badgeContentUncopied={
|
||||||
<p className="ml-1 text-xs">
|
<p className="ml-1 text-xs">
|
||||||
<Trans>Copy</Trans>
|
<Trans>Copy</Trans>
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export const findDocumentAuditLogs = async ({
|
|||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
|
||||||
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_LINK_COPIED,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
|||||||
'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
|
'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
|
||||||
'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated.
|
'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated.
|
||||||
'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team.
|
'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team.
|
||||||
|
'DOCUMENT_SIGNING_LINK_COPIED', // When a signing link is copied.
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const ZDocumentAuditLogEmailTypeSchema = z.enum([
|
export const ZDocumentAuditLogEmailTypeSchema = z.enum([
|
||||||
@ -225,6 +226,16 @@ export const ZDocumentAuditLogEventDocumentDeletedSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event: Document signing link copied.
|
||||||
|
*/
|
||||||
|
export const ZDocumentAuditLogEventDocumentSigningLinkCopiedSchema = z.object({
|
||||||
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_LINK_COPIED),
|
||||||
|
data: ZBaseRecipientDataSchema.extend({
|
||||||
|
isBulkCopy: z.boolean(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event: Document field inserted.
|
* Event: Document field inserted.
|
||||||
*/
|
*/
|
||||||
@ -490,6 +501,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
|||||||
ZDocumentAuditLogEventDocumentCompletedSchema,
|
ZDocumentAuditLogEventDocumentCompletedSchema,
|
||||||
ZDocumentAuditLogEventDocumentCreatedSchema,
|
ZDocumentAuditLogEventDocumentCreatedSchema,
|
||||||
ZDocumentAuditLogEventDocumentDeletedSchema,
|
ZDocumentAuditLogEventDocumentDeletedSchema,
|
||||||
|
ZDocumentAuditLogEventDocumentSigningLinkCopiedSchema,
|
||||||
ZDocumentAuditLogEventDocumentMovedToTeamSchema,
|
ZDocumentAuditLogEventDocumentMovedToTeamSchema,
|
||||||
ZDocumentAuditLogEventDocumentFieldInsertedSchema,
|
ZDocumentAuditLogEventDocumentFieldInsertedSchema,
|
||||||
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
||||||
|
|||||||
@ -385,6 +385,10 @@ export const formatDocumentAuditLogAction = (
|
|||||||
anonymous: msg`Document completed`,
|
anonymous: msg`Document completed`,
|
||||||
identified: msg`Document completed`,
|
identified: msg`Document completed`,
|
||||||
}))
|
}))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_LINK_COPIED }, () => ({
|
||||||
|
anonymous: msg`Document signing link copied`,
|
||||||
|
identified: msg`${prefix} copied the document signing link`,
|
||||||
|
}))
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -25,10 +25,13 @@ import { sendDocument } from '@documenso/lib/server-only/document/send-document'
|
|||||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||||
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||||
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentDataType, DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentDataType, DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
|
ZCreateAuditLogMutationSchema,
|
||||||
ZCreateDocumentRequestSchema,
|
ZCreateDocumentRequestSchema,
|
||||||
ZCreateDocumentV2RequestSchema,
|
ZCreateDocumentV2RequestSchema,
|
||||||
ZCreateDocumentV2ResponseSchema,
|
ZCreateDocumentV2ResponseSchema,
|
||||||
@ -626,4 +629,37 @@ export const documentRouter = router({
|
|||||||
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
|
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
createAuditLog: authenticatedProcedure
|
||||||
|
.input(ZCreateAuditLogMutationSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { teamId } = ctx;
|
||||||
|
const { documentId, type, data } = input;
|
||||||
|
|
||||||
|
const document = await getDocumentById({
|
||||||
|
documentId,
|
||||||
|
userId: ctx.user.id,
|
||||||
|
teamId,
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
if (!document || (teamId && document.teamId !== teamId)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
message: 'You do not have access to this document.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await prisma.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
documentId,
|
||||||
|
user: ctx.user,
|
||||||
|
metadata: ctx.metadata,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -326,6 +326,18 @@ export const ZDownloadCertificateMutationSchema = z.object({
|
|||||||
documentId: z.number(),
|
documentId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZCreateAuditLogMutationSchema = z.object({
|
||||||
|
documentId: z.number(),
|
||||||
|
type: z.literal('DOCUMENT_SIGNING_LINK_COPIED'),
|
||||||
|
data: z.object({
|
||||||
|
recipientEmail: z.string(),
|
||||||
|
recipientName: z.string(),
|
||||||
|
recipientId: z.number(),
|
||||||
|
recipientRole: z.string(),
|
||||||
|
isBulkCopy: z.boolean(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZMoveDocumentToTeamSchema = z.object({
|
export const ZMoveDocumentToTeamSchema = z.object({
|
||||||
documentId: z.number().describe('The ID of the document to move to a team.'),
|
documentId: z.number().describe('The ID of the document to move to a team.'),
|
||||||
teamId: z.number().describe('The ID of the team to move the document to.'),
|
teamId: z.number().describe('The ID of the team to move the document to.'),
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
RecipientRole,
|
RecipientRole,
|
||||||
} from '@documenso/prisma/client';
|
} from '@documenso/prisma/client';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
@ -58,6 +59,8 @@ export const AddSubjectFormPartial = ({
|
|||||||
}: AddSubjectFormProps) => {
|
}: AddSubjectFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
|
const { mutateAsync: createAuditLog } = trpc.document.createAuditLog.useMutation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -98,6 +101,25 @@ export const AddSubjectFormPartial = ({
|
|||||||
const onFormSubmit = handleSubmit(onSubmit);
|
const onFormSubmit = handleSubmit(onSubmit);
|
||||||
const { currentStep, totalSteps, previousStep } = useStep();
|
const { currentStep, totalSteps, previousStep } = useStep();
|
||||||
|
|
||||||
|
const onCopyLink = (recipient: Recipient) => {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Copied to clipboard`),
|
||||||
|
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||||
|
});
|
||||||
|
|
||||||
|
void createAuditLog({
|
||||||
|
documentId: document.id,
|
||||||
|
type: 'DOCUMENT_SIGNING_LINK_COPIED',
|
||||||
|
data: {
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
isBulkCopy: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentFlowFormContainerHeader
|
<DocumentFlowFormContainerHeader
|
||||||
@ -236,14 +258,7 @@ export const AddSubjectFormPartial = ({
|
|||||||
{recipient.role !== RecipientRole.CC && (
|
{recipient.role !== RecipientRole.CC && (
|
||||||
<CopyTextButton
|
<CopyTextButton
|
||||||
value={formatSigningLink(recipient.token)}
|
value={formatSigningLink(recipient.token)}
|
||||||
onCopySuccess={() => {
|
onCopySuccess={() => void onCopyLink(recipient)}
|
||||||
toast({
|
|
||||||
title: _(msg`Copied to clipboard`),
|
|
||||||
description: _(
|
|
||||||
msg`The signing link has been copied to your clipboard.`,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
badgeContentUncopied={
|
badgeContentUncopied={
|
||||||
<p className="ml-1 text-xs">
|
<p className="ml-1 text-xs">
|
||||||
<Trans>Copy</Trans>
|
<Trans>Copy</Trans>
|
||||||
|
|||||||
Reference in New Issue
Block a user