diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx
index 2dba30c18..b30026f16 100644
--- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx
+++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx
@@ -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 && (
-
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"
- >
-
-
-
-
-
-
+ />
)}
{pendingFieldCreation && (
@@ -644,3 +635,119 @@ export default function EnvelopeEditorFieldsPageRenderer() {
);
}
+
+type FieldActionButtonsProps = React.HTMLAttributes & {
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ editorFields.setSelectedRecipient(recipient.id);
+ handleChangeRecipient(recipient.id);
+ setShowRecipientSelector(false);
+ }}
+ recipients={envelope.recipients}
+ fields={envelope.fields}
+ />
+
+
+ );
+};
diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
index ceb8e072a..e4b3bf82f 100644
--- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
+++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
@@ -29,7 +29,6 @@ import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animat
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
-import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector';
import { Separator } from '@documenso/ui/primitives/separator';
import { EditorFieldCheckboxForm } from '~/components/forms/editor/editor-field-checkbox-form';
@@ -45,6 +44,7 @@ import { EditorFieldTextForm } from '~/components/forms/editor/editor-field-text
import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop';
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
+import { EnvelopeRecipientSelector } from './envelope-recipient-selector';
const EnvelopeEditorFieldsPageRenderer = lazy(
async () => import('./envelope-editor-fields-page-renderer'),
@@ -164,12 +164,13 @@ export const EnvelopeEditorFieldsPage = () => {
Selected Recipient
-
editorFields.setSelectedRecipient(recipient.id)
}
recipients={envelope.recipients}
+ fields={envelope.fields}
className="w-full"
align="end"
/>
diff --git a/apps/remix/app/components/general/envelope-editor/envelope-recipient-selector.tsx b/apps/remix/app/components/general/envelope-editor/envelope-recipient-selector.tsx
new file mode 100644
index 000000000..5ebe602c9
--- /dev/null
+++ b/apps/remix/app/components/general/envelope-editor/envelope-recipient-selector.tsx
@@ -0,0 +1,252 @@
+import { useCallback, useState } from 'react';
+
+import { useLingui } from '@lingui/react/macro';
+import { Trans } from '@lingui/react/macro';
+import type { Field, Recipient } from '@prisma/client';
+import { RecipientRole, SendStatus } from '@prisma/client';
+import { Check, ChevronsUpDown, Info } from 'lucide-react';
+import { sortBy } from 'remeda';
+
+import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
+import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
+import { getRecipientColorStyles } from '@documenso/ui/lib/recipient-colors';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+} from '@documenso/ui/primitives/command';
+import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
+
+export interface EnvelopeRecipientSelectorProps {
+ className?: string;
+ selectedRecipient: Recipient | null;
+ onSelectedRecipientChange: (recipient: Recipient) => void;
+ recipients: Recipient[];
+ fields: Field[];
+ align?: 'center' | 'end' | 'start';
+}
+
+export const EnvelopeRecipientSelector = ({
+ className,
+ selectedRecipient,
+ onSelectedRecipientChange,
+ recipients,
+ fields,
+ align = 'start',
+}: EnvelopeRecipientSelectorProps) => {
+ const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
+
+ return (
+
+
+
+
+
+
+ {
+ onSelectedRecipientChange(recipient);
+ setShowRecipientsSelector(false);
+ }}
+ recipients={recipients}
+ />
+
+
+ );
+};
+
+interface EnvelopeRecipientSelectorCommandProps {
+ className?: string;
+ selectedRecipient: Recipient | null;
+ onSelectedRecipientChange: (recipient: Recipient) => void;
+ recipients: Recipient[];
+ fields: Field[];
+ placeholder?: string;
+}
+
+export const EnvelopeRecipientSelectorCommand = ({
+ className,
+ selectedRecipient,
+ onSelectedRecipientChange,
+ recipients,
+ fields,
+ placeholder,
+}: EnvelopeRecipientSelectorCommandProps) => {
+ const { t } = useLingui();
+
+ const recipientsByRole = useCallback(() => {
+ const recipientsByRole: Record = {
+ CC: [],
+ VIEWER: [],
+ SIGNER: [],
+ APPROVER: [],
+ ASSISTANT: [],
+ };
+
+ recipients.forEach((recipient) => {
+ recipientsByRole[recipient.role].push(recipient);
+ });
+
+ return recipientsByRole;
+ }, [recipients]);
+
+ const recipientsByRoleToDisplay = useCallback(() => {
+ return Object.entries(recipientsByRole())
+ .filter(
+ ([role]) =>
+ role !== RecipientRole.CC &&
+ role !== RecipientRole.VIEWER &&
+ role !== RecipientRole.ASSISTANT,
+ )
+ .map(
+ ([role, roleRecipients]) =>
+ [
+ role,
+ sortBy(
+ roleRecipients,
+ [(r) => r.signingOrder || Number.MAX_SAFE_INTEGER, 'asc'],
+ [(r) => r.id, 'asc'],
+ ),
+ ] as [RecipientRole, Recipient[]],
+ );
+ }, [recipientsByRole]);
+
+ const isRecipientDisabled = useCallback(
+ (recipientId: number) => {
+ const recipient = recipients.find((r) => r.id === recipientId);
+ const recipientFields = fields.filter((f) => f.recipientId === recipientId);
+
+ return !recipient || !canRecipientFieldsBeModified(recipient, recipientFields);
+ },
+ [fields, recipients],
+ );
+
+ return (
+
+
+
+
+
+ No recipient matching this description was found.
+
+
+
+ {recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
+
+
+ {t(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
+
+
+ {roleRecipients.length === 0 && (
+
+ No recipients with this role
+
+ )}
+
+ {roleRecipients.map((recipient) => (
+ r.id === recipient.id),
+ 0,
+ ),
+ ).comboxBoxItem,
+ {
+ 'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
+ 'cursor-not-allowed': isRecipientDisabled(recipient.id),
+ },
+ )}
+ onSelect={() => {
+ if (!isRecipientDisabled(recipient.id)) {
+ onSelectedRecipientChange(recipient);
+ }
+ }}
+ >
+
+ {recipient.name && (
+
+ {recipient.name} ({recipient.email})
+
+ )}
+
+ {!recipient.name && {recipient.email}}
+
+
+
+ {!isRecipientDisabled(recipient.id) ? (
+
+ ) : (
+
+
+
+
+
+
+
+ This document has already been sent to this recipient. You can no longer
+ edit this recipient.
+
+
+
+ )}
+
+
+ ))}
+
+ ))}
+
+ );
+};
diff --git a/packages/lib/client-only/hooks/use-editor-fields.ts b/packages/lib/client-only/hooks/use-editor-fields.ts
index 921699f2b..6b0b37776 100644
--- a/packages/lib/client-only/hooks/use-editor-fields.ts
+++ b/packages/lib/client-only/hooks/use-editor-fields.ts
@@ -170,10 +170,15 @@ export const useEditorFields = ({
);
const setFieldId = (formId: string, id: number) => {
- const index = localFields.findIndex((field) => field.formId === formId);
+ const { fields } = form.getValues();
+
+ const index = fields.findIndex((field) => field.formId === formId);
if (index !== -1) {
- form.setValue(`fields.${index}.id`, id);
+ update(index, {
+ ...fields[index],
+ id,
+ });
}
};