feat: allow editing pending documents (#1346)

## Description

Adds the ability for the document owner to edit recipients and their
fields after the document has been sent.

A recipient can only be updated or deleted if:
- The recipient has not inserted any fields
- Has not completed the document

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

- **New Features**
- Added new localization messages to clarify user actions regarding
document signing.
  - Enhanced French translations for improved user interaction.

- **Improvements**
- Updated localization strings in German and English for clearer
feedback on signer and recipient statuses.
- Improved overall structure of localization files for better
maintainability.

- **Dependency Updates**
- Upgraded `next-axiom` and `remeda` libraries to their latest versions,
potentially enhancing performance and stability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Mythie <me@lucasjamessmith.me>
This commit is contained in:
David Nguyen
2024-09-20 13:58:21 +10:00
committed by GitHub
parent efb2bc94ab
commit ddee8a8272
21 changed files with 533 additions and 204 deletions

View File

@ -32,11 +32,16 @@ import {
ZFieldMetaSchema,
} from '@documenso/lib/types/field-meta';
import { nanoid } from '@documenso/lib/universal/id';
import {
canRecipientBeModified,
canRecipientFieldsBeModified,
} from '@documenso/lib/utils/recipients';
import type { Field, Recipient } from '@documenso/prisma/client';
import { FieldType, RecipientRole, SendStatus } from '@documenso/prisma/client';
import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors';
import { cn } from '../../lib/utils';
import { Alert, AlertDescription } from '../alert';
import { Button } from '../button';
import { Card, CardContent } from '../card';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../command';
@ -191,8 +196,6 @@ export const AddFieldsFormPartial = ({
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
);
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) =>
fields
.filter((field) => field.type === fieldType)
@ -225,11 +228,13 @@ export const AddFieldsFormPartial = ({
const hasErrors =
emptyCheckboxFields.length > 0 || emptyRadioFields.length > 0 || emptySelectFields.length > 0;
const isFieldsDisabled =
!selectedSigner ||
hasSelectedSignerBeenSent ||
selectedSigner?.role === RecipientRole.VIEWER ||
selectedSigner?.role === RecipientRole.CC;
const isFieldsDisabled = useMemo(() => {
if (!selectedSigner) {
return true;
}
return !canRecipientFieldsBeModified(selectedSigner, fields);
}, [selectedSigner, fields]);
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
const [coords, setCoords] = useState({
@ -568,7 +573,8 @@ export const AddFieldsFormPartial = ({
recipientIndex={recipientIndex === -1 ? 0 : recipientIndex}
field={field}
disabled={
selectedSigner?.email !== field.signerEmail || hasSelectedSignerBeenSent
selectedSigner?.email !== field.signerEmail ||
!canRecipientBeModified(selectedSigner, fields)
}
minHeight={fieldBounds.current.height}
minWidth={fieldBounds.current.width}
@ -976,6 +982,7 @@ export const AddFieldsFormPartial = ({
</div>
</div>
</DocumentFlowFormContainerContent>
{hasErrors && (
<div className="mt-4">
<ul>
@ -993,6 +1000,18 @@ export const AddFieldsFormPartial = ({
</ul>
</div>
)}
{selectedSigner && !canRecipientFieldsBeModified(selectedSigner, fields) && (
<Alert variant="warning">
<AlertDescription>
<Trans>
This recipient can no longer be modified as they have signed a field, or completed
the document.
</Trans>
</AlertDescription>
</Alert>
)}
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />

View File

@ -16,6 +16,7 @@ import { prop, sortBy } from 'remeda';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id';
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
import type { Field, Recipient } from '@documenso/prisma/client';
import { DocumentSigningOrder, RecipientRole, SendStatus } from '@documenso/prisma/client';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
@ -159,21 +160,19 @@ export const AddSignersFormPartial = ({
(recipient) => recipient.sendStatus === SendStatus.SENT,
);
const hasBeenSentToRecipientId = useCallback(
(id?: number) => {
if (!id) {
return false;
}
const canRecipientBeModified = (recipientId?: number) => {
if (recipientId === undefined) {
return true;
}
return recipients.some(
(recipient) =>
recipient.id === id &&
recipient.sendStatus === SendStatus.SENT &&
recipient.role !== RecipientRole.CC,
);
},
[recipients],
);
const recipient = recipients.find((recipient) => recipient.id === recipientId);
if (!recipient) {
return false;
}
return utilCanRecipientBeModified(recipient, fields);
};
const onAddSigner = () => {
appendSigner({
@ -189,10 +188,10 @@ export const AddSignersFormPartial = ({
const onRemoveSigner = (index: number) => {
const signer = signers[index];
if (hasBeenSentToRecipientId(signer.nativeId)) {
if (!canRecipientBeModified(signer.nativeId)) {
toast({
title: _(msg`Cannot remove signer`),
description: _(msg`This signer has already received the document.`),
description: _(msg`This signer has already signed the document.`),
variant: 'destructive',
});
@ -235,7 +234,7 @@ export const AddSignersFormPartial = ({
const [reorderedSigner] = items.splice(result.source.index, 1);
let insertIndex = result.destination.index;
while (insertIndex < items.length && hasBeenSentToRecipientId(items[insertIndex].nativeId)) {
while (insertIndex < items.length && !canRecipientBeModified(items[insertIndex].nativeId)) {
insertIndex++;
}
@ -243,7 +242,7 @@ export const AddSignersFormPartial = ({
const updatedSigners = items.map((item, index) => ({
...item,
signingOrder: hasBeenSentToRecipientId(item.nativeId) ? item.signingOrder : index + 1,
signingOrder: !canRecipientBeModified(item.nativeId) ? item.signingOrder : index + 1,
}));
updatedSigners.forEach((item, index) => {
@ -270,7 +269,7 @@ export const AddSignersFormPartial = ({
await form.trigger('signers');
},
[form, hasBeenSentToRecipientId, watchedSigners],
[form, canRecipientBeModified, watchedSigners],
);
const triggerDragAndDrop = useCallback(
@ -315,9 +314,19 @@ export const AddSignersFormPartial = ({
if (index === oldIndex) {
return { ...signer, signingOrder: newIndex + 1 };
} else if (index >= newIndex && index < oldIndex) {
return { ...signer, signingOrder: (signer.signingOrder ?? index + 1) + 1 };
return {
...signer,
signingOrder: !canRecipientBeModified(signer.nativeId)
? signer.signingOrder
: (signer.signingOrder ?? index + 1) + 1,
};
} else if (index <= newIndex && index > oldIndex) {
return { ...signer, signingOrder: Math.max(1, (signer.signingOrder ?? index + 1) - 1) };
return {
...signer,
signingOrder: !canRecipientBeModified(signer.nativeId)
? signer.signingOrder
: Math.max(1, (signer.signingOrder ?? index + 1) - 1),
};
}
return signer;
});
@ -326,7 +335,7 @@ export const AddSignersFormPartial = ({
form.setValue(`signers.${index}.signingOrder`, signer.signingOrder);
});
},
[form],
[form, canRecipientBeModified],
);
const handleSigningOrderChange = useCallback(
@ -417,7 +426,7 @@ export const AddSignersFormPartial = ({
isDragDisabled={
!isSigningOrderSequential ||
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
!canRecipientBeModified(signer.nativeId) ||
!signer.signingOrder
}
>
@ -433,7 +442,7 @@ export const AddSignersFormPartial = ({
>
<motion.fieldset
data-native-id={signer.nativeId}
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
disabled={isSubmitting || !canRecipientBeModified(signer.nativeId)}
className={cn('grid grid-cols-10 items-end gap-2 pb-2', {
'border-b pt-2': showAdvancedSettings,
'grid-cols-12 pr-3': isSigningOrderSequential,
@ -466,7 +475,7 @@ export const AddSignersFormPartial = ({
disabled={
snapshot.isDragging ||
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId)
!canRecipientBeModified(signer.nativeId)
}
/>
</FormControl>
@ -500,7 +509,7 @@ export const AddSignersFormPartial = ({
disabled={
snapshot.isDragging ||
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId)
!canRecipientBeModified(signer.nativeId)
}
onKeyDown={onKeyDown}
/>
@ -534,7 +543,7 @@ export const AddSignersFormPartial = ({
disabled={
snapshot.isDragging ||
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId)
!canRecipientBeModified(signer.nativeId)
}
onKeyDown={onKeyDown}
/>
@ -562,7 +571,7 @@ export const AddSignersFormPartial = ({
disabled={
snapshot.isDragging ||
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId)
!canRecipientBeModified(signer.nativeId)
}
/>
</FormControl>
@ -585,7 +594,7 @@ export const AddSignersFormPartial = ({
disabled={
snapshot.isDragging ||
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId)
!canRecipientBeModified(signer.nativeId)
}
/>
</FormControl>
@ -601,7 +610,7 @@ export const AddSignersFormPartial = ({
disabled={
snapshot.isDragging ||
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
!canRecipientBeModified(signer.nativeId) ||
signers.length === 1
}
onClick={() => onRemoveSigner(index)}

View File

@ -210,6 +210,7 @@ export const FieldItem = ({
onFocus?.();
}}
ref={$el}
data-field-id={field.nativeId}
>
{match(field.type)
.with('CHECKBOX', () => <CheckboxField field={field} />)