mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
fix: prevent client side distribution when missing signatures (#2260)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -7,9 +7,7 @@ import {
|
||||
DocumentDistributionMethod,
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
type Field,
|
||||
FieldType,
|
||||
type Recipient,
|
||||
RecipientRole,
|
||||
} from '@prisma/client';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
@@ -19,8 +17,8 @@ import { useNavigate } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@@ -52,16 +50,13 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { SpinnerBox } from '@documenso/ui/primitives/spinner';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type EnvelopeDistributeDialogProps = {
|
||||
envelope: Pick<TEnvelope, 'id' | 'userId' | 'teamId' | 'status' | 'type' | 'documentMeta'> & {
|
||||
recipients: Recipient[];
|
||||
fields: Pick<Field, 'type' | 'recipientId'>[];
|
||||
};
|
||||
onDistribute?: () => Promise<void>;
|
||||
documentRootPath: string;
|
||||
trigger?: React.ReactNode;
|
||||
@@ -86,20 +81,20 @@ export const ZEnvelopeDistributeFormSchema = z.object({
|
||||
export type TEnvelopeDistributeFormSchema = z.infer<typeof ZEnvelopeDistributeFormSchema>;
|
||||
|
||||
export const EnvelopeDistributeDialog = ({
|
||||
envelope,
|
||||
trigger,
|
||||
documentRootPath,
|
||||
onDistribute,
|
||||
}: EnvelopeDistributeDialogProps) => {
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const recipients = envelope.recipients;
|
||||
const { envelope, syncEnvelope, isAutosaving, autosaveError } = useCurrentEnvelopeEditor();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
const { mutateAsync: distributeEnvelope } = trpcReact.envelope.distribute.useMutation();
|
||||
|
||||
@@ -189,6 +184,29 @@ export const EnvelopeDistributeDialog = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
if (isSyncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSyncing(true);
|
||||
|
||||
try {
|
||||
await syncEnvelope();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
setIsSyncing(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Resync the whole envelope if the envelope is mid saving.
|
||||
if (isOpen && (isAutosaving || autosaveError)) {
|
||||
void handleSync();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (envelope.status !== DocumentStatus.DRAFT || envelope.type !== EnvelopeType.DOCUMENT) {
|
||||
return null;
|
||||
}
|
||||
@@ -208,7 +226,7 @@ export const EnvelopeDistributeDialog = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!invalidEnvelopeCode ? (
|
||||
{!invalidEnvelopeCode || isSyncing ? (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={isSubmitting}>
|
||||
@@ -236,7 +254,16 @@ export const EnvelopeDistributeDialog = ({
|
||||
})}
|
||||
>
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
{distributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||
{isSyncing ? (
|
||||
<motion.div
|
||||
key={'Flushing'}
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
|
||||
exit={{ opacity: 0, transition: { duration: 0.15 } }}
|
||||
>
|
||||
<SpinnerBox spinnerProps={{ size: 'sm' }} className="h-72" />
|
||||
</motion.div>
|
||||
) : distributionMethod === DocumentDistributionMethod.EMAIL ? (
|
||||
<motion.div
|
||||
key={'Emails'}
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
@@ -339,7 +366,7 @@ export const EnvelopeDistributeDialog = ({
|
||||
<TooltipTrigger type="button">
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-muted-foreground p-4">
|
||||
<TooltipContent className="p-4 text-muted-foreground">
|
||||
<DocumentSendEmailMessageHelper />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -347,7 +374,7 @@ export const EnvelopeDistributeDialog = ({
|
||||
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="bg-background mt-2 h-16 resize-none"
|
||||
className="mt-2 h-16 resize-none bg-background"
|
||||
{...field}
|
||||
maxLength={5000}
|
||||
/>
|
||||
@@ -359,9 +386,7 @@ export const EnvelopeDistributeDialog = ({
|
||||
</fieldset>
|
||||
</Form>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{distributionMethod === DocumentDistributionMethod.NONE && (
|
||||
) : distributionMethod === DocumentDistributionMethod.NONE ? (
|
||||
<motion.div
|
||||
key={'Links'}
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
@@ -369,7 +394,7 @@ export const EnvelopeDistributeDialog = ({
|
||||
exit={{ opacity: 0, transition: { duration: 0.15 } }}
|
||||
className="min-h-60 rounded-lg border"
|
||||
>
|
||||
<div className="text-muted-foreground py-24 text-center text-sm">
|
||||
<div className="py-24 text-center text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans>We won't send anything to notify recipients.</Trans>
|
||||
</p>
|
||||
@@ -382,7 +407,7 @@ export const EnvelopeDistributeDialog = ({
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
@@ -393,7 +418,7 @@ export const EnvelopeDistributeDialog = ({
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button loading={isSubmitting} type="submit">
|
||||
<Button loading={isSubmitting} disabled={isSyncing} type="submit">
|
||||
{distributionMethod === DocumentDistributionMethod.EMAIL ? (
|
||||
<Trans>Send</Trans>
|
||||
) : (
|
||||
|
||||
@@ -30,18 +30,11 @@ import { EnvelopeItemTitleInput } from './envelope-editor-title-input';
|
||||
export default function EnvelopeEditorHeader() {
|
||||
const { t } = useLingui();
|
||||
|
||||
const {
|
||||
envelope,
|
||||
isDocument,
|
||||
isTemplate,
|
||||
updateEnvelope,
|
||||
autosaveError,
|
||||
relativePath,
|
||||
editorFields,
|
||||
} = useCurrentEnvelopeEditor();
|
||||
const { envelope, isDocument, isTemplate, updateEnvelope, autosaveError, relativePath } =
|
||||
useCurrentEnvelopeEditor();
|
||||
|
||||
return (
|
||||
<nav className="bg-background border-border w-full border-b px-4 py-3 md:px-6">
|
||||
<nav className="w-full border-b border-border bg-background px-4 py-3 md:px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link to="/">
|
||||
@@ -147,10 +140,6 @@ export default function EnvelopeEditorHeader() {
|
||||
{isDocument && (
|
||||
<>
|
||||
<EnvelopeDistributeDialog
|
||||
envelope={{
|
||||
...envelope,
|
||||
fields: editorFields.localFields,
|
||||
}}
|
||||
documentRootPath={relativePath.documentRootPath}
|
||||
trigger={
|
||||
<Button size="sm">
|
||||
|
||||
@@ -152,30 +152,30 @@ export default function EnvelopeEditor() {
|
||||
envelopeEditorSteps.find((step) => step.id === currentStep) || envelopeEditorSteps[0];
|
||||
|
||||
return (
|
||||
<div className="dark:bg-background h-screen w-screen bg-gray-50">
|
||||
<div className="h-screen w-screen bg-gray-50 dark:bg-background">
|
||||
<EnvelopeEditorHeader />
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex h-[calc(100vh-4rem)] w-screen">
|
||||
{/* Left Section - Step Navigation */}
|
||||
<div className="bg-background border-border flex w-80 flex-shrink-0 flex-col overflow-y-auto border-r py-4">
|
||||
<div className="flex w-80 flex-shrink-0 flex-col overflow-y-auto border-r border-border bg-background py-4">
|
||||
{/* Left section step selector. */}
|
||||
<div className="px-4">
|
||||
<h3 className="text-foreground flex items-end justify-between text-sm font-semibold">
|
||||
<h3 className="flex items-end justify-between text-sm font-semibold text-foreground">
|
||||
{isDocument ? <Trans>Document Editor</Trans> : <Trans>Template Editor</Trans>}
|
||||
|
||||
<span className="text-muted-foreground bg-muted/50 ml-2 rounded border px-2 py-0.5 text-xs">
|
||||
<span className="ml-2 rounded border bg-muted/50 px-2 py-0.5 text-xs text-muted-foreground">
|
||||
<Trans context="The step counter">
|
||||
Step {currentStepData.order}/{envelopeEditorSteps.length}
|
||||
</Trans>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<div className="bg-muted relative my-4 h-[4px] rounded-md">
|
||||
<div className="relative my-4 h-[4px] rounded-md bg-muted">
|
||||
<motion.div
|
||||
layout="size"
|
||||
layoutId="document-flow-container-step"
|
||||
className="bg-documenso absolute inset-y-0 left-0"
|
||||
className="absolute inset-y-0 left-0 bg-documenso"
|
||||
style={{
|
||||
width: `${(100 / envelopeEditorSteps.length) * (currentStepData.order ?? 0)}%`,
|
||||
}}
|
||||
@@ -219,7 +219,7 @@ export default function EnvelopeEditor() {
|
||||
>
|
||||
{t(step.title)}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">{t(step.description)}</div>
|
||||
<div className="text-xs text-muted-foreground">{t(step.description)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -232,7 +232,7 @@ export default function EnvelopeEditor() {
|
||||
|
||||
{/* Quick Actions. */}
|
||||
<div className="space-y-3 px-4">
|
||||
<h4 className="text-foreground text-sm font-semibold">
|
||||
<h4 className="text-sm font-semibold text-foreground">
|
||||
<Trans>Quick Actions</Trans>
|
||||
</h4>
|
||||
<EnvelopeEditorSettingsDialog
|
||||
@@ -246,10 +246,6 @@ export default function EnvelopeEditor() {
|
||||
|
||||
{isDocument && (
|
||||
<EnvelopeDistributeDialog
|
||||
envelope={{
|
||||
...envelope,
|
||||
fields: editorFields.localFields,
|
||||
}}
|
||||
documentRootPath={relativePath.documentRootPath}
|
||||
trigger={
|
||||
<Button variant="ghost" size="sm" className="w-full justify-start">
|
||||
|
||||
@@ -132,7 +132,12 @@ export const EnvelopeEditorProvider = ({
|
||||
});
|
||||
|
||||
const envelopeFieldSetMutationQuery = trpc.envelope.field.set.useMutation({
|
||||
onSuccess: () => {
|
||||
onSuccess: ({ data: fields }) => {
|
||||
setEnvelope((prev) => ({
|
||||
...prev,
|
||||
fields,
|
||||
}));
|
||||
|
||||
setAutosaveError(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -154,8 +159,18 @@ export const EnvelopeEditorProvider = ({
|
||||
setEnvelope((prev) => ({
|
||||
...prev,
|
||||
recipients,
|
||||
fields: prev.fields.filter((field) =>
|
||||
recipients.some((recipient) => recipient.id === field.recipientId),
|
||||
),
|
||||
}));
|
||||
|
||||
// Reset the local fields to ensure deleted recipient fields are removed.
|
||||
editorFields.resetForm(
|
||||
envelope.fields.filter((field) =>
|
||||
recipients.some((recipient) => recipient.id === field.recipientId),
|
||||
),
|
||||
);
|
||||
|
||||
setAutosaveError(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -265,7 +280,7 @@ export const EnvelopeEditorProvider = ({
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetch and sycn the envelope back into the editor.
|
||||
* Fetch and sync the envelope back into the editor.
|
||||
*
|
||||
* Overrides everything.
|
||||
*/
|
||||
|
||||
@@ -66,7 +66,7 @@ export const setEnvelopeFieldsRoute = authenticatedProcedure
|
||||
|
||||
return {
|
||||
data: result.fields.map((field) => ({
|
||||
id: field.id,
|
||||
...field,
|
||||
formId: field.formId,
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ZClampedFieldPositionXSchema,
|
||||
ZClampedFieldPositionYSchema,
|
||||
ZClampedFieldWidthSchema,
|
||||
ZEnvelopeFieldSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
|
||||
@@ -37,12 +38,9 @@ export const ZSetEnvelopeFieldsRequestSchema = z.object({
|
||||
});
|
||||
|
||||
export const ZSetEnvelopeFieldsResponseSchema = z.object({
|
||||
data: z
|
||||
.object({
|
||||
id: z.number(),
|
||||
formId: z.string().optional(),
|
||||
})
|
||||
.array(),
|
||||
data: ZEnvelopeFieldSchema.extend({
|
||||
formId: z.string().optional(),
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export type TSetEnvelopeFieldsRequest = z.infer<typeof ZSetEnvelopeFieldsRequestSchema>;
|
||||
|
||||
Reference in New Issue
Block a user