mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
This PR is handles the changes required to support envelopes. The new envelope editor/signing page will be hidden during release. The core changes here is to migrate the documents and templates model to a centralized envelopes model. Even though Documents and Templates are removed, from the user perspective they will still exist as we remap envelopes to documents and templates.
282 lines
7.9 KiB
TypeScript
282 lines
7.9 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import type { Recipient } from '@prisma/client';
|
|
import { FieldType } from '@prisma/client';
|
|
import { useFieldArray, useForm } from 'react-hook-form';
|
|
import { z } from 'zod';
|
|
|
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
import { nanoid } from '@documenso/lib/universal/id';
|
|
|
|
import type { TEnvelope } from '../../types/envelope';
|
|
|
|
export const ZLocalFieldSchema = z.object({
|
|
// This is the actual ID of the field if created.
|
|
id: z.number().optional(),
|
|
// This is the local client side ID of the field.
|
|
formId: z.string().min(1),
|
|
// This is the ID of the envelope item to put the field on.
|
|
envelopeItemId: z.string(),
|
|
type: z.nativeEnum(FieldType),
|
|
recipientId: z.number(),
|
|
page: z.number().min(1),
|
|
positionX: z.number().min(0),
|
|
positionY: z.number().min(0),
|
|
width: z.number().min(0),
|
|
height: z.number().min(0),
|
|
fieldMeta: ZFieldMetaSchema,
|
|
});
|
|
|
|
export type TLocalField = z.infer<typeof ZLocalFieldSchema>;
|
|
|
|
const ZEditorFieldsFormSchema = z.object({
|
|
fields: z.array(ZLocalFieldSchema),
|
|
});
|
|
|
|
export type TEditorFieldsFormSchema = z.infer<typeof ZEditorFieldsFormSchema>;
|
|
|
|
type EditorFieldsProps = {
|
|
envelope: TEnvelope;
|
|
handleFieldsUpdate: (fields: TLocalField[]) => unknown;
|
|
};
|
|
|
|
type UseEditorFieldsResponse = {
|
|
localFields: TLocalField[];
|
|
|
|
// Selected field
|
|
selectedField: TLocalField | undefined;
|
|
setSelectedField: (formId: string | null) => void;
|
|
|
|
// Field operations
|
|
addField: (field: Omit<TLocalField, 'formId'>) => TLocalField;
|
|
removeFieldsByFormId: (formIds: string[]) => void;
|
|
updateFieldByFormId: (formId: string, updates: Partial<TLocalField>) => void;
|
|
duplicateField: (field: TLocalField, recipientId?: number) => TLocalField;
|
|
duplicateFieldToAllPages: (field: TLocalField, recipientId?: number) => TLocalField[];
|
|
|
|
// Field utilities
|
|
getFieldByFormId: (formId: string) => TLocalField | undefined;
|
|
getFieldsByRecipient: (recipientId: number) => TLocalField[];
|
|
|
|
// Selected recipient
|
|
selectedRecipient: Recipient | null;
|
|
setSelectedRecipient: (recipientId: number | null) => void;
|
|
};
|
|
|
|
export const useEditorFields = ({
|
|
envelope,
|
|
handleFieldsUpdate,
|
|
}: EditorFieldsProps): UseEditorFieldsResponse => {
|
|
const [selectedFieldFormId, setSelectedFieldFormId] = useState<string | null>(null);
|
|
const [selectedRecipientId, setSelectedRecipientId] = useState<number | null>(null);
|
|
|
|
const form = useForm<TEditorFieldsFormSchema>({
|
|
defaultValues: {
|
|
fields: envelope.fields.map(
|
|
(field): TLocalField => ({
|
|
id: field.id,
|
|
formId: nanoid(),
|
|
envelopeItemId: field.envelopeItemId,
|
|
page: field.page,
|
|
type: field.type,
|
|
positionX: Number(field.positionX),
|
|
positionY: Number(field.positionY),
|
|
width: Number(field.width),
|
|
height: Number(field.height),
|
|
recipientId: field.recipientId,
|
|
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
|
}),
|
|
),
|
|
},
|
|
resolver: zodResolver(ZEditorFieldsFormSchema),
|
|
});
|
|
|
|
const {
|
|
append,
|
|
remove,
|
|
update,
|
|
fields: localFields,
|
|
} = useFieldArray({
|
|
control: form.control,
|
|
name: 'fields',
|
|
keyName: 'react-hook-form-id',
|
|
});
|
|
|
|
const triggerFieldsUpdate = () => {
|
|
void handleFieldsUpdate(form.getValues().fields);
|
|
};
|
|
|
|
const setSelectedField = (formId: string | null, bypassCheck = false) => {
|
|
if (!formId) {
|
|
setSelectedFieldFormId(null);
|
|
return;
|
|
}
|
|
|
|
const foundField = localFields.find((field) => field.formId === formId);
|
|
const recipient = envelope.recipients.find(
|
|
(recipient) => recipient.id === foundField?.recipientId,
|
|
);
|
|
|
|
if (recipient) {
|
|
setSelectedRecipient(recipient.id);
|
|
}
|
|
|
|
if (bypassCheck) {
|
|
console.log(3);
|
|
setSelectedFieldFormId(formId);
|
|
return;
|
|
}
|
|
|
|
setSelectedFieldFormId(foundField?.formId ?? null);
|
|
};
|
|
|
|
const addField = useCallback(
|
|
(fieldData: Omit<TLocalField, 'formId'>): TLocalField => {
|
|
const field: TLocalField = {
|
|
...fieldData,
|
|
formId: nanoid(12),
|
|
};
|
|
|
|
append(field);
|
|
triggerFieldsUpdate();
|
|
setSelectedField(field.formId, true);
|
|
return field;
|
|
},
|
|
[append, triggerFieldsUpdate, setSelectedField],
|
|
);
|
|
|
|
const removeFieldsByFormId = useCallback(
|
|
(formIds: string[]) => {
|
|
const indexes = formIds
|
|
.map((formId) => localFields.findIndex((field) => field.formId === formId))
|
|
.filter((index) => index !== -1);
|
|
|
|
if (indexes.length > 0) {
|
|
remove(indexes);
|
|
triggerFieldsUpdate();
|
|
}
|
|
},
|
|
[localFields, remove, triggerFieldsUpdate],
|
|
);
|
|
|
|
const updateFieldByFormId = useCallback(
|
|
(formId: string, updates: Partial<TLocalField>) => {
|
|
const index = localFields.findIndex((field) => field.formId === formId);
|
|
|
|
if (index !== -1) {
|
|
update(index, { ...localFields[index], ...updates });
|
|
triggerFieldsUpdate();
|
|
}
|
|
},
|
|
[localFields, update, triggerFieldsUpdate],
|
|
);
|
|
|
|
const duplicateField = useCallback(
|
|
(field: TLocalField): TLocalField => {
|
|
const newField: TLocalField = {
|
|
...structuredClone(field),
|
|
id: undefined,
|
|
formId: nanoid(12),
|
|
recipientId: field.recipientId,
|
|
positionX: field.positionX + 3,
|
|
positionY: field.positionY + 3,
|
|
};
|
|
|
|
append(newField);
|
|
triggerFieldsUpdate();
|
|
return newField;
|
|
},
|
|
[append, triggerFieldsUpdate],
|
|
);
|
|
|
|
const duplicateFieldToAllPages = useCallback(
|
|
(field: TLocalField): TLocalField[] => {
|
|
const pages = Array.from(document.querySelectorAll('[data-page-number]'));
|
|
const newFields: TLocalField[] = [];
|
|
|
|
pages.forEach((_, index) => {
|
|
const pageNumber = index + 1;
|
|
|
|
if (pageNumber === field.page) {
|
|
return;
|
|
}
|
|
|
|
const newField: TLocalField = {
|
|
...structuredClone(field),
|
|
id: undefined,
|
|
formId: nanoid(12),
|
|
page: pageNumber,
|
|
};
|
|
|
|
append(newField);
|
|
newFields.push(newField);
|
|
});
|
|
|
|
triggerFieldsUpdate();
|
|
return newFields;
|
|
},
|
|
[append, triggerFieldsUpdate],
|
|
);
|
|
|
|
const getFieldByFormId = useCallback(
|
|
(formId: string): TLocalField | undefined => {
|
|
return localFields.find((field) => field.formId === formId) as TLocalField | undefined;
|
|
},
|
|
[localFields],
|
|
);
|
|
|
|
const getFieldsByRecipient = useCallback(
|
|
(recipientId: number): TLocalField[] => {
|
|
return localFields.filter((field) => field.recipientId === recipientId);
|
|
},
|
|
[localFields],
|
|
);
|
|
|
|
const selectedRecipient = useMemo(() => {
|
|
return envelope.recipients.find((recipient) => recipient.id === selectedRecipientId) || null;
|
|
}, [selectedRecipientId, envelope.recipients]);
|
|
|
|
const selectedField = useMemo(() => {
|
|
return localFields.find((field) => field.formId === selectedFieldFormId);
|
|
}, [selectedFieldFormId, localFields]);
|
|
|
|
/**
|
|
* Keep the selected field form ID in sync with the local fields.
|
|
*/
|
|
useEffect(() => {
|
|
const foundField = localFields.find((field) => field.formId === selectedFieldFormId);
|
|
setSelectedFieldFormId(foundField?.formId ?? null);
|
|
}, [selectedFieldFormId, localFields]);
|
|
|
|
const setSelectedRecipient = (recipientId: number | null) => {
|
|
const foundRecipient = envelope.recipients.find((recipient) => recipient.id === recipientId);
|
|
|
|
setSelectedRecipientId(foundRecipient?.id ?? null);
|
|
};
|
|
|
|
return {
|
|
// Core state
|
|
localFields,
|
|
|
|
// Field operations
|
|
addField,
|
|
removeFieldsByFormId,
|
|
updateFieldByFormId,
|
|
duplicateField,
|
|
duplicateFieldToAllPages,
|
|
|
|
// Field utilities
|
|
getFieldByFormId,
|
|
getFieldsByRecipient,
|
|
|
|
// Selected field
|
|
selectedField,
|
|
setSelectedField,
|
|
|
|
// Selected recipient
|
|
selectedRecipient,
|
|
setSelectedRecipient,
|
|
};
|
|
};
|