feat: add ability to change field recipient (#2194)

## Description

Add ability to change recipients for a given field.

<img width="336" height="224" alt="image"
src="https://github.com/user-attachments/assets/c122672d-68ab-4652-9c76-1a1bc874c16a"
/>

<img width="716" height="465" alt="image"
src="https://github.com/user-attachments/assets/a8e8636c-c780-4d56-910b-5522161b12d3"
/>
This commit is contained in:
David Nguyen
2025-11-15 00:46:33 +11:00
committed by GitHub
parent 5811716d12
commit dabd2564cd
4 changed files with 400 additions and 35 deletions

View File

@ -5,7 +5,7 @@ import type { FieldType } from '@prisma/client';
import Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import type { Transformer } from 'konva/lib/shapes/Transformer';
import { CopyPlusIcon, SquareStackIcon, TrashIcon } from 'lucide-react';
import { CopyPlusIcon, SquareStackIcon, TrashIcon, UserCircleIcon } from 'lucide-react';
import type { TLocalField } from '@documenso/lib/client-only/hooks/use-editor-fields';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
@ -20,8 +20,10 @@ import {
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import { CommandDialog } from '@documenso/ui/primitives/command';
import { fieldButtonList } from './envelope-editor-fields-drag-drop';
import { EnvelopeRecipientSelectorCommand } from './envelope-recipient-selector';
export default function EnvelopeEditorFieldsPageRenderer() {
const { t, i18n } = useLingui();
@ -468,6 +470,18 @@ export default function EnvelopeEditorFieldsPageRenderer() {
setSelectedFields([]);
};
const changeSelectedFieldsRecipients = (recipientId: number) => {
const fields = selectedKonvaFieldGroups
.map((field) => editorFields.getFieldByFormId(field.id()))
.filter((field) => field !== undefined);
for (const field of fields) {
if (field.recipientId !== recipientId) {
editorFields.updateFieldByFormId(field.formId, { recipientId, id: undefined });
}
}
};
const duplicatedSelectedFields = () => {
const fields = selectedKonvaFieldGroups
.map((field) => editorFields.getFieldByFormId(field.id()))
@ -552,7 +566,12 @@ export default function EnvelopeEditorFieldsPageRenderer() {
{selectedKonvaFieldGroups.length > 0 &&
interactiveTransformer.current &&
!isFieldChanging && (
<div
<FieldActionButtons
handleDuplicateSelectedFields={duplicatedSelectedFields}
handleDuplicateSelectedFieldsOnAllPages={duplicatedSelectedFieldsOnAllPages}
handleDeleteSelectedFields={deletedSelectedFields}
handleChangeRecipient={changeSelectedFieldsRecipients}
selectedFieldFormId={selectedKonvaFieldGroups.map((field) => field.id())}
style={{
position: 'absolute',
top:
@ -569,35 +588,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
pointerEvents: 'auto',
zIndex: 50,
}}
className="group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5"
>
<button
title={t`Duplicate`}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={() => duplicatedSelectedFields()}
onTouchEnd={() => duplicatedSelectedFields()}
>
<CopyPlusIcon className="h-3 w-3" />
</button>
<button
title={t`Duplicate on all pages`}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={() => duplicatedSelectedFieldsOnAllPages()}
onTouchEnd={() => duplicatedSelectedFieldsOnAllPages()}
>
<SquareStackIcon className="h-3 w-3" />
</button>
<button
title={t`Remove`}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={() => deletedSelectedFields()}
onTouchEnd={() => deletedSelectedFields()}
>
<TrashIcon className="h-3 w-3" />
</button>
</div>
/>
)}
{pendingFieldCreation && (
@ -644,3 +635,119 @@ export default function EnvelopeEditorFieldsPageRenderer() {
</div>
);
}
type FieldActionButtonsProps = React.HTMLAttributes<HTMLDivElement> & {
handleDuplicateSelectedFields: () => void;
handleDuplicateSelectedFieldsOnAllPages: () => void;
handleDeleteSelectedFields: () => void;
handleChangeRecipient: (recipientId: number) => void;
selectedFieldFormId: string[];
};
const FieldActionButtons = ({
handleDuplicateSelectedFields,
handleDuplicateSelectedFieldsOnAllPages,
handleDeleteSelectedFields,
handleChangeRecipient,
selectedFieldFormId,
...props
}: FieldActionButtonsProps) => {
const { t } = useLingui();
const [showRecipientSelector, setShowRecipientSelector] = useState(false);
const { editorFields, envelope } = useCurrentEnvelopeEditor();
/**
* Decide the preselected recipient in the command input.
*
* If all fields belong to the same recipient then use that recipient as the default.
*
* Otherwise show the placeholder.
*/
const preselectedRecipient = useMemo(() => {
if (selectedFieldFormId.length === 0) {
return null;
}
const fields = editorFields.localFields.filter((field) =>
selectedFieldFormId.includes(field.formId),
);
const recipient = envelope.recipients.find(
(recipient) => recipient.id === fields[0].recipientId,
);
if (!recipient) {
return null;
}
const isRecipientsSame = fields.every((field) => field.recipientId === recipient.id);
if (isRecipientsSame) {
return recipient;
}
return null;
}, [editorFields.localFields]);
return (
<div className="flex flex-col items-center" {...props}>
<div className="group flex w-fit items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
<button
title={t`Change Recipient`}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={() => setShowRecipientSelector(true)}
onTouchEnd={() => setShowRecipientSelector(true)}
>
<UserCircleIcon className="h-3 w-3" />
</button>
<button
title={t`Duplicate`}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={handleDuplicateSelectedFields}
onTouchEnd={handleDuplicateSelectedFields}
>
<CopyPlusIcon className="h-3 w-3" />
</button>
<button
title={t`Duplicate on all pages`}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={handleDuplicateSelectedFieldsOnAllPages}
onTouchEnd={handleDuplicateSelectedFieldsOnAllPages}
>
<SquareStackIcon className="h-3 w-3" />
</button>
<button
title={t`Remove`}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={handleDeleteSelectedFields}
onTouchEnd={handleDeleteSelectedFields}
>
<TrashIcon className="h-3 w-3" />
</button>
</div>
<CommandDialog
position="start"
open={showRecipientSelector}
onOpenChange={setShowRecipientSelector}
>
<EnvelopeRecipientSelectorCommand
placeholder={t`Select a recipient`}
selectedRecipient={preselectedRecipient}
onSelectedRecipientChange={(recipient) => {
editorFields.setSelectedRecipient(recipient.id);
handleChangeRecipient(recipient.id);
setShowRecipientSelector(false);
}}
recipients={envelope.recipients}
fields={envelope.fields}
/>
</CommandDialog>
</div>
);
};