feat: add signing link copy (#1449)
This commit is contained in:
@@ -33,6 +33,8 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dropdown-menu';
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
|
||||||
|
|
||||||
import { ResendDocumentActionItem } from '../_action-items/resend-document';
|
import { ResendDocumentActionItem } from '../_action-items/resend-document';
|
||||||
import { DeleteDocumentDialog } from '../delete-document-dialog';
|
import { DeleteDocumentDialog } from '../delete-document-dialog';
|
||||||
import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
|
import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
|
||||||
@@ -62,6 +64,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
|
|
||||||
const isOwner = document.User.id === session.user.id;
|
const isOwner = document.User.id === session.user.id;
|
||||||
const isDraft = document.status === DocumentStatus.DRAFT;
|
const isDraft = document.status === DocumentStatus.DRAFT;
|
||||||
|
const isPending = document.status === DocumentStatus.PENDING;
|
||||||
const isDeleted = document.deletedAt !== null;
|
const isDeleted = document.deletedAt !== null;
|
||||||
const isComplete = document.status === DocumentStatus.COMPLETED;
|
const isComplete = document.status === DocumentStatus.COMPLETED;
|
||||||
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
||||||
@@ -145,6 +148,21 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
<Trans>Share</Trans>
|
<Trans>Share</Trans>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
|
{canManageDocument && (
|
||||||
|
<DocumentRecipientLinkCopyDialog
|
||||||
|
recipients={document.Recipient}
|
||||||
|
trigger={
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!isPending || isDeleted}
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
<Trans>Signing Links</Trans>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<ResendDocumentActionItem
|
<ResendDocumentActionItem
|
||||||
document={document}
|
document={document}
|
||||||
recipients={nonSignedRecipients}
|
recipients={nonSignedRecipients}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
@@ -6,11 +8,14 @@ import { CheckIcon, Clock, MailIcon, MailOpenIcon, PenIcon, PlusIcon } from 'luc
|
|||||||
import { match } from 'ts-pattern';
|
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 { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
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 { 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';
|
||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
import { Badge } from '@documenso/ui/primitives/badge';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DocumentPageViewRecipientsProps = {
|
export type DocumentPageViewRecipientsProps = {
|
||||||
document: Document & {
|
document: Document & {
|
||||||
@@ -24,6 +29,7 @@ export const DocumentPageViewRecipients = ({
|
|||||||
documentRootPath,
|
documentRootPath,
|
||||||
}: DocumentPageViewRecipientsProps) => {
|
}: DocumentPageViewRecipientsProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const recipients = document.Recipient;
|
const recipients = document.Recipient;
|
||||||
|
|
||||||
@@ -68,53 +74,69 @@ export const DocumentPageViewRecipients = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{document.status !== DocumentStatus.DRAFT &&
|
<div className="flex flex-row items-center">
|
||||||
recipient.signingStatus === SigningStatus.SIGNED && (
|
{document.status !== DocumentStatus.DRAFT &&
|
||||||
<Badge variant="default">
|
recipient.signingStatus === SigningStatus.SIGNED && (
|
||||||
{match(recipient.role)
|
<Badge variant="default">
|
||||||
.with(RecipientRole.APPROVER, () => (
|
{match(recipient.role)
|
||||||
<>
|
.with(RecipientRole.APPROVER, () => (
|
||||||
<CheckIcon className="mr-1 h-3 w-3" />
|
|
||||||
<Trans>Approved</Trans>
|
|
||||||
</>
|
|
||||||
))
|
|
||||||
.with(RecipientRole.CC, () =>
|
|
||||||
document.status === DocumentStatus.COMPLETED ? (
|
|
||||||
<>
|
|
||||||
<MailIcon className="mr-1 h-3 w-3" />
|
|
||||||
<Trans>Sent</Trans>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<CheckIcon className="mr-1 h-3 w-3" />
|
<CheckIcon className="mr-1 h-3 w-3" />
|
||||||
<Trans>Ready</Trans>
|
<Trans>Approved</Trans>
|
||||||
</>
|
</>
|
||||||
),
|
))
|
||||||
)
|
.with(RecipientRole.CC, () =>
|
||||||
|
document.status === DocumentStatus.COMPLETED ? (
|
||||||
|
<>
|
||||||
|
<MailIcon className="mr-1 h-3 w-3" />
|
||||||
|
<Trans>Sent</Trans>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckIcon className="mr-1 h-3 w-3" />
|
||||||
|
<Trans>Ready</Trans>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
.with(RecipientRole.SIGNER, () => (
|
.with(RecipientRole.SIGNER, () => (
|
||||||
<>
|
<>
|
||||||
<SignatureIcon className="mr-1 h-3 w-3" />
|
<SignatureIcon className="mr-1 h-3 w-3" />
|
||||||
<Trans>Signed</Trans>
|
<Trans>Signed</Trans>
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
.with(RecipientRole.VIEWER, () => (
|
.with(RecipientRole.VIEWER, () => (
|
||||||
<>
|
<>
|
||||||
<MailOpenIcon className="mr-1 h-3 w-3" />
|
<MailOpenIcon className="mr-1 h-3 w-3" />
|
||||||
<Trans>Viewed</Trans>
|
<Trans>Viewed</Trans>
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{document.status !== DocumentStatus.DRAFT &&
|
{document.status !== DocumentStatus.DRAFT &&
|
||||||
recipient.signingStatus === SigningStatus.NOT_SIGNED && (
|
recipient.signingStatus === SigningStatus.NOT_SIGNED && (
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
<Clock className="mr-1 h-3 w-3" />
|
<Clock className="mr-1 h-3 w-3" />
|
||||||
<Trans>Pending</Trans>
|
<Trans>Pending</Trans>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{document.status === DocumentStatus.PENDING &&
|
||||||
|
recipient.signingStatus === SigningStatus.NOT_SIGNED &&
|
||||||
|
recipient.role !== RecipientRole.CC && (
|
||||||
|
<CopyTextButton
|
||||||
|
value={formatSigningLink(recipient.token)}
|
||||||
|
onCopySuccess={() => {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Copied to clipboard`),
|
||||||
|
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|||||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||||
import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
|
import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
|
||||||
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
|
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
|
||||||
|
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
|
||||||
import {
|
import {
|
||||||
DocumentStatus as DocumentStatusComponent,
|
DocumentStatus as DocumentStatusComponent,
|
||||||
FRIENDLY_STATUS_MAP,
|
FRIENDLY_STATUS_MAP,
|
||||||
@@ -134,6 +135,10 @@ 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 && (
|
||||||
|
<DocumentRecipientLinkCopyDialog recipients={recipients} />
|
||||||
|
)}
|
||||||
|
|
||||||
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
<Trans>Documents</Trans>
|
<Trans>Documents</Trans>
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dropdown-menu';
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
|
||||||
|
|
||||||
import { ResendDocumentActionItem } from './_action-items/resend-document';
|
import { ResendDocumentActionItem } from './_action-items/resend-document';
|
||||||
import { DeleteDocumentDialog } from './delete-document-dialog';
|
import { DeleteDocumentDialog } from './delete-document-dialog';
|
||||||
import { DuplicateDocumentDialog } from './duplicate-document-dialog';
|
import { DuplicateDocumentDialog } from './duplicate-document-dialog';
|
||||||
@@ -69,7 +71,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
const isOwner = row.User.id === session.user.id;
|
const isOwner = row.User.id === session.user.id;
|
||||||
// const isRecipient = !!recipient;
|
// const isRecipient = !!recipient;
|
||||||
const isDraft = row.status === DocumentStatus.DRAFT;
|
const isDraft = row.status === DocumentStatus.DRAFT;
|
||||||
// const isPending = row.status === DocumentStatus.PENDING;
|
const isPending = row.status === DocumentStatus.PENDING;
|
||||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
const isCurrentTeamDocument = team && row.team?.url === team.url;
|
const isCurrentTeamDocument = team && row.team?.url === team.url;
|
||||||
@@ -191,6 +193,20 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
|||||||
<Trans>Share</Trans>
|
<Trans>Share</Trans>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
|
{canManageDocument && (
|
||||||
|
<DocumentRecipientLinkCopyDialog
|
||||||
|
recipients={row.Recipient}
|
||||||
|
trigger={
|
||||||
|
<DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}>
|
||||||
|
<div>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
<Trans>Signing Links</Trans>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<ResendDocumentActionItem document={row} recipients={nonSignedRecipients} team={team} />
|
<ResendDocumentActionItem document={row} recipients={nonSignedRecipients} team={team} />
|
||||||
|
|
||||||
<DocumentShareButton
|
<DocumentShareButton
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
|
import { Trans, msg } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
|
||||||
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
|
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
|
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||||
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
|
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||||
|
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
export type DocumentRecipientLinkCopyDialogProps = {
|
||||||
|
trigger?: React.ReactNode;
|
||||||
|
recipients: Recipient[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentRecipientLinkCopyDialog = ({
|
||||||
|
trigger,
|
||||||
|
recipients,
|
||||||
|
}: DocumentRecipientLinkCopyDialogProps) => {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [, copy] = useCopyToClipboard();
|
||||||
|
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const actionSearchParam = searchParams?.get('action');
|
||||||
|
|
||||||
|
const onBulkCopy = async () => {
|
||||||
|
const generatedString = recipients
|
||||||
|
.filter((recipient) => recipient.role !== RecipientRole.CC)
|
||||||
|
.map((recipient) => `${recipient.email}\n${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`)
|
||||||
|
.join('\n\n');
|
||||||
|
|
||||||
|
await copy(generatedString).then(() => {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Copied to clipboard`),
|
||||||
|
description: _(msg`All signing links have been copied to your clipboard.`),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (actionSearchParam === 'view-signing-links') {
|
||||||
|
setOpen(true);
|
||||||
|
updateSearchParams({ action: null });
|
||||||
|
}
|
||||||
|
}, [actionSearchParam, open, setOpen, updateSearchParams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => setOpen(value)}>
|
||||||
|
<DialogTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||||
|
{trigger}
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent position="center">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="pb-0.5">
|
||||||
|
<Trans>Copy Signing Links</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>
|
||||||
|
You can copy and share these links to recipients so they can action the document.
|
||||||
|
</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<ul className="text-muted-foreground divide-y rounded-lg border">
|
||||||
|
{recipients.length === 0 && (
|
||||||
|
<li className="flex flex-col items-center justify-center py-6 text-sm">
|
||||||
|
<Trans>No recipients</Trans>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{recipients.map((recipient) => (
|
||||||
|
<li key={recipient.id} className="flex items-center justify-between px-4 py-3 text-sm">
|
||||||
|
<AvatarWithText
|
||||||
|
avatarFallback={recipient.email.slice(0, 1).toUpperCase()}
|
||||||
|
primaryText={<p className="text-muted-foreground text-sm">{recipient.email}</p>}
|
||||||
|
secondaryText={
|
||||||
|
<p className="text-muted-foreground/70 text-xs">
|
||||||
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{recipient.role !== RecipientRole.CC && (
|
||||||
|
<CopyTextButton
|
||||||
|
value={formatSigningLink(recipient.token)}
|
||||||
|
onCopySuccess={() => {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Copied to clipboard`),
|
||||||
|
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
badgeContentUncopied={
|
||||||
|
<p className="ml-1 text-xs">
|
||||||
|
<Trans>Copy</Trans>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
badgeContentCopied={
|
||||||
|
<p className="ml-1 text-xs">
|
||||||
|
<Trans>Copied</Trans>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="secondary">
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
|
<Button type="button" onClick={onBulkCopy}>
|
||||||
|
<Trans>Bulk Copy</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||||
|
|
||||||
|
export const formatSigningLink = (token: string) => `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether a recipient can be modified by the document owner.
|
* Whether a recipient can be modified by the document owner.
|
||||||
*/
|
*/
|
||||||
|
|||||||
82
packages/ui/components/common/copy-text-button.tsx
Normal file
82
packages/ui/components/common/copy-text-button.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { AnimatePresence } from 'framer-motion';
|
||||||
|
import { CheckSquareIcon, CopyIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
export type CopyTextButtonProps = {
|
||||||
|
value: string;
|
||||||
|
badgeContentUncopied?: React.ReactNode;
|
||||||
|
badgeContentCopied?: React.ReactNode;
|
||||||
|
onCopySuccess?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CopyTextButton = ({
|
||||||
|
value,
|
||||||
|
onCopySuccess,
|
||||||
|
badgeContentUncopied,
|
||||||
|
badgeContentCopied,
|
||||||
|
}: CopyTextButtonProps) => {
|
||||||
|
const [, copy] = useCopyToClipboard();
|
||||||
|
|
||||||
|
const [copiedTimeout, setCopiedTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const onCopy = async () => {
|
||||||
|
await copy(value).then(() => onCopySuccess?.());
|
||||||
|
|
||||||
|
if (copiedTimeout) {
|
||||||
|
clearTimeout(copiedTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCopiedTimeout(
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopiedTimeout(null);
|
||||||
|
}, 2000),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="none"
|
||||||
|
className="ml-2 h-7 rounded border bg-neutral-50 px-0.5 font-normal dark:border dark:border-neutral-500 dark:bg-neutral-600"
|
||||||
|
onClick={async () => onCopy()}
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-row items-center"
|
||||||
|
key={copiedTimeout ? 'copied' : 'copy'}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0, transition: { duration: 0.1 } }}
|
||||||
|
>
|
||||||
|
{copiedTimeout ? badgeContentCopied : badgeContentUncopied}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex h-6 w-6 items-center justify-center rounded transition-all hover:bg-neutral-200 hover:active:bg-neutral-300 dark:hover:bg-neutral-500 dark:hover:active:bg-neutral-400',
|
||||||
|
{
|
||||||
|
'ml-1': Boolean(badgeContentCopied || badgeContentUncopied),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute">
|
||||||
|
{copiedTimeout ? (
|
||||||
|
<CheckSquareIcon className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<CopyIcon className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user