mirror of
https://github.com/documenso/documenso.git
synced 2026-07-27 18:35:05 +10:00
chore: merge main, resolve biome formatting conflicts
Reconcile toolbar-filters changes with main's Kysely-based query rewrite and biome reformatting. Notable: - find-documents.ts: rebase on main's Kysely impl while preserving PR semantics (status[] / source[] filters via OR-of-predicates, period 'all' bypass). - get-stats.ts: skip period filter when 'all'. - find-templates.ts: preserve PR templateType array + query filter on top of main's where/include structure. - find-documents-internal: drop preloaded team plumbing (main resolves team internally) and switch appMetaTags to msg`...`. - Restore packages/lib/translations/ from HEAD (autogenerated).
This commit is contained in:
@@ -1,28 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Paperclip, Plus, X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DocumentAttachmentsPopoverProps = {
|
||||
envelopeId: string;
|
||||
buttonClassName?: string;
|
||||
@@ -36,6 +29,7 @@ const ZAttachmentFormSchema = z.object({
|
||||
|
||||
type TAttachmentFormSchema = z.infer<typeof ZAttachmentFormSchema>;
|
||||
|
||||
// NOTE: REMEMBER TO UPDATE THE EMBEDDED VERSION OF THIS COMPONENT TOO.
|
||||
export const DocumentAttachmentsPopover = ({
|
||||
envelopeId,
|
||||
buttonClassName,
|
||||
@@ -49,16 +43,22 @@ export const DocumentAttachmentsPopover = ({
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { data: attachments } = trpc.envelope.attachment.find.useQuery({
|
||||
envelopeId,
|
||||
});
|
||||
const { data: attachments } = trpc.envelope.attachment.find.useQuery(
|
||||
{
|
||||
envelopeId,
|
||||
},
|
||||
{
|
||||
// Note: The invalidation of the query is manually handled by the onSuccess
|
||||
// callbacks below for create and delete mutations.
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: createAttachment, isPending: isCreating } =
|
||||
trpc.envelope.attachment.create.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.envelope.attachment.find.invalidate({ envelopeId });
|
||||
},
|
||||
});
|
||||
const { mutateAsync: createAttachment, isPending: isCreating } = trpc.envelope.attachment.create.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.envelope.attachment.find.invalidate({ envelopeId });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteAttachment } = trpc.envelope.attachment.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -130,9 +130,7 @@ export const DocumentAttachmentsPopover = ({
|
||||
|
||||
<span>
|
||||
<Trans>Attachments</Trans>
|
||||
{attachments && attachments.data.length > 0 && (
|
||||
<span className="ml-1">({attachments.data.length})</span>
|
||||
)}
|
||||
{attachments && attachments.data.length > 0 && <span className="ml-1">({attachments.data.length})</span>}
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -143,7 +141,7 @@ export const DocumentAttachmentsPopover = ({
|
||||
<h4 className="font-medium">
|
||||
<Trans>Attachments</Trans>
|
||||
</h4>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
<p className="mt-1 text-muted-foreground text-sm">
|
||||
<Trans>Add links to relevant documents or resources.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
@@ -153,15 +151,15 @@ export const DocumentAttachmentsPopover = ({
|
||||
{attachments?.data.map((attachment) => (
|
||||
<div
|
||||
key={attachment.id}
|
||||
className="border-border flex items-center justify-between rounded-md border p-2"
|
||||
className="flex items-center justify-between rounded-md border border-border p-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium">{attachment.label}</p>
|
||||
<p className="truncate font-medium text-sm">{attachment.label}</p>
|
||||
<a
|
||||
href={attachment.data}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-foreground truncate text-xs underline"
|
||||
className="truncate text-muted-foreground text-xs underline hover:text-foreground"
|
||||
>
|
||||
{attachment.data}
|
||||
</a>
|
||||
@@ -181,12 +179,7 @@ export const DocumentAttachmentsPopover = ({
|
||||
)}
|
||||
|
||||
{!isAdding && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => setIsAdding(true)}
|
||||
>
|
||||
<Button variant="outline" size="sm" className="w-full" onClick={() => setIsAdding(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<Trans>Add Attachment</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DownloadIcon } from 'lucide-react';
|
||||
|
||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||
import { base64 } from '@documenso/lib/universal/base64';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DownloadIcon } from 'lucide-react';
|
||||
|
||||
export type DocumentAuditLogDownloadButtonProps = {
|
||||
className?: string;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const DocumentAuditLogDownloadButton = ({
|
||||
className,
|
||||
documentId,
|
||||
}: DocumentAuditLogDownloadButtonProps) => {
|
||||
export const DocumentAuditLogDownloadButton = ({ className, documentId }: DocumentAuditLogDownloadButtonProps) => {
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { mutateAsync: downloadAuditLogs, isPending } =
|
||||
trpc.document.auditLog.download.useMutation();
|
||||
const { mutateAsync: downloadAuditLogs, isPending } = trpc.document.auditLog.download.useMutation();
|
||||
|
||||
const onDownloadAuditLogsClick = async () => {
|
||||
try {
|
||||
@@ -41,9 +36,7 @@ export const DocumentAuditLogDownloadButton = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`Sorry, we were unable to download the audit logs. Please try again later.`,
|
||||
),
|
||||
description: _(msg`Sorry, we were unable to download the audit logs. Please try again later.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
+7
-11
@@ -1,9 +1,3 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentStatus } from '@prisma/client';
|
||||
import { DownloadIcon } from 'lucide-react';
|
||||
|
||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||
import { base64 } from '@documenso/lib/universal/base64';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
@@ -11,6 +5,11 @@ import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentStatus } from '@prisma/client';
|
||||
import { DownloadIcon } from 'lucide-react';
|
||||
|
||||
export type DocumentCertificateDownloadButtonProps = {
|
||||
className?: string;
|
||||
@@ -26,8 +25,7 @@ export const DocumentCertificateDownloadButton = ({
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { mutateAsync: downloadCertificate, isPending } =
|
||||
trpc.document.downloadCertificate.useMutation();
|
||||
const { mutateAsync: downloadCertificate, isPending } = trpc.document.downloadCertificate.useMutation();
|
||||
|
||||
const onDownloadCertificatesClick = async () => {
|
||||
try {
|
||||
@@ -45,9 +43,7 @@ export const DocumentCertificateDownloadButton = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`Sorry, we were unable to download the certificate. Please try again later.`,
|
||||
),
|
||||
description: _(msg`Sorry, we were unable to download the certificate. Please try again later.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { lazy, useEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { type DocumentData, DocumentStatus, type EnvelopeItem, EnvelopeType } from '@prisma/client';
|
||||
import { DownloadIcon } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import {
|
||||
EnvelopeRenderProvider,
|
||||
useCurrentEnvelopeRender,
|
||||
} from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i18n';
|
||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -21,15 +15,18 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { type DocumentData, DocumentStatus, type EnvelopeItem, EnvelopeType } from '@prisma/client';
|
||||
import { DownloadIcon } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
||||
import { EnvelopePdfViewer } from '~/components/general/pdf-viewer/envelope-pdf-viewer';
|
||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||
|
||||
import { EnvelopeRendererFileSelector } from '../envelope-editor/envelope-file-selector';
|
||||
|
||||
const EnvelopeGenericPageRenderer = lazy(
|
||||
async () => import('~/components/general/envelope-editor/envelope-generic-page-renderer'),
|
||||
);
|
||||
import { EnvelopeGenericPageRenderer } from '../envelope-editor/envelope-generic-page-renderer';
|
||||
|
||||
export type DocumentCertificateQRViewProps = {
|
||||
documentId: number;
|
||||
@@ -58,9 +55,7 @@ export const DocumentCertificateQRView = ({
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(() => !!documentViaUser);
|
||||
|
||||
const formattedDate = completedDate
|
||||
? DateTime.fromJSDate(completedDate).toLocaleString(DateTime.DATETIME_MED)
|
||||
: '';
|
||||
const formattedDate = completedDate ? DateTime.fromJSDate(completedDate).toLocaleString(DateTime.DATETIME_MED) : '';
|
||||
|
||||
useEffect(() => {
|
||||
if (documentViaUser) {
|
||||
@@ -81,8 +76,8 @@ export const DocumentCertificateQRView = ({
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
This document is available in your Documenso account. You can view more details,
|
||||
recipients, and audit logs there.
|
||||
This document is available in your Documenso account. You can view more details, recipients, and audit
|
||||
logs there.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -104,11 +99,13 @@ export const DocumentCertificateQRView = ({
|
||||
|
||||
{internalVersion === 2 ? (
|
||||
<EnvelopeRenderProvider
|
||||
version="current"
|
||||
envelope={{
|
||||
envelopeItems,
|
||||
id: envelopeItems[0].envelopeId,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
}}
|
||||
envelopeItems={envelopeItems}
|
||||
token={token}
|
||||
>
|
||||
<DocumentCertificateQrV2
|
||||
@@ -122,8 +119,8 @@ export const DocumentCertificateQRView = ({
|
||||
<>
|
||||
<div className="flex w-full flex-col justify-between gap-4 md:flex-row md:items-end">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-xl font-medium">{title}</h1>
|
||||
<div className="flex flex-col gap-0.5 text-sm text-muted-foreground">
|
||||
<h1 className="font-medium text-xl">{title}</h1>
|
||||
<div className="flex flex-col gap-0.5 text-muted-foreground text-sm">
|
||||
<p>
|
||||
<Trans>{recipientCount} recipients</Trans>
|
||||
</p>
|
||||
@@ -150,10 +147,16 @@ export const DocumentCertificateQRView = ({
|
||||
|
||||
<div className="mt-12 w-full">
|
||||
<PDFViewerLazy
|
||||
key={envelopeItems[0].id}
|
||||
envelopeItem={envelopeItems[0]}
|
||||
token={token}
|
||||
version="signed"
|
||||
key={envelopeItems[0]?.id}
|
||||
data={getDocumentDataUrlForPdfViewer({
|
||||
envelopeId: envelopeItems[0]?.envelopeId,
|
||||
envelopeItemId: envelopeItems[0]?.id,
|
||||
documentDataId: envelopeItems[0]?.documentDataId,
|
||||
version: 'current',
|
||||
token,
|
||||
presignToken: undefined,
|
||||
})}
|
||||
scrollParentRef="window"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -169,20 +172,15 @@ type DocumentCertificateQrV2Props = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
const DocumentCertificateQrV2 = ({
|
||||
title,
|
||||
recipientCount,
|
||||
formattedDate,
|
||||
token,
|
||||
}: DocumentCertificateQrV2Props) => {
|
||||
const { currentEnvelopeItem, envelopeItems } = useCurrentEnvelopeRender();
|
||||
const DocumentCertificateQrV2 = ({ title, recipientCount, formattedDate, token }: DocumentCertificateQrV2Props) => {
|
||||
const { envelopeItems } = useCurrentEnvelopeRender();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-start">
|
||||
<div className="flex w-full flex-col justify-between gap-4 md:flex-row md:items-end">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-xl font-medium">{title}</h1>
|
||||
<div className="flex flex-col gap-0.5 text-sm text-muted-foreground">
|
||||
<h1 className="font-medium text-xl">{title}</h1>
|
||||
<div className="flex flex-col gap-0.5 text-muted-foreground text-sm">
|
||||
<p>
|
||||
<Trans>{recipientCount} recipients</Trans>
|
||||
</p>
|
||||
@@ -210,7 +208,11 @@ const DocumentCertificateQrV2 = ({
|
||||
<div className="mt-12 w-full">
|
||||
<EnvelopeRendererFileSelector className="mb-4 p-0" fields={[]} secondaryOverride={''} />
|
||||
|
||||
<PDFViewerKonvaLazy renderer="preview" customPageRenderer={EnvelopeGenericPageRenderer} />
|
||||
<EnvelopePdfViewer
|
||||
scrollParentRef="window"
|
||||
customPageRenderer={EnvelopeGenericPageRenderer}
|
||||
errorMessage={PDF_VIEWER_ERROR_MESSAGES.preview}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
||||
import {
|
||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
SKIP_QUERY_BATCH_META,
|
||||
} from '@documenso/lib/constants/trpc';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META } from '@documenso/lib/constants/trpc';
|
||||
import type { TDocument } from '@documenso/lib/types/document';
|
||||
import { ZDocumentAccessAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@@ -27,10 +17,16 @@ import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/ad
|
||||
import type { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
|
||||
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
||||
import { PDFViewerLazy } from '@documenso/ui/primitives/pdf-viewer/lazy';
|
||||
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type DocumentEditFormProps = {
|
||||
@@ -42,11 +38,7 @@ export type DocumentEditFormProps = {
|
||||
type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
|
||||
const EditDocumentSteps: EditDocumentStep[] = ['settings', 'signers', 'fields', 'subject'];
|
||||
|
||||
export const DocumentEditForm = ({
|
||||
className,
|
||||
initialDocument,
|
||||
documentRootPath,
|
||||
}: DocumentEditFormProps) => {
|
||||
export const DocumentEditForm = ({ className, initialDocument, documentRootPath }: DocumentEditFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
@@ -162,9 +154,7 @@ export const DocumentEditForm = ({
|
||||
const saveSettingsData = async (data: TAddSettingsFormSchema) => {
|
||||
const { timezone, dateFormat, redirectUrl, language, signatureTypes } = data.meta;
|
||||
|
||||
const parsedGlobalAccessAuth = z
|
||||
.array(ZDocumentAccessAuthTypesSchema)
|
||||
.safeParse(data.globalAccessAuth);
|
||||
const parsedGlobalAccessAuth = z.array(ZDocumentAccessAuthTypesSchema).safeParse(data.globalAccessAuth);
|
||||
|
||||
return updateDocument({
|
||||
documentId: document.id,
|
||||
@@ -342,8 +332,7 @@ export const DocumentEditForm = ({
|
||||
};
|
||||
|
||||
const saveSubjectData = async (data: TAddSubjectFormSchema) => {
|
||||
const { subject, message, distributionMethod, emailId, emailReplyTo, emailSettings } =
|
||||
data.meta;
|
||||
const { subject, message, distributionMethod, emailId, emailReplyTo, emailSettings } = data.meta;
|
||||
|
||||
return updateDocument({
|
||||
documentId: document.id,
|
||||
@@ -359,8 +348,7 @@ export const DocumentEditForm = ({
|
||||
};
|
||||
|
||||
const sendDocumentWithSubject = async (data: TAddSubjectFormSchema) => {
|
||||
const { subject, message, distributionMethod, emailId, emailReplyTo, emailSettings } =
|
||||
data.meta;
|
||||
const { subject, message, distributionMethod, emailId, emailReplyTo, emailSettings } = data.meta;
|
||||
|
||||
return sendDocument({
|
||||
documentId: document.id,
|
||||
@@ -435,26 +423,26 @@ export const DocumentEditForm = ({
|
||||
|
||||
return (
|
||||
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
|
||||
<Card
|
||||
className="relative col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7"
|
||||
gradient
|
||||
>
|
||||
<Card className="relative col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7" gradient>
|
||||
<CardContent className="p-2">
|
||||
<PDFViewerLazy
|
||||
key={document.envelopeItems[0].id}
|
||||
envelopeItem={document.envelopeItems[0]}
|
||||
token={undefined}
|
||||
version="signed"
|
||||
key={document.envelopeItems[0]?.id}
|
||||
data={getDocumentDataUrlForPdfViewer({
|
||||
envelopeId: document.envelopeId,
|
||||
envelopeItemId: document.envelopeItems[0]?.id,
|
||||
documentDataId: initialDocument.documentDataId,
|
||||
version: 'current',
|
||||
token: undefined,
|
||||
presignToken: undefined,
|
||||
})}
|
||||
scrollParentRef="window"
|
||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||
<DocumentFlowFormContainer
|
||||
className="lg:h-[calc(100vh-6rem)]"
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<DocumentFlowFormContainer className="lg:h-[calc(100vh-6rem)]" onSubmit={(e) => e.preventDefault()}>
|
||||
<Stepper
|
||||
currentStep={currentDocumentFlow.stepIndex}
|
||||
setCurrentStep={(step) => setStep(EditDocumentSteps[step - 1])}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
||||
|
||||
@@ -43,19 +42,19 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
|
||||
{match(role)
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<>
|
||||
<Pencil className="-ml-1 mr-2 h-4 w-4" />
|
||||
<Pencil className="mr-2 -ml-1 h-4 w-4" />
|
||||
<Trans>Sign</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
<>
|
||||
<CheckCircle className="-ml-1 mr-2 h-4 w-4" />
|
||||
<CheckCircle className="mr-2 -ml-1 h-4 w-4" />
|
||||
<Trans>Approve</Trans>
|
||||
</>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<>
|
||||
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
|
||||
<EyeIcon className="mr-2 -ml-1 h-4 w-4" />
|
||||
<Trans>View</Trans>
|
||||
</>
|
||||
))}
|
||||
@@ -77,7 +76,7 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
|
||||
token={recipient?.token}
|
||||
trigger={
|
||||
<Button className="w-full">
|
||||
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
||||
<Download className="mr-2 -ml-1 inline h-4 w-4" />
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import {
|
||||
Copy,
|
||||
Download,
|
||||
Edit,
|
||||
Loader,
|
||||
MoreHorizontal,
|
||||
ScrollTextIcon,
|
||||
Share,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { getEnvelopeItemPermissions, mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -28,12 +12,29 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from '@documenso/ui/primitives/dropdown-menu';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import {
|
||||
Copy,
|
||||
Download,
|
||||
Edit,
|
||||
FileOutputIcon,
|
||||
Loader,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
ScrollTextIcon,
|
||||
Share,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
|
||||
import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
|
||||
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
|
||||
import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog';
|
||||
import { EnvelopeDeleteDialog } from '~/components/dialogs/envelope-delete-dialog';
|
||||
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
||||
import { EnvelopeDuplicateDialog } from '~/components/dialogs/envelope-duplicate-dialog';
|
||||
import { EnvelopeRenameDialog } from '~/components/dialogs/envelope-rename-dialog';
|
||||
import { EnvelopeSaveAsTemplateDialog } from '~/components/dialogs/envelope-save-as-template-dialog';
|
||||
import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
@@ -43,14 +44,14 @@ export type DocumentPageViewDropdownProps = {
|
||||
|
||||
export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownProps) => {
|
||||
const { user } = useSession();
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
||||
const trpcUtils = trpcReact.useUtils();
|
||||
|
||||
const [isRenameDialogOpen, setRenameDialogOpen] = useState(false);
|
||||
const [isSaveAsTemplateDialogOpen, setSaveAsTemplateDialogOpen] = useState(false);
|
||||
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.email === user.email);
|
||||
|
||||
@@ -62,14 +63,16 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
||||
const isCurrentTeamDocument = team && envelope.teamId === team.id;
|
||||
const canManageDocument = Boolean(isOwner || isCurrentTeamDocument);
|
||||
|
||||
const { canTitleBeChanged } = getEnvelopeItemPermissions(envelope, []);
|
||||
|
||||
const documentsPath = formatDocumentsPath(team.url);
|
||||
|
||||
const nonSignedRecipients = envelope.recipients.filter((item) => item.signingStatus !== 'SIGNED');
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
|
||||
<DropdownMenuTrigger data-testid="document-page-view-action-btn">
|
||||
<MoreHorizontal className="h-5 w-5 text-muted-foreground" />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className="w-52" align="end" forceMount>
|
||||
@@ -86,10 +89,18 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{canManageDocument && canTitleBeChanged && (
|
||||
<DropdownMenuItem onClick={() => setRenameDialogOpen(true)}>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
<Trans>Rename</Trans>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={envelope.id}
|
||||
envelopeStatus={envelope.status}
|
||||
token={recipient?.token}
|
||||
isLegacy={envelope.internalVersion === 1}
|
||||
token={canManageDocument ? undefined : recipient?.token}
|
||||
envelopeItems={envelope.envelopeItems}
|
||||
trigger={
|
||||
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
|
||||
@@ -108,15 +119,42 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
<Trans>Duplicate</Trans>
|
||||
<EnvelopeDuplicateDialog
|
||||
envelopeId={envelope.id}
|
||||
envelopeType={EnvelopeType.DOCUMENT}
|
||||
trigger={
|
||||
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
<Trans>Duplicate</Trans>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuItem onClick={() => setSaveAsTemplateDialogOpen(true)}>
|
||||
<FileOutputIcon className="mr-2 h-4 w-4" />
|
||||
<Trans>Save as Template</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)} disabled={isDeleted}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<Trans>Delete</Trans>
|
||||
</DropdownMenuItem>
|
||||
<EnvelopeDeleteDialog
|
||||
id={envelope.id}
|
||||
type={EnvelopeType.DOCUMENT}
|
||||
status={envelope.status}
|
||||
title={envelope.title}
|
||||
canManageDocument={canManageDocument}
|
||||
onDelete={() => {
|
||||
void navigate(documentsPath);
|
||||
}}
|
||||
trigger={
|
||||
<DropdownMenuItem asChild disabled={isDeleted} onSelect={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<Trans>Delete</Trans>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuLabel>
|
||||
<Trans>Share</Trans>
|
||||
@@ -126,10 +164,7 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
||||
<DocumentRecipientLinkCopyDialog
|
||||
recipients={envelope.recipients}
|
||||
trigger={
|
||||
<DropdownMenuItem
|
||||
disabled={!isPending || isDeleted}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<DropdownMenuItem disabled={!isPending || isDeleted} onSelect={(e) => e.preventDefault()}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
<Trans>Signing Links</Trans>
|
||||
</DropdownMenuItem>
|
||||
@@ -159,26 +194,21 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
|
||||
<DocumentDeleteDialog
|
||||
id={mapSecondaryIdToDocumentId(envelope.secondaryId)}
|
||||
status={envelope.status}
|
||||
documentTitle={envelope.title}
|
||||
open={isDeleteDialogOpen}
|
||||
canManageDocument={canManageDocument}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onDelete={() => {
|
||||
void navigate(documentsPath);
|
||||
}}
|
||||
<EnvelopeSaveAsTemplateDialog
|
||||
envelopeId={envelope.id}
|
||||
open={isSaveAsTemplateDialogOpen}
|
||||
onOpenChange={setSaveAsTemplateDialogOpen}
|
||||
/>
|
||||
|
||||
{isDuplicateDialogOpen && (
|
||||
<DocumentDuplicateDialog
|
||||
id={envelope.id}
|
||||
token={recipient?.token}
|
||||
open={isDuplicateDialogOpen}
|
||||
onOpenChange={setDuplicateDialogOpen}
|
||||
/>
|
||||
)}
|
||||
<EnvelopeRenameDialog
|
||||
id={envelope.id}
|
||||
initialTitle={envelope.title}
|
||||
open={isRenameDialogOpen}
|
||||
onOpenChange={setRenameDialogOpen}
|
||||
onSuccess={async () => {
|
||||
await trpcUtils.envelope.get.invalidate();
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export type DocumentPageViewInformationProps = {
|
||||
userId: number;
|
||||
envelope: TEnvelope;
|
||||
};
|
||||
|
||||
export const DocumentPageViewInformation = ({
|
||||
envelope,
|
||||
userId,
|
||||
}: DocumentPageViewInformationProps) => {
|
||||
export const DocumentPageViewInformation = ({ envelope, userId }: DocumentPageViewInformationProps) => {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const { _, i18n } = useLingui();
|
||||
@@ -26,8 +21,7 @@ export const DocumentPageViewInformation = ({
|
||||
return [
|
||||
{
|
||||
description: msg`Uploaded by`,
|
||||
value:
|
||||
userId === envelope.userId ? _(msg`You`) : (envelope.user.name ?? envelope.user.email),
|
||||
value: userId === envelope.userId ? _(msg`You`) : (envelope.user.name ?? envelope.user.email),
|
||||
},
|
||||
{
|
||||
description: msg`Created`,
|
||||
@@ -50,17 +44,14 @@ export const DocumentPageViewInformation = ({
|
||||
}, [isMounted, envelope, userId]);
|
||||
|
||||
return (
|
||||
<section className="dark:bg-background text-foreground border-border bg-widget flex flex-col rounded-xl border">
|
||||
<section className="flex flex-col rounded-xl border border-border bg-widget text-foreground dark:bg-background">
|
||||
<h1 className="px-4 py-3 font-medium">
|
||||
<Trans>Information</Trans>
|
||||
</h1>
|
||||
|
||||
<ul className="divide-y border-t">
|
||||
{documentInformation.map((item, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="flex items-center justify-between px-4 py-2.5 text-sm last:border-b"
|
||||
>
|
||||
<li key={i} className="flex items-center justify-between px-4 py-2.5 text-sm last:border-b">
|
||||
<span className="text-muted-foreground">{_(item.description)}</span>
|
||||
<span>{item.value}</span>
|
||||
</li>
|
||||
|
||||
@@ -1,49 +1,37 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { AlertTriangle, CheckCheckIcon, CheckIcon, Loader, MailOpen } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { AlertTriangle, CheckCheckIcon, CheckIcon, Loader, MailOpen } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useMemo } from 'react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
export type DocumentPageViewRecentActivityProps = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const DocumentPageViewRecentActivity = ({
|
||||
documentId,
|
||||
userId,
|
||||
}: DocumentPageViewRecentActivityProps) => {
|
||||
export const DocumentPageViewRecentActivity = ({ documentId, userId }: DocumentPageViewRecentActivityProps) => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isLoadingError,
|
||||
refetch,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = trpc.document.auditLog.find.useInfiniteQuery(
|
||||
{
|
||||
documentId,
|
||||
filterForRecentActivity: true,
|
||||
orderByColumn: 'createdAt',
|
||||
orderByDirection: 'asc',
|
||||
perPage: 10,
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
},
|
||||
);
|
||||
const { data, isLoading, isLoadingError, refetch, hasNextPage, fetchNextPage, isFetchingNextPage } =
|
||||
trpc.document.auditLog.find.useInfiniteQuery(
|
||||
{
|
||||
documentId,
|
||||
filterForRecentActivity: true,
|
||||
orderByColumn: 'createdAt',
|
||||
orderByDirection: 'asc',
|
||||
perPage: 10,
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
},
|
||||
);
|
||||
|
||||
const documentAuditLogs = useMemo(() => (data?.pages ?? []).flatMap((page) => page.data), [data]);
|
||||
|
||||
@@ -65,12 +53,12 @@ export const DocumentPageViewRecentActivity = ({
|
||||
|
||||
{isLoadingError && (
|
||||
<div className="flex h-full flex-col items-center justify-center py-16">
|
||||
<p className="text-sm text-foreground/80">
|
||||
<p className="text-foreground/80 text-sm">
|
||||
<Trans>Unable to load document history</Trans>
|
||||
</p>
|
||||
<button
|
||||
onClick={async () => refetch()}
|
||||
className="mt-2 text-sm text-foreground/70 hover:text-muted-foreground"
|
||||
className="mt-2 text-foreground/70 text-sm hover:text-muted-foreground"
|
||||
>
|
||||
<Trans>Click here to retry</Trans>
|
||||
</button>
|
||||
@@ -82,7 +70,7 @@ export const DocumentPageViewRecentActivity = ({
|
||||
<ul role="list" className="space-y-6 p-4">
|
||||
{hasNextPage && (
|
||||
<li className="relative flex gap-x-4">
|
||||
<div className="absolute -bottom-6 left-0 top-0 flex w-6 justify-center">
|
||||
<div className="absolute top-0 -bottom-6 left-0 flex w-6 justify-center">
|
||||
<div className="w-px bg-border" />
|
||||
</div>
|
||||
|
||||
@@ -92,7 +80,7 @@ export const DocumentPageViewRecentActivity = ({
|
||||
|
||||
<button
|
||||
onClick={async () => fetchNextPage()}
|
||||
className="text-xs text-foreground/70 hover:text-muted-foreground"
|
||||
className="text-foreground/70 text-xs hover:text-muted-foreground"
|
||||
>
|
||||
{isFetchingNextPage ? _(msg`Loading...`) : _(msg`Load older activity`)}
|
||||
</button>
|
||||
@@ -101,7 +89,7 @@ export const DocumentPageViewRecentActivity = ({
|
||||
|
||||
{documentAuditLogs.length === 0 && (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
<p className="text-muted-foreground/70 text-sm">
|
||||
<Trans>No recent activity</Trans>
|
||||
</p>
|
||||
</div>
|
||||
@@ -112,7 +100,7 @@ export const DocumentPageViewRecentActivity = ({
|
||||
<div
|
||||
className={cn(
|
||||
auditLogIndex === documentAuditLogs.length - 1 ? 'h-6' : '-bottom-6',
|
||||
'absolute left-0 top-0 flex w-6 justify-center',
|
||||
'absolute top-0 left-0 flex w-6 justify-center',
|
||||
)}
|
||||
>
|
||||
<div className="w-px bg-border" />
|
||||
@@ -146,13 +134,13 @@ export const DocumentPageViewRecentActivity = ({
|
||||
</div>
|
||||
|
||||
<p
|
||||
className="flex-auto truncate py-0.5 text-xs leading-5 text-muted-foreground dark:text-muted-foreground/70"
|
||||
className="flex-auto truncate py-0.5 text-muted-foreground text-xs leading-5 dark:text-muted-foreground/70"
|
||||
title={formatDocumentAuditLogAction(i18n, auditLog, userId).description}
|
||||
>
|
||||
{formatDocumentAuditLogAction(i18n, auditLog, userId).description}
|
||||
</p>
|
||||
|
||||
<time className="flex-none py-0.5 text-xs leading-5 text-muted-foreground dark:text-muted-foreground/70">
|
||||
<time className="flex-none py-0.5 text-muted-foreground text-xs leading-5 dark:text-muted-foreground/70">
|
||||
{DateTime.fromJSDate(auditLog.createdAt).toRelative({ style: 'short' })}
|
||||
</time>
|
||||
</li>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatSigningLink, isRecipientExpired } from '@documenso/lib/utils/recipients';
|
||||
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
@@ -9,42 +18,25 @@ import {
|
||||
AlertTriangle,
|
||||
CheckIcon,
|
||||
Clock,
|
||||
Clock8Icon,
|
||||
MailIcon,
|
||||
MailOpenIcon,
|
||||
PenIcon,
|
||||
PlusIcon,
|
||||
UserIcon,
|
||||
} from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@documenso/ui/primitives/tooltip';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DocumentPageViewRecipientsProps = {
|
||||
envelope: TEnvelope;
|
||||
documentRootPath: string;
|
||||
};
|
||||
|
||||
export const DocumentPageViewRecipients = ({
|
||||
envelope,
|
||||
documentRootPath,
|
||||
}: DocumentPageViewRecipientsProps) => {
|
||||
const { _ } = useLingui();
|
||||
export const DocumentPageViewRecipients = ({ envelope, documentRootPath }: DocumentPageViewRecipientsProps) => {
|
||||
const { _, i18n } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
@@ -66,9 +58,9 @@ export const DocumentPageViewRecipients = ({
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
return (
|
||||
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">
|
||||
<section className="flex flex-col rounded-xl border border-border bg-widget dark:bg-background">
|
||||
<div className="flex flex-row items-center justify-between px-4 py-3">
|
||||
<h1 className="text-foreground font-medium">
|
||||
<h1 className="font-medium text-foreground">
|
||||
<Trans>Recipients</Trans>
|
||||
</h1>
|
||||
|
||||
@@ -78,16 +70,12 @@ export const DocumentPageViewRecipients = ({
|
||||
title={_(msg`Modify recipients`)}
|
||||
className="flex flex-row items-center justify-between"
|
||||
>
|
||||
{recipients.length === 0 ? (
|
||||
<PlusIcon className="ml-2 h-4 w-4" />
|
||||
) : (
|
||||
<PenIcon className="ml-2 h-3 w-3" />
|
||||
)}
|
||||
{recipients.length === 0 ? <PlusIcon className="ml-2 h-4 w-4" /> : <PenIcon className="ml-2 h-3 w-3" />}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="text-muted-foreground divide-y border-t">
|
||||
<ul className="divide-y border-t text-muted-foreground">
|
||||
{recipients.length === 0 && (
|
||||
<li className="flex flex-col items-center justify-center py-6 text-sm">
|
||||
<Trans>No recipients</Trans>
|
||||
@@ -107,83 +95,106 @@ export const DocumentPageViewRecipients = ({
|
||||
/>
|
||||
|
||||
<div className="flex flex-row items-center">
|
||||
{envelope.status !== DocumentStatus.DRAFT &&
|
||||
recipient.signingStatus === SigningStatus.SIGNED && (
|
||||
<Badge variant="default">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
{envelope.status !== DocumentStatus.DRAFT && recipient.signingStatus === SigningStatus.SIGNED && (
|
||||
<Badge variant="default">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
<>
|
||||
<CheckIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Approved</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.CC, () =>
|
||||
envelope.status === DocumentStatus.COMPLETED ? (
|
||||
<>
|
||||
<MailIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Sent</Trans>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Approved</Trans>
|
||||
<Trans>Ready</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.CC, () =>
|
||||
envelope.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, () => (
|
||||
<>
|
||||
<SignatureIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Signed</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
<>
|
||||
<MailOpenIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Viewed</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.ASSISTANT, () => (
|
||||
<>
|
||||
<UserIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Assisted</Trans>
|
||||
</>
|
||||
))
|
||||
.exhaustive()}
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<>
|
||||
<SignatureIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Signed</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
<>
|
||||
<MailOpenIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Viewed</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.ASSISTANT, () => (
|
||||
<>
|
||||
<UserIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Assisted</Trans>
|
||||
</>
|
||||
))
|
||||
.exhaustive()}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{envelope.status !== DocumentStatus.DRAFT &&
|
||||
recipient.signingStatus === SigningStatus.NOT_SIGNED &&
|
||||
isRecipientExpired(recipient) && (
|
||||
<Badge variant="destructive">
|
||||
<Clock8Icon className="mr-1 h-3 w-3" />
|
||||
<Trans>Expired</Trans>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{envelope.status !== DocumentStatus.DRAFT &&
|
||||
recipient.signingStatus === SigningStatus.NOT_SIGNED && (
|
||||
recipient.signingStatus === SigningStatus.NOT_SIGNED &&
|
||||
!isRecipientExpired(recipient) &&
|
||||
(recipient.expiresAt ? (
|
||||
<PopoverHover
|
||||
trigger={
|
||||
<Badge variant="secondary">
|
||||
<Clock className="mr-1 h-3 w-3" />
|
||||
<Trans>Pending</Trans>
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<Trans>
|
||||
Expires {recipient.expiresAt ? i18n.date(recipient.expiresAt, DateTime.DATETIME_MED) : 'N/A'}
|
||||
</Trans>
|
||||
</p>
|
||||
</PopoverHover>
|
||||
) : (
|
||||
<Badge variant="secondary">
|
||||
<Clock className="mr-1 h-3 w-3" />
|
||||
<Trans>Pending</Trans>
|
||||
</Badge>
|
||||
)}
|
||||
))}
|
||||
|
||||
{envelope.status !== DocumentStatus.DRAFT &&
|
||||
recipient.signingStatus === SigningStatus.REJECTED && (
|
||||
<PopoverHover
|
||||
trigger={
|
||||
<Badge variant="destructive">
|
||||
<AlertTriangle className="mr-1 h-3 w-3" />
|
||||
<Trans>Rejected</Trans>
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
<p className="text-sm">
|
||||
<Trans>Reason for rejection: </Trans>
|
||||
</p>
|
||||
{envelope.status !== DocumentStatus.DRAFT && recipient.signingStatus === SigningStatus.REJECTED && (
|
||||
<PopoverHover
|
||||
trigger={
|
||||
<Badge variant="destructive">
|
||||
<AlertTriangle className="mr-1 h-3 w-3" />
|
||||
<Trans>Rejected</Trans>
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
<p className="text-sm">
|
||||
<Trans>Reason for rejection: </Trans>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
{recipient.rejectionReason}
|
||||
</p>
|
||||
</PopoverHover>
|
||||
)}
|
||||
<p className="mt-1 text-muted-foreground text-sm">{recipient.rejectionReason}</p>
|
||||
</PopoverHover>
|
||||
)}
|
||||
|
||||
{envelope.status === DocumentStatus.PENDING &&
|
||||
recipient.signingStatus === SigningStatus.NOT_SIGNED &&
|
||||
recipient.role !== RecipientRole.CC && (
|
||||
recipient.role !== RecipientRole.CC &&
|
||||
!isRecipientExpired(recipient) && (
|
||||
<TooltipProvider>
|
||||
<Tooltip open={shouldHighlightCopyButtons && i === 0}>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -196,9 +207,7 @@ export const DocumentPageViewRecipients = ({
|
||||
onCopySuccess={() => {
|
||||
toast({
|
||||
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.`),
|
||||
});
|
||||
setShouldHighlightCopyButtons(false);
|
||||
}}
|
||||
|
||||
+11
-18
@@ -1,16 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
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 type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
@@ -26,16 +18,19 @@ import {
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
export type DocumentRecipientLinkCopyDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
recipients: Recipient[];
|
||||
recipients: TRecipientLite[];
|
||||
};
|
||||
|
||||
export const DocumentRecipientLinkCopyDialog = ({
|
||||
trigger,
|
||||
recipients,
|
||||
}: DocumentRecipientLinkCopyDialogProps) => {
|
||||
export const DocumentRecipientLinkCopyDialog = ({ trigger, recipients }: DocumentRecipientLinkCopyDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -82,13 +77,11 @@ export const DocumentRecipientLinkCopyDialog = ({
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
You can copy and share these links to recipients so they can action the document.
|
||||
</Trans>
|
||||
<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">
|
||||
<ul className="divide-y rounded-lg border text-muted-foreground">
|
||||
{recipients.length === 0 && (
|
||||
<li className="flex flex-col items-center justify-center py-6 text-sm">
|
||||
<Trans>No recipients</Trans>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckCircle2, Clock, File, XCircle } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||
|
||||
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
type FriendlyStatus = {
|
||||
label: MessageDescriptor;
|
||||
@@ -60,12 +58,7 @@ export type DocumentStatusProps = HTMLAttributes<HTMLSpanElement> & {
|
||||
inheritColor?: boolean;
|
||||
};
|
||||
|
||||
export const DocumentStatus = ({
|
||||
className,
|
||||
status,
|
||||
inheritColor,
|
||||
...props
|
||||
}: DocumentStatusProps) => {
|
||||
export const DocumentStatus = ({ className, status, inheritColor, ...props }: DocumentStatusProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { label, icon: Icon, color } = FRIENDLY_STATUS_MAP[status];
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
|
||||
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
|
||||
import { buildDropzoneRejectionDescription } from '@documenso/ui/lib/handle-dropzone-rejection';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { DocumentUploadButton as DocumentUploadButtonPrimitive } from '@documenso/ui/primitives/document-upload-button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@documenso/ui/primitives/tooltip';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { FileRejection } from 'react-dropzone';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
@@ -35,10 +29,7 @@ export type DocumentUploadButtonLegacyProps = {
|
||||
type: EnvelopeType;
|
||||
};
|
||||
|
||||
export const DocumentUploadButtonLegacy = ({
|
||||
className,
|
||||
type,
|
||||
}: DocumentUploadButtonLegacyProps) => {
|
||||
export const DocumentUploadButtonLegacy = ({ className, type }: DocumentUploadButtonLegacyProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { user } = useSession();
|
||||
@@ -162,10 +153,10 @@ export const DocumentUploadButtonLegacy = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onFileDropRejected = () => {
|
||||
const onFileDropRejected = (fileRejections: FileRejection[]) => {
|
||||
toast({
|
||||
title: _(msg`Your document failed to upload.`),
|
||||
description: _(msg`File cannot be larger than ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB`),
|
||||
description: _(buildDropzoneRejectionDescription(fileRejections)),
|
||||
duration: 5000,
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Paperclip, Plus, X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type EmbeddedEditorAttachmentPopoverProps = {
|
||||
buttonClassName?: string;
|
||||
buttonSize?: 'sm' | 'default';
|
||||
};
|
||||
|
||||
const ZAttachmentFormSchema = z.object({
|
||||
label: z.string().min(1, 'Label is required'),
|
||||
url: z.string().url('Must be a valid URL'),
|
||||
});
|
||||
|
||||
type TAttachmentFormSchema = z.infer<typeof ZAttachmentFormSchema>;
|
||||
|
||||
// NOTE: REMEMBER TO UPDATE THE NON-EMBEDDED VERSION OF THIS COMPONENT TOO.
|
||||
export const EmbeddedEditorAttachmentPopover = ({
|
||||
buttonClassName,
|
||||
buttonSize,
|
||||
}: EmbeddedEditorAttachmentPopoverProps) => {
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
|
||||
const { envelope, setLocalEnvelope } = useCurrentEnvelopeEditor();
|
||||
|
||||
const attachments = envelope.attachments ?? [];
|
||||
|
||||
const form = useForm<TAttachmentFormSchema>({
|
||||
resolver: zodResolver(ZAttachmentFormSchema),
|
||||
defaultValues: {
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: TAttachmentFormSchema) => {
|
||||
setLocalEnvelope({
|
||||
attachments: [
|
||||
...attachments,
|
||||
{
|
||||
id: nanoid(),
|
||||
type: 'link',
|
||||
label: data.label,
|
||||
data: data.url,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
form.reset();
|
||||
setIsAdding(false);
|
||||
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Attachment added successfully.`),
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteAttachment = (id: string) => {
|
||||
setLocalEnvelope({
|
||||
attachments: attachments.filter((a) => a.id !== id),
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Attachment removed successfully.`),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className={cn('gap-2', buttonClassName)} size={buttonSize}>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
|
||||
<span>
|
||||
<Trans>Attachments</Trans>
|
||||
{attachments.length > 0 && <span className="ml-1">({attachments.length})</span>}
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="w-96" align="end">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium">
|
||||
<Trans>Attachments</Trans>
|
||||
</h4>
|
||||
<p className="mt-1 text-muted-foreground text-sm">
|
||||
<Trans>Add links to relevant documents or resources.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{attachments.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{attachments.map((attachment) => (
|
||||
<div
|
||||
key={attachment.id}
|
||||
className="flex items-center justify-between rounded-md border border-border p-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-medium text-sm">{attachment.label}</p>
|
||||
<a
|
||||
href={attachment.data}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="truncate text-muted-foreground text-xs underline hover:text-foreground"
|
||||
>
|
||||
{attachment.data}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDeleteAttachment(attachment.id)}
|
||||
className="ml-2 h-8 w-8 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isAdding && (
|
||||
<Button variant="outline" size="sm" className="w-full" onClick={() => setIsAdding(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<Trans>Add Attachment</Trans>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isAdding && (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="label"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input placeholder={_(msg`Label`)} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input type="url" placeholder={_(msg`URL`)} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setIsAdding(false);
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" size="sm" className="flex-1">
|
||||
<Trans>Add</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user