diff --git a/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx b/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx index 4e267b0e5..b644045d6 100644 --- a/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -54,6 +54,8 @@ type TemplateDirectLinkDialogProps = { directLink?: Pick | null; recipients: Recipient[]; trigger?: React.ReactNode; + onCreateSuccess?: () => Promise | void; + onDeleteSuccess?: () => Promise | void; }; type TemplateDirectLinkStep = 'ONBOARD' | 'SELECT_RECIPIENT' | 'MANAGE' | 'CONFIRM_DELETE'; @@ -63,6 +65,8 @@ export const TemplateDirectLinkDialog = ({ directLink, recipients, trigger, + onCreateSuccess, + onDeleteSuccess, }: TemplateDirectLinkDialogProps) => { const { toast } = useToast(); const { quota, remaining } = useLimits(); @@ -97,6 +101,7 @@ export const TemplateDirectLinkDialog = ({ } = trpcReact.template.createTemplateDirectLink.useMutation({ onSuccess: async (data) => { await revalidate(); + await onCreateSuccess?.(); setToken(data.token); setIsEnabled(data.enabled); @@ -142,6 +147,7 @@ export const TemplateDirectLinkDialog = ({ trpcReact.template.deleteTemplateDirectLink.useMutation({ onSuccess: async () => { await revalidate(); + await onDeleteSuccess?.(); setOpen(false); setToken(null); @@ -234,7 +240,7 @@ export const TemplateDirectLinkDialog = ({

{_(step.title)}

-

{_(step.description)}

+

{_(step.description)}

))} @@ -320,13 +326,13 @@ export const TemplateDirectLinkDialog = ({ onClick={async () => onRecipientTableRowClick(row.id)} > -
+

{row.name}

-

{row.email}

+

{row.email}

- + {_(RECIPIENT_ROLES_DESCRIPTION[row.role].roleName)} @@ -350,7 +356,7 @@ export const TemplateDirectLinkDialog = ({
{validDirectTemplateRecipients.length !== 0 && ( -

+

Or

)} @@ -392,7 +398,7 @@ export const TemplateDirectLinkDialog = ({ - + Disabling direct link signing will prevent anyone from accessing the link. diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx index 7b17e9d97..ab83b3189 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DragDropContext, @@ -7,28 +7,23 @@ import { Droppable, type SensorAPI, } from '@hello-pangea/dnd'; -import { zodResolver } from '@hookform/resolvers/zod'; import { plural } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client'; import { motion } from 'framer-motion'; import { GripVerticalIcon, HelpCircleIcon, PlusIcon, SparklesIcon, TrashIcon } from 'lucide-react'; -import { useFieldArray, useForm, useWatch } from 'react-hook-form'; +import { useFieldArray, useWatch } from 'react-hook-form'; import { useRevalidator, useSearchParams } from 'react-router'; -import { isDeepEqual, prop, sortBy } from 'remeda'; -import { z } from 'zod'; +import { isDeepEqual } from 'remeda'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; +import { ZEditorRecipientsFormSchema } from '@documenso/lib/client-only/hooks/use-editor-recipients'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useSession } from '@documenso/lib/client-only/providers/session'; import type { TDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema'; -import { - ZRecipientActionAuthTypesSchema, - ZRecipientAuthOptionsSchema, -} from '@documenso/lib/types/document-auth'; -import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient'; +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 { trpc } from '@documenso/trpc/react'; @@ -67,26 +62,9 @@ import { AiFeaturesEnableDialog } from '~/components/dialogs/ai-features-enable- import { AiRecipientDetectionDialog } from '~/components/dialogs/ai-recipient-detection-dialog'; import { useCurrentTeam } from '~/providers/team'; -const ZEnvelopeRecipientsForm = z.object({ - signers: z.array( - z.object({ - formId: z.string().min(1), - id: z.number().optional(), - email: ZRecipientEmailSchema, - name: z.string(), - role: z.nativeEnum(RecipientRole), - signingOrder: z.number().optional(), - actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]), - }), - ), - signingOrder: z.nativeEnum(DocumentSigningOrder), - allowDictateNextSigner: z.boolean().default(false), -}); - -type TEnvelopeRecipientsForm = z.infer; - export const EnvelopeEditorRecipientForm = () => { - const { envelope, setRecipientsDebounced, updateEnvelope } = useCurrentEnvelopeEditor(); + const { envelope, setRecipientsDebounced, updateEnvelope, editorRecipients } = + useCurrentEnvelopeEditor(); const organisation = useCurrentOrganisation(); const team = useCurrentTeam(); @@ -145,7 +123,6 @@ export const EnvelopeEditorRecipientForm = () => { const debouncedRecipientSearchQuery = useDebouncedValue(recipientSearchQuery, 500); - const initialId = useId(); const $sensorApi = useRef(null); const isFirstRender = useRef(true); const { recipients, fields } = envelope; @@ -161,42 +138,7 @@ export const EnvelopeEditorRecipientForm = () => { const recipientSuggestions = recipientSuggestionsData?.results || []; - const defaultRecipients = [ - { - formId: initialId, - name: '', - email: '', - role: RecipientRole.SIGNER, - signingOrder: 1, - actionAuth: [], - }, - ]; - - const form = useForm({ - resolver: zodResolver(ZEnvelopeRecipientsForm), - mode: 'onChange', // Used for autosave purposes, maybe can try onBlur instead? - defaultValues: { - signers: - recipients.length > 0 - ? sortBy( - recipients.map((recipient, index) => ({ - id: recipient.id, - formId: String(recipient.id), - name: recipient.name, - email: recipient.email, - role: recipient.role, - signingOrder: recipient.signingOrder ?? index + 1, - actionAuth: - ZRecipientAuthOptionsSchema.parse(recipient.authOptions)?.actionAuth ?? undefined, - })), - [prop('signingOrder'), 'asc'], - [prop('id'), 'asc'], - ) - : defaultRecipients, - signingOrder: envelope.documentMeta.signingOrder, - allowDictateNextSigner: envelope.documentMeta.allowDictateNextSigner, - }, - }); + const { form } = editorRecipients; const recipientHasAuthSettings = useMemo(() => { const recipientHasAuthOptions = recipients.find((recipient) => { @@ -588,7 +530,7 @@ export const EnvelopeEditorRecipientForm = () => { return; } - const validatedFormValues = ZEnvelopeRecipientsForm.safeParse(formValues); + const validatedFormValues = ZEditorRecipientsFormSchema.safeParse(formValues); if (!validatedFormValues.success) { return; @@ -848,246 +790,205 @@ export const EnvelopeEditorRecipientForm = () => { ref={provided.innerRef} className="flex w-full flex-col gap-y-2" > - {signers.map((signer, index) => ( - - {(provided, snapshot) => ( -
- { + const isDirectRecipient = + envelope.type === EnvelopeType.TEMPLATE && + envelope.directLink !== null && + signer.id === envelope.directLink.directTemplateRecipientId; + + return ( + + {(provided, snapshot) => ( +
-
- {isSigningOrderSequential && ( + +
+ {isSigningOrderSequential && ( + ( + + + + { + field.onChange(e); + handleSigningOrderChange(index, e.target.value); + }} + onBlur={(e) => { + field.onBlur(); + handleSigningOrderChange(index, e.target.value); + }} + disabled={ + snapshot.isDragging || + isSubmitting || + !canRecipientBeModified(signer.id) + } + /> + + + + )} + /> + )} + ( - + {!showAdvancedSettings && index === 0 && ( + + Email + + )} + - { - field.onChange(e); - handleSigningOrderChange(index, e.target.value); - }} - onBlur={(e) => { - field.onBlur(); - handleSigningOrderChange(index, e.target.value); - }} + + handleRecipientAutoCompleteSelect(index, suggestion) + } + onSearchQueryChange={(query) => { + field.onChange(query); + setRecipientSearchQuery(query); + }} + loading={isLoading} + data-testid="signer-email-input" + maxLength={254} /> + )} /> - )} - ( - - {!showAdvancedSettings && index === 0 && ( - - Email - - )} - - - - handleRecipientAutoCompleteSelect(index, suggestion) - } - onSearchQueryChange={(query) => { - field.onChange(query); - setRecipientSearchQuery(query); - }} - loading={isLoading} - data-testid="signer-email-input" - maxLength={254} - /> - - - - - )} - /> - - ( - - {!showAdvancedSettings && index === 0 && ( - - Name - - )} - - - - handleRecipientAutoCompleteSelect(index, suggestion) - } - onSearchQueryChange={(query) => { - field.onChange(query); - setRecipientSearchQuery(query); - }} - loading={isLoading} - maxLength={255} - /> - - - - - )} - /> - - ( - - - { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - handleRoleChange(index, value as RecipientRole); - field.onChange(value); - }} - disabled={ - snapshot.isDragging || - isSubmitting || - !canRecipientBeModified(signer.id) - } - /> - - - - - )} - /> - - -
- - {showAdvancedSettings && - organisation.organisationClaim.flags.cfr21 && ( ( + {!showAdvancedSettings && index === 0 && ( + + Name + + )} + + + + handleRecipientAutoCompleteSelect(index, suggestion) + } + onSearchQueryChange={(query) => { + field.onChange(query); + setRecipientSearchQuery(query); + }} + loading={isLoading} + maxLength={255} + /> + + + + + )} + /> + + ( + - { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + handleRoleChange(index, value as RecipientRole); + field.onChange(value); + }} disabled={ snapshot.isDragging || isSubmitting || @@ -1100,12 +1001,63 @@ export const EnvelopeEditorRecipientForm = () => { )} /> - )} -
-
- )} - - ))} + + +
+ + {showAdvancedSettings && + organisation.organisationClaim.flags.cfr21 && ( + ( + + + + + + + + )} + /> + )} +
+
+ )} +
+ ); + })} {provided.placeholder}
diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor.tsx index af5eecdd6..cf4da6970 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor.tsx @@ -81,7 +81,7 @@ export default function EnvelopeEditor() { isAutosaving, flushAutosave, relativePath, - editorFields, + syncEnvelope, } = useCurrentEnvelopeEditor(); const [searchParams, setSearchParams] = useSearchParams(); @@ -278,6 +278,8 @@ export default function EnvelopeEditor() { templateId={mapSecondaryIdToTemplateId(envelope.secondaryId)} directLink={envelope.directLink} recipients={envelope.recipients} + onCreateSuccess={async () => await syncEnvelope()} + onDeleteSuccess={async () => await syncEnvelope()} trigger={