fix: envelope autosave (#2103)

This commit is contained in:
David Nguyen
2025-10-27 19:53:35 +11:00
committed by GitHub
parent 35250fa308
commit b0b07106b4
9 changed files with 107 additions and 33 deletions

View File

@ -1,6 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/react/macro';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import type { z } from 'zod'; import type { z } from 'zod';
@ -60,7 +61,12 @@ export const EditorFieldSignatureForm = ({
<Form {...form}> <Form {...form}>
<form> <form>
<fieldset className="flex flex-col gap-2"> <fieldset className="flex flex-col gap-2">
<EditorGenericFontSizeField formControl={form.control} /> <div>
<EditorGenericFontSizeField formControl={form.control} />
<p className="text-muted-foreground mt-0.5 text-xs">
<Trans>The typed signature font size</Trans>
</p>
</div>
</fieldset> </fieldset>
</form> </form>
</Form> </Form>

View File

@ -57,8 +57,6 @@ export default function EnvelopeEditorFieldsPageRenderer() {
); );
const handleResizeOrMove = (event: KonvaEventObject<Event>) => { const handleResizeOrMove = (event: KonvaEventObject<Event>) => {
console.log('Field resized or moved');
const { current: container } = canvasElement; const { current: container } = canvasElement;
if (!container) { if (!container) {
@ -273,9 +271,6 @@ export default function EnvelopeEditorFieldsPageRenderer() {
return; return;
} }
console.log(`pointerPosition.x: ${pointerPosition.x}`);
console.log(`pointerPosition.y: ${pointerPosition.y}`);
x1 = pointerPosition.x / scale; x1 = pointerPosition.x / scale;
y1 = pointerPosition.y / scale; y1 = pointerPosition.y / scale;
x2 = pointerPosition.x / scale; x2 = pointerPosition.x / scale;

View File

@ -21,6 +21,7 @@ import type {
TNameFieldMeta, TNameFieldMeta,
TNumberFieldMeta, TNumberFieldMeta,
TRadioFieldMeta, TRadioFieldMeta,
TSignatureFieldMeta,
TTextFieldMeta, TTextFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients'; import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
@ -38,6 +39,7 @@ import { EditorFieldInitialsForm } from '~/components/forms/editor/editor-field-
import { EditorFieldNameForm } from '~/components/forms/editor/editor-field-name-form'; import { EditorFieldNameForm } from '~/components/forms/editor/editor-field-name-form';
import { EditorFieldNumberForm } from '~/components/forms/editor/editor-field-number-form'; import { EditorFieldNumberForm } from '~/components/forms/editor/editor-field-number-form';
import { EditorFieldRadioForm } from '~/components/forms/editor/editor-field-radio-form'; import { EditorFieldRadioForm } from '~/components/forms/editor/editor-field-radio-form';
import { EditorFieldSignatureForm } from '~/components/forms/editor/editor-field-signature-form';
import { EditorFieldTextForm } from '~/components/forms/editor/editor-field-text-form'; import { EditorFieldTextForm } from '~/components/forms/editor/editor-field-text-form';
import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop'; import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop';
@ -189,7 +191,7 @@ export const EnvelopeEditorFieldsPage = () => {
{/* Field details section. */} {/* Field details section. */}
<AnimateGenericFadeInOut key={editorFields.selectedField?.formId}> <AnimateGenericFadeInOut key={editorFields.selectedField?.formId}>
{selectedField && selectedField.type !== FieldType.SIGNATURE && ( {selectedField && (
<section> <section>
<Separator className="my-4" /> <Separator className="my-4" />
@ -199,6 +201,12 @@ export const EnvelopeEditorFieldsPage = () => {
</h3> </h3>
{match(selectedField.type) {match(selectedField.type)
.with(FieldType.SIGNATURE, () => (
<EditorFieldSignatureForm
value={selectedField?.fieldMeta as TSignatureFieldMeta | undefined}
onValueChange={(value) => updateSelectedFieldMeta(value)}
/>
))
.with(FieldType.CHECKBOX, () => ( .with(FieldType.CHECKBOX, () => (
<EditorFieldCheckboxForm <EditorFieldCheckboxForm
value={selectedField?.fieldMeta as TCheckboxFieldMeta | undefined} value={selectedField?.fieldMeta as TCheckboxFieldMeta | undefined}

View File

@ -50,6 +50,7 @@ type UseEditorFieldsResponse = {
// Field operations // Field operations
addField: (field: Omit<TLocalField, 'formId'>) => TLocalField; addField: (field: Omit<TLocalField, 'formId'>) => TLocalField;
setFieldId: (formId: string, id: number) => void;
removeFieldsByFormId: (formIds: string[]) => void; removeFieldsByFormId: (formIds: string[]) => void;
updateFieldByFormId: (formId: string, updates: Partial<TLocalField>) => void; updateFieldByFormId: (formId: string, updates: Partial<TLocalField>) => void;
duplicateField: (field: TLocalField, recipientId?: number) => TLocalField; duplicateField: (field: TLocalField, recipientId?: number) => TLocalField;
@ -160,6 +161,17 @@ export const useEditorFields = ({
[localFields, remove, triggerFieldsUpdate], [localFields, remove, triggerFieldsUpdate],
); );
const setFieldId = (formId: string, id: number) => {
const index = localFields.findIndex((field) => field.formId === formId);
if (index !== -1) {
update(index, {
...localFields[index],
id,
});
}
};
const updateFieldByFormId = useCallback( const updateFieldByFormId = useCallback(
(formId: string, updates: Partial<TLocalField>) => { (formId: string, updates: Partial<TLocalField>) => {
const index = localFields.findIndex((field) => field.formId === formId); const index = localFields.findIndex((field) => field.formId === formId);
@ -269,6 +281,7 @@ export const useEditorFields = ({
// Field operations // Field operations
addField, addField,
setFieldId,
removeFieldsByFormId, removeFieldsByFormId,
updateFieldByFormId, updateFieldByFormId,
duplicateField, duplicateField,

View File

@ -97,6 +97,11 @@ export const EnvelopeEditorProvider = ({
const [envelope, setEnvelope] = useState(initialEnvelope); const [envelope, setEnvelope] = useState(initialEnvelope);
const [autosaveError, setAutosaveError] = useState<boolean>(false); const [autosaveError, setAutosaveError] = useState<boolean>(false);
const editorFields = useEditorFields({
envelope,
handleFieldsUpdate: (fields) => setFieldsDebounced(fields),
});
const envelopeUpdateMutationQuery = trpc.envelope.update.useMutation({ const envelopeUpdateMutationQuery = trpc.envelope.update.useMutation({
onSuccess: (response, input) => { onSuccess: (response, input) => {
setEnvelope({ setEnvelope({
@ -184,13 +189,24 @@ export const EnvelopeEditorProvider = ({
triggerSave: setFieldsDebounced, triggerSave: setFieldsDebounced,
flush: setFieldsAsync, flush: setFieldsAsync,
isPending: isFieldsMutationPending, isPending: isFieldsMutationPending,
} = useEnvelopeAutosave(async (fields: TLocalField[]) => { } = useEnvelopeAutosave(async (localFields: TLocalField[]) => {
await envelopeFieldSetMutationQuery.mutateAsync({ const envelopeFields = await envelopeFieldSetMutationQuery.mutateAsync({
envelopeId: envelope.id, envelopeId: envelope.id,
envelopeType: envelope.type, envelopeType: envelope.type,
fields, fields: localFields,
}); });
}, 1000);
// Insert the IDs into the local fields.
envelopeFields.fields.forEach((field) => {
const localField = localFields.find((localField) => localField.formId === field.formId);
if (localField && !localField.id) {
localField.id = field.id;
editorFields.setFieldId(localField.formId, field.id);
}
});
}, 2000);
const { const {
triggerSave: setEnvelopeDebounced, triggerSave: setEnvelopeDebounced,
@ -221,11 +237,6 @@ export const EnvelopeEditorProvider = ({
setEnvelopeDebounced(envelopeUpdates); setEnvelopeDebounced(envelopeUpdates);
}; };
const editorFields = useEditorFields({
envelope,
handleFieldsUpdate: (fields) => setFieldsDebounced(fields),
});
const getRecipientColorKey = useCallback( const getRecipientColorKey = useCallback(
(recipientId: number) => { (recipientId: number) => {
const recipientIndex = envelope.recipients.findIndex( const recipientIndex = envelope.recipients.findIndex(

View File

@ -306,7 +306,10 @@ export const setFieldsForDocument = async ({
}); });
} }
return upsertedField; return {
...upsertedField,
formId: field.formId,
};
}), }),
); );
}); });
@ -340,17 +343,25 @@ export const setFieldsForDocument = async ({
} }
// Filter out fields that have been removed or have been updated. // Filter out fields that have been removed or have been updated.
const filteredFields = existingFields.filter((field) => { const mappedFilteredFields = existingFields
const isRemoved = removedFields.find((removedField) => removedField.id === field.id); .filter((field) => {
const isUpdated = persistedFields.find((persistedField) => persistedField.id === field.id); const isRemoved = removedFields.find((removedField) => removedField.id === field.id);
const isUpdated = persistedFields.find((persistedField) => persistedField.id === field.id);
return !isRemoved && !isUpdated; return !isRemoved && !isUpdated;
}); })
.map((field) => ({
...mapFieldToLegacyField(field, envelope),
formId: undefined,
}));
const mappedPersistentFields = persistedFields.map((field) => ({
...mapFieldToLegacyField(field, envelope),
formId: field?.formId,
}));
return { return {
fields: [...filteredFields, ...persistedFields].map((field) => fields: [...mappedFilteredFields, ...mappedPersistentFields],
mapFieldToLegacyField(field, envelope),
),
}; };
}; };
@ -359,6 +370,7 @@ export const setFieldsForDocument = async ({
*/ */
type FieldData = { type FieldData = {
id?: number | null; id?: number | null;
formId?: string;
envelopeItemId: string; envelopeItemId: string;
type: FieldType; type: FieldType;
recipientId: number; recipientId: number;

View File

@ -27,6 +27,7 @@ export type SetFieldsForTemplateOptions = {
id: EnvelopeIdOptions; id: EnvelopeIdOptions;
fields: { fields: {
id?: number | null; id?: number | null;
formId?: string;
envelopeItemId: string; envelopeItemId: string;
type: FieldType; type: FieldType;
recipientId: number; recipientId: number;
@ -111,10 +112,10 @@ export const setFieldsForTemplate = async ({
}; };
}); });
const persistedFields = await prisma.$transaction( const persistedFields = await Promise.all(
// Disabling as wrapping promises here causes type issues // Disabling as wrapping promises here causes type issues
// eslint-disable-next-line @typescript-eslint/promise-function-async // eslint-disable-next-line @typescript-eslint/promise-function-async
linkedFields.map((field) => { linkedFields.map(async (field) => {
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined; const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
if (field.type === FieldType.TEXT && field.fieldMeta) { if (field.type === FieldType.TEXT && field.fieldMeta) {
@ -176,7 +177,7 @@ export const setFieldsForTemplate = async ({
} }
// Proceed with upsert operation // Proceed with upsert operation
return prisma.field.upsert({ const upsertedField = await prisma.field.upsert({
where: { where: {
id: field._persisted?.id ?? -1, id: field._persisted?.id ?? -1,
envelopeId: envelope.id, envelopeId: envelope.id,
@ -219,6 +220,11 @@ export const setFieldsForTemplate = async ({
}, },
}, },
}); });
return {
...upsertedField,
formId: field.formId,
};
}), }),
); );
@ -240,9 +246,17 @@ export const setFieldsForTemplate = async ({
return !isRemoved && !isUpdated; return !isRemoved && !isUpdated;
}); });
const mappedFilteredFields = filteredFields.map((field) => ({
...mapFieldToLegacyField(field, envelope),
formId: undefined,
}));
const mappedPersistentFields = persistedFields.map((field) => ({
...mapFieldToLegacyField(field, envelope),
formId: field?.formId,
}));
return { return {
fields: [...filteredFields, ...persistedFields].map((field) => fields: [...mappedFilteredFields, ...mappedPersistentFields],
mapFieldToLegacyField(field, envelope),
),
}; };
}; };

View File

@ -24,7 +24,7 @@ export const setEnvelopeFieldsRoute = authenticatedProcedure
}, },
}); });
await match(envelopeType) const result = await match(envelopeType)
.with(EnvelopeType.DOCUMENT, async () => .with(EnvelopeType.DOCUMENT, async () =>
setFieldsForDocument({ setFieldsForDocument({
userId: ctx.user.id, userId: ctx.user.id,
@ -63,4 +63,11 @@ export const setEnvelopeFieldsRoute = authenticatedProcedure
}), }),
) )
.exhaustive(); .exhaustive();
return {
fields: result.fields.map((field) => ({
id: field.id,
formId: field.formId,
})),
};
}); });

View File

@ -12,6 +12,7 @@ export const ZSetEnvelopeFieldsRequestSchema = z.object({
.number() .number()
.optional() .optional()
.describe('The id of the field. If not provided, a new field will be created.'), .describe('The id of the field. If not provided, a new field will be created.'),
formId: z.string().optional().describe('A temporary ID to keep track of new fields created'),
envelopeItemId: z.string().describe('The id of the envelope item to put the field on'), envelopeItemId: z.string().describe('The id of the envelope item to put the field on'),
recipientId: z.number(), recipientId: z.number(),
type: z.nativeEnum(FieldType), type: z.nativeEnum(FieldType),
@ -45,7 +46,14 @@ export const ZSetEnvelopeFieldsRequestSchema = z.object({
), ),
}); });
export const ZSetEnvelopeFieldsResponseSchema = z.void(); export const ZSetEnvelopeFieldsResponseSchema = z.object({
fields: z
.object({
id: z.number(),
formId: z.string().optional(),
})
.array(),
});
export type TSetEnvelopeFieldsRequest = z.infer<typeof ZSetEnvelopeFieldsRequestSchema>; export type TSetEnvelopeFieldsRequest = z.infer<typeof ZSetEnvelopeFieldsRequestSchema>;
export type TSetEnvelopeFieldsResponse = z.infer<typeof ZSetEnvelopeFieldsResponseSchema>; export type TSetEnvelopeFieldsResponse = z.infer<typeof ZSetEnvelopeFieldsResponseSchema>;