import type { TEditorRecipientsFormSchema } from '@documenso/lib/client-only/hooks/use-editor-recipients'; import type { RecipientStep } from '@documenso/lib/utils/recipient-groups'; import { cn } from '@documenso/ui/lib/utils'; import { Badge } from '@documenso/ui/primitives/badge'; import { Button } from '@documenso/ui/primitives/button'; import type { DraggableProvided, DraggableStateSnapshot } from '@hello-pangea/dnd'; import { Draggable, Droppable } from '@hello-pangea/dnd'; import { Trans } from '@lingui/react/macro'; import { GripVerticalIcon, Users2Icon } from 'lucide-react'; import { RecipientRow, type RecipientRowProps } from './recipient-row'; type TEditorSigner = TEditorRecipientsFormSchema['signers'][number]; export type DraggingType = 'STEP' | 'RECIPIENT' | null; /** * Skips the drop animation. The post-drop state update re-sorts and renumbers * the groups anyway, so gliding to the predicted slot first makes every drop * feel like it settles twice โ€” snapping hands control to the real re-render * immediately instead. */ const getDraggableStyle = (provided: DraggableProvided, snapshot: DraggableStateSnapshot) => { if (!snapshot.isDropAnimating) { return provided.draggableProps.style; } return { ...provided.draggableProps.style, transitionDuration: '0.001s', }; }; export type RecipientStepCardSharedRowProps = Pick< RecipientRowProps, | 'showAdvancedSettings' | 'recipientSuggestions' | 'isLoadingSuggestions' | 'onRoleChange' | 'onRemove' | 'onAutoCompleteSelect' | 'onSearchQueryChange' >; export type RecipientStepCardProps = { stepIndex: number; step: RecipientStep; isLastStep: boolean; draggableProvided: DraggableProvided; draggableSnapshot: DraggableStateSnapshot; draggingType: DraggingType; isStepLocked: boolean; isRemoveDisabled: boolean; flatIndexByFormId: Map; canSignerBeModified: (signer: TEditorSigner) => boolean; isSubmitting: boolean; onUngroup: (stepIndex: number) => void; rowProps: RecipientStepCardSharedRowProps; }; /** * The drop-zone strip rendered above each group card (and below the last one) * that receives recipient-row drops. Invisible until a dragged row hovers it, * then it shows a full-width green line marking the insertion point. * * Notes: * - It lives INSIDE the step's Draggable so it shifts together with the card * while groups are being reordered โ€” a static strip between draggables * would stay behind while the cards around it are displaced, making group * drags look broken. * - Its `droppableId` must stay STABLE while mounted (anchored to a formId, * never a positional index): @hello-pangea/dnd does not support changing * ids on mounted droppables/draggables, which silently breaks them. * - `type="RECIPIENT"` already scopes it to recipient-row drags, and * `isDropDisabled` must not be toggled based on the active drag, as * @hello-pangea/dnd snapshots it at drag start (before state updates land). * - It must keep a CONSTANT size: droppable geometry is captured when a drag * starts, so resizing during the drag would leave the visible strip and the * actual hit area in different places. Only colors may change mid-drag. */ const RecipientStepGap = ({ droppableId }: { droppableId: string }) => ( {(provided, snapshot) => (
{provided.placeholder}
)} ); export const RecipientStepCard = ({ stepIndex, step, isLastStep, draggableProvided, draggableSnapshot, draggingType, isStepLocked, isRemoveDisabled, flatIndexByFormId, canSignerBeModified, isSubmitting, onUngroup, rowProps, }: RecipientStepCardProps) => { const isGroup = step.members.length > 1; const isCombineTarget = draggingType === 'STEP' && Boolean(draggableSnapshot.combineTargetFor); // All droppable ids are anchored to the first member's formId (never a // positional index) so they stay stable while cards are reordered โ€” // @hello-pangea/dnd does not support changing ids on mounted elements. const stepAnchor = step.members[0].formId; return (
{(droppableProvided, droppableSnapshot) => { const isJoinTarget = draggingType === 'RECIPIENT' && droppableSnapshot.isDraggingOver; const isHighlighted = isCombineTarget || isJoinTarget; return (
{isHighlighted && ( Release to group )}
Group {step.order} {isGroup && ( <> {step.members.length} recipients ยท any order )}
{step.members.map((member, memberIndex) => { const signerIndex = flatIndexByFormId.get(member.formId) ?? -1; const canBeModified = canSignerBeModified(member); return ( {(memberProvided, memberSnapshot) => (
)}
); })} {droppableProvided.placeholder}
); }}
{isLastStep && }
); };