mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 03:01:59 +10:00
feat: expiry endpoint
This commit is contained in:
@ -419,6 +419,8 @@ export const EditDocumentForm = ({
|
|||||||
isDocumentEnterprise={isDocumentEnterprise}
|
isDocumentEnterprise={isDocumentEnterprise}
|
||||||
onSubmit={onAddSignersFormSubmit}
|
onSubmit={onAddSignersFormSubmit}
|
||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||||
|
documentId={document.id}
|
||||||
|
// teamId={team?.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddFieldsFormPartial
|
<AddFieldsFormPartial
|
||||||
|
|||||||
107
packages/lib/server-only/recipient/set-recipient-expiry.ts
Normal file
107
packages/lib/server-only/recipient/set-recipient-expiry.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import type { Team } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
|
|
||||||
|
export type SetRecipientExpiryOptions = {
|
||||||
|
documentId: number;
|
||||||
|
recipientId: number;
|
||||||
|
expiry: Date;
|
||||||
|
userId: number;
|
||||||
|
teamId?: number;
|
||||||
|
requestMetadata?: RequestMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setRecipientExpiry = async ({
|
||||||
|
documentId,
|
||||||
|
recipientId,
|
||||||
|
expiry,
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
requestMetadata,
|
||||||
|
}: SetRecipientExpiryOptions) => {
|
||||||
|
const recipient = await prisma.recipient.findFirst({
|
||||||
|
where: {
|
||||||
|
id: recipientId,
|
||||||
|
Document: {
|
||||||
|
id: documentId,
|
||||||
|
...(teamId
|
||||||
|
? {
|
||||||
|
team: {
|
||||||
|
id: teamId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
userId,
|
||||||
|
teamId: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!recipient) {
|
||||||
|
throw new Error('Recipient not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let team: Team | null = null;
|
||||||
|
|
||||||
|
if (teamId) {
|
||||||
|
team = await prisma.team.findFirst({
|
||||||
|
where: {
|
||||||
|
id: teamId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedRecipient = await prisma.$transaction(async (tx) => {
|
||||||
|
const updated = await tx.recipient.update({
|
||||||
|
where: {
|
||||||
|
id: recipient.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
expired: new Date(expiry),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: fix the audit logs
|
||||||
|
// await tx.documentAuditLog.create({
|
||||||
|
// data: createDocumentAuditLogData({
|
||||||
|
// type: 'RECIPIENT_EXPIRY_UPDATED',
|
||||||
|
// documentId,
|
||||||
|
// user: {
|
||||||
|
// id: team?.id ?? user.id,
|
||||||
|
// email: team?.name ?? user.email,
|
||||||
|
// name: team ? '' : user.name,
|
||||||
|
// },
|
||||||
|
// data: {
|
||||||
|
// recipientEmail: recipient.email,
|
||||||
|
// recipientName: recipient.name,
|
||||||
|
// recipientId: recipient.id,
|
||||||
|
// recipientRole: recipient.role,
|
||||||
|
// expiry,
|
||||||
|
// },
|
||||||
|
// requestMetadata,
|
||||||
|
// }),
|
||||||
|
// });
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedRecipient;
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server';
|
|||||||
|
|
||||||
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
||||||
import { rejectDocumentWithToken } from '@documenso/lib/server-only/document/reject-document-with-token';
|
import { rejectDocumentWithToken } from '@documenso/lib/server-only/document/reject-document-with-token';
|
||||||
|
import { setRecipientExpiry } from '@documenso/lib/server-only/recipient/set-recipient-expiry';
|
||||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||||
import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template';
|
import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template';
|
||||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
@ -12,6 +13,7 @@ import {
|
|||||||
ZAddTemplateSignersMutationSchema,
|
ZAddTemplateSignersMutationSchema,
|
||||||
ZCompleteDocumentWithTokenMutationSchema,
|
ZCompleteDocumentWithTokenMutationSchema,
|
||||||
ZRejectDocumentWithTokenMutationSchema,
|
ZRejectDocumentWithTokenMutationSchema,
|
||||||
|
ZSetSignerExpirySchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
export const recipientRouter = router({
|
export const recipientRouter = router({
|
||||||
@ -45,6 +47,30 @@ export const recipientRouter = router({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setSignerExpiry: authenticatedProcedure
|
||||||
|
.input(ZSetSignerExpirySchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const { documentId, signerId, expiry, teamId } = input;
|
||||||
|
|
||||||
|
return await setRecipientExpiry({
|
||||||
|
documentId,
|
||||||
|
recipientId: signerId,
|
||||||
|
expiry,
|
||||||
|
teamId,
|
||||||
|
userId: ctx.user.id,
|
||||||
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: "We're unable to set the expiry for this signer. Please try again later.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
addTemplateSigners: authenticatedProcedure
|
addTemplateSigners: authenticatedProcedure
|
||||||
.input(ZAddTemplateSignersMutationSchema)
|
.input(ZAddTemplateSignersMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@ -80,3 +80,12 @@ export const ZRejectDocumentWithTokenMutationSchema = z.object({
|
|||||||
export type TRejectDocumentWithTokenMutationSchema = z.infer<
|
export type TRejectDocumentWithTokenMutationSchema = z.infer<
|
||||||
typeof ZRejectDocumentWithTokenMutationSchema
|
typeof ZRejectDocumentWithTokenMutationSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export const ZSetSignerExpirySchema = z.object({
|
||||||
|
documentId: z.number(),
|
||||||
|
signerId: z.number(),
|
||||||
|
expiry: z.date(),
|
||||||
|
teamId: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSetSignerExpirySchema = z.infer<typeof ZSetSignerExpirySchema>;
|
||||||
|
|||||||
@ -52,6 +52,7 @@ export type AddSignersFormProps = {
|
|||||||
isDocumentEnterprise: boolean;
|
isDocumentEnterprise: boolean;
|
||||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||||
isDocumentPdfLoaded: boolean;
|
isDocumentPdfLoaded: boolean;
|
||||||
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddSignersFormPartial = ({
|
export const AddSignersFormPartial = ({
|
||||||
@ -62,6 +63,7 @@ export const AddSignersFormPartial = ({
|
|||||||
isDocumentEnterprise,
|
isDocumentEnterprise,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isDocumentPdfLoaded,
|
isDocumentPdfLoaded,
|
||||||
|
documentId,
|
||||||
}: AddSignersFormProps) => {
|
}: AddSignersFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -634,6 +636,8 @@ export const AddSignersFormPartial = ({
|
|||||||
'mb-6': form.formState.errors.signers?.[index],
|
'mb-6': form.formState.errors.signers?.[index],
|
||||||
})}
|
})}
|
||||||
onDelete={() => onRemoveSigner(index)}
|
onDelete={() => onRemoveSigner(index)}
|
||||||
|
signer={signer}
|
||||||
|
documentId={documentId}
|
||||||
deleteDisabled={
|
deleteDisabled={
|
||||||
snapshot.isDragging ||
|
snapshot.isDragging ||
|
||||||
isSubmitting ||
|
isSubmitting ||
|
||||||
|
|||||||
@ -6,10 +6,7 @@ import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-a
|
|||||||
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
|
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
|
||||||
import { DocumentSigningOrder, RecipientRole } from '.prisma/client';
|
import { DocumentSigningOrder, RecipientRole } from '.prisma/client';
|
||||||
|
|
||||||
export const ZAddSignersFormSchema = z
|
export const ZAddSignerSchema = z.object({
|
||||||
.object({
|
|
||||||
signers: z.array(
|
|
||||||
z.object({
|
|
||||||
formId: z.string().min(1),
|
formId: z.string().min(1),
|
||||||
nativeId: z.number().optional(),
|
nativeId: z.number().optional(),
|
||||||
email: z
|
email: z
|
||||||
@ -19,11 +16,12 @@ export const ZAddSignersFormSchema = z
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(ZRecipientActionAuthTypesSchema.optional()),
|
||||||
ZRecipientActionAuthTypesSchema.optional(),
|
});
|
||||||
),
|
|
||||||
}),
|
export const ZAddSignersFormSchema = z
|
||||||
),
|
.object({
|
||||||
|
signers: z.array(ZAddSignerSchema),
|
||||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
@ -37,3 +35,4 @@ export const ZAddSignersFormSchema = z
|
|||||||
);
|
);
|
||||||
|
|
||||||
export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;
|
export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;
|
||||||
|
export type TAddSignerSchema = z.infer<typeof ZAddSignerSchema>;
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Calendar as CalendarIcon } from 'lucide-react';
|
import { Calendar as CalendarIcon } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Calendar } from '@documenso/ui/primitives/calendar';
|
import { Calendar } from '@documenso/ui/primitives/calendar';
|
||||||
import {
|
import {
|
||||||
@ -30,9 +34,11 @@ import {
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||||
|
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
import { useToast } from '../use-toast';
|
||||||
|
import type { TAddSignerSchema as Signer } from './add-signers.types';
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
expiryDate: z.date({
|
expiry: z.date({
|
||||||
required_error: 'Please select an expiry date.',
|
required_error: 'Please select an expiry date.',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -40,38 +46,62 @@ const formSchema = z.object({
|
|||||||
type DocumentExpiryDialogProps = {
|
type DocumentExpiryDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
|
signer: Signer;
|
||||||
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DocumentExpiryDialog({ open, onOpenChange }: DocumentExpiryDialogProps) {
|
export default function DocumentExpiryDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
signer,
|
||||||
|
documentId,
|
||||||
|
}: DocumentExpiryDialogProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
// const { mutateAsync: moveDocument, isLoading } = trpc.document.moveDocumentToTeam.useMutation({
|
const { mutateAsync: setSignerExpiry, isLoading } = trpc.recipient.setSignerExpiry.useMutation({
|
||||||
// onSuccess: () => {
|
onSuccess: () => {
|
||||||
// router.refresh();
|
router.refresh();
|
||||||
// toast({
|
toast({
|
||||||
// title: _(msg`Document moved`),
|
title: _(msg`Signer Expiry Set`),
|
||||||
// description: _(msg`The document has been successfully moved to the selected team.`),
|
description: _(msg`The expiry date for the signer has been set.`),
|
||||||
// duration: 5000,
|
duration: 5000,
|
||||||
// });
|
});
|
||||||
// onOpenChange(false);
|
|
||||||
// },
|
|
||||||
// onError: (error) => {
|
|
||||||
// toast({
|
|
||||||
// title: _(msg`Error`),
|
|
||||||
// description: error.message || _(msg`An error occurred while moving the document.`),
|
|
||||||
// variant: 'destructive',
|
|
||||||
// duration: 7500,
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
|
||||||
console.log(values);
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: error.message || _(msg`An error occurred while setting the expiry date.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: 7500,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSetExpiry = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
if (!signer.nativeId) {
|
||||||
|
return toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: _(msg`An error occurred while setting the expiry date.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: 7500,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await setSignerExpiry({
|
||||||
|
documentId,
|
||||||
|
signerId: signer.nativeId,
|
||||||
|
expiry: new Date(values.expiry),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[450px]">
|
<DialogContent className="sm:max-w-[450px]">
|
||||||
@ -83,10 +113,10 @@ export default function DocumentExpiryDialog({ open, onOpenChange }: DocumentExp
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
<form onSubmit={form.handleSubmit(onSetExpiry)} className="space-y-8">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="expiryDate"
|
name="expiry"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col">
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel>Expiry Date</FormLabel>
|
<FormLabel>Expiry Date</FormLabel>
|
||||||
@ -128,7 +158,7 @@ export default function DocumentExpiryDialog({ open, onOpenChange }: DocumentExp
|
|||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button type="submit">
|
<Button type="submit" loading={isLoading}>
|
||||||
<Trans>Save Changes</Trans>
|
<Trans>Save Changes</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -13,15 +13,23 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dropdown-menu';
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
|
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
import type { TAddSignerSchema as Signer } from './add-signers.types';
|
||||||
import DocumentExpiryDialog from './document-expiry-dialog';
|
import DocumentExpiryDialog from './document-expiry-dialog';
|
||||||
|
|
||||||
type SignerActionDropdownProps = {
|
type SignerActionDropdownProps = {
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
deleteDisabled?: boolean;
|
deleteDisabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
signer: Signer;
|
||||||
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SignerActionDropdown({ deleteDisabled, className }: SignerActionDropdownProps) {
|
export function SignerActionDropdown({
|
||||||
|
deleteDisabled,
|
||||||
|
className,
|
||||||
|
signer,
|
||||||
|
documentId,
|
||||||
|
}: SignerActionDropdownProps) {
|
||||||
const [isExpiryDialogOpen, setExpiryDialogOpen] = useState(false);
|
const [isExpiryDialogOpen, setExpiryDialogOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -45,7 +53,12 @@ export function SignerActionDropdown({ deleteDisabled, className }: SignerAction
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<DocumentExpiryDialog open={isExpiryDialogOpen} onOpenChange={setExpiryDialogOpen} />
|
<DocumentExpiryDialog
|
||||||
|
open={isExpiryDialogOpen}
|
||||||
|
onOpenChange={setExpiryDialogOpen}
|
||||||
|
signer={signer}
|
||||||
|
documentId={documentId}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user