mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
feat: bulk add fields (#1683)
## Demo  --------- Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
This commit is contained in:
@ -173,34 +173,59 @@ export const ConfigureFieldsView = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onFieldCopy = useCallback(
|
const onFieldCopy = useCallback(
|
||||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
|
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
||||||
const { duplicate = false } = options ?? {};
|
const { duplicate = false, duplicateAll = false } = options ?? {};
|
||||||
|
|
||||||
if (lastActiveField) {
|
if (lastActiveField) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
|
||||||
if (!duplicate) {
|
if (duplicate) {
|
||||||
setFieldClipboard(lastActiveField);
|
const newField: TConfigureFieldsFormSchema['fields'][0] = {
|
||||||
|
...structuredClone(lastActiveField),
|
||||||
|
nativeId: undefined,
|
||||||
|
formId: nanoid(12),
|
||||||
|
signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail,
|
||||||
|
recipientId: selectedRecipient?.id ?? lastActiveField.recipientId,
|
||||||
|
pageX: lastActiveField.pageX + 3,
|
||||||
|
pageY: lastActiveField.pageY + 3,
|
||||||
|
};
|
||||||
|
|
||||||
toast({
|
append(newField);
|
||||||
title: 'Copied field',
|
|
||||||
description: 'Copied field to clipboard',
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateAll) {
|
||||||
|
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
|
||||||
|
|
||||||
|
pages.forEach((_, index) => {
|
||||||
|
const pageNumber = index + 1;
|
||||||
|
|
||||||
|
if (pageNumber === lastActiveField.pageNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newField: TConfigureFieldsFormSchema['fields'][0] = {
|
||||||
|
...structuredClone(lastActiveField),
|
||||||
|
nativeId: undefined,
|
||||||
|
formId: nanoid(12),
|
||||||
|
signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail,
|
||||||
|
recipientId: selectedRecipient?.id ?? lastActiveField.recipientId,
|
||||||
|
pageNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
append(newField);
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newField: TConfigureFieldsFormSchema['fields'][0] = {
|
setFieldClipboard(lastActiveField);
|
||||||
...structuredClone(lastActiveField),
|
|
||||||
nativeId: undefined,
|
|
||||||
formId: nanoid(12),
|
|
||||||
signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail,
|
|
||||||
recipientId: selectedRecipient?.id ?? lastActiveField.recipientId,
|
|
||||||
pageX: lastActiveField.pageX + 3,
|
|
||||||
pageY: lastActiveField.pageY + 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
append(newField);
|
toast({
|
||||||
|
title: 'Copied field',
|
||||||
|
description: 'Copied field to clipboard',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[append, lastActiveField, selectedRecipient?.email, selectedRecipient?.id, toast],
|
[append, lastActiveField, selectedRecipient?.email, selectedRecipient?.id, toast],
|
||||||
@ -533,6 +558,7 @@ export const ConfigureFieldsView = ({
|
|||||||
onMove={(node) => onFieldMove(node, index)}
|
onMove={(node) => onFieldMove(node, index)}
|
||||||
onRemove={() => remove(index)}
|
onRemove={() => remove(index)}
|
||||||
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
||||||
|
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
||||||
onFocus={() => setLastActiveField(field)}
|
onFocus={() => setLastActiveField(field)}
|
||||||
onBlur={() => setLastActiveField(null)}
|
onBlur={() => setLastActiveField(null)}
|
||||||
onAdvancedSettings={() => {
|
onAdvancedSettings={() => {
|
||||||
|
|||||||
@ -69,8 +69,6 @@ export const TemplatesTableActionDropdown = ({
|
|||||||
? `${templateRootPath}/f/${row.folderId}/${row.id}/edit`
|
? `${templateRootPath}/f/${row.folderId}/${row.id}/edit`
|
||||||
: `${templateRootPath}/${row.id}/edit`;
|
: `${templateRootPath}/${row.id}/edit`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger data-testid="template-table-action-btn">
|
<DropdownMenuTrigger data-testid="template-table-action-btn">
|
||||||
|
|||||||
@ -400,35 +400,60 @@ export const AddFieldsFormPartial = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onFieldCopy = useCallback(
|
const onFieldCopy = useCallback(
|
||||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
|
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
||||||
const { duplicate = false } = options ?? {};
|
const { duplicate = false, duplicateAll = false } = options ?? {};
|
||||||
|
|
||||||
if (lastActiveField) {
|
if (lastActiveField) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
|
||||||
if (!duplicate) {
|
if (duplicate) {
|
||||||
setFieldClipboard(lastActiveField);
|
const newField: TAddFieldsFormSchema['fields'][0] = {
|
||||||
|
...structuredClone(lastActiveField),
|
||||||
|
nativeId: undefined,
|
||||||
|
formId: nanoid(12),
|
||||||
|
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||||
|
pageX: lastActiveField.pageX + 3,
|
||||||
|
pageY: lastActiveField.pageY + 3,
|
||||||
|
};
|
||||||
|
|
||||||
toast({
|
append(newField);
|
||||||
title: 'Copied field',
|
|
||||||
description: 'Copied field to clipboard',
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateAll) {
|
||||||
|
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
|
||||||
|
|
||||||
|
pages.forEach((_, index) => {
|
||||||
|
const pageNumber = index + 1;
|
||||||
|
|
||||||
|
if (pageNumber === lastActiveField.pageNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newField: TAddFieldsFormSchema['fields'][0] = {
|
||||||
|
...structuredClone(lastActiveField),
|
||||||
|
nativeId: undefined,
|
||||||
|
formId: nanoid(12),
|
||||||
|
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||||
|
pageNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
append(newField);
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newField: TAddFieldsFormSchema['fields'][0] = {
|
setFieldClipboard(lastActiveField);
|
||||||
...structuredClone(lastActiveField),
|
|
||||||
formId: nanoid(12),
|
|
||||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
|
||||||
pageX: lastActiveField.pageX + 3,
|
|
||||||
pageY: lastActiveField.pageY + 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
append(newField);
|
toast({
|
||||||
|
title: 'Copied field',
|
||||||
|
description: 'Copied field to clipboard',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[append, lastActiveField, selectedSigner?.email, toast],
|
[append, lastActiveField, selectedSigner?.email, selectedSigner?.id, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFieldPaste = useCallback(
|
const onFieldPaste = useCallback(
|
||||||
@ -641,6 +666,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
onMove={(options) => onFieldMove(options, index)}
|
onMove={(options) => onFieldMove(options, index)}
|
||||||
onRemove={() => remove(index)}
|
onRemove={() => remove(index)}
|
||||||
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
||||||
|
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
||||||
onAdvancedSettings={() => {
|
onAdvancedSettings={() => {
|
||||||
setCurrentField(field);
|
setCurrentField(field);
|
||||||
handleAdvancedSettings();
|
handleAdvancedSettings();
|
||||||
|
|||||||
@ -311,6 +311,7 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.otherwise(() => null)}
|
.otherwise(() => null)}
|
||||||
|
|
||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<ul>
|
<ul>
|
||||||
@ -323,6 +324,7 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|
||||||
<DocumentFlowFormContainerFooter className="mt-auto">
|
<DocumentFlowFormContainerFooter className="mt-auto">
|
||||||
<DocumentFlowFormContainerActions
|
<DocumentFlowFormContainerActions
|
||||||
goNextLabel={msg`Save`}
|
goNextLabel={msg`Save`}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { FieldType } from '@prisma/client';
|
import { FieldType } from '@prisma/client';
|
||||||
import { CopyPlus, Settings2, Trash } from 'lucide-react';
|
import { CopyPlus, Settings2, SquareStack, Trash } from 'lucide-react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Rnd } from 'react-rnd';
|
import { Rnd } from 'react-rnd';
|
||||||
|
|
||||||
@ -29,6 +31,7 @@ export type FieldItemProps = {
|
|||||||
onMove?: (_node: HTMLElement) => void;
|
onMove?: (_node: HTMLElement) => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
onDuplicate?: () => void;
|
onDuplicate?: () => void;
|
||||||
|
onDuplicateAllPages?: () => void;
|
||||||
onAdvancedSettings?: () => void;
|
onAdvancedSettings?: () => void;
|
||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
@ -55,15 +58,18 @@ export const FieldItem = ({
|
|||||||
onMove,
|
onMove,
|
||||||
onRemove,
|
onRemove,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
|
onDuplicateAllPages,
|
||||||
|
onAdvancedSettings,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
onAdvancedSettings,
|
|
||||||
recipientIndex = 0,
|
recipientIndex = 0,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
active,
|
active,
|
||||||
onFieldActivate,
|
onFieldActivate,
|
||||||
onFieldDeactivate,
|
onFieldDeactivate,
|
||||||
}: FieldItemProps) => {
|
}: FieldItemProps) => {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [coords, setCoords] = useState({
|
const [coords, setCoords] = useState({
|
||||||
pageX: 0,
|
pageX: 0,
|
||||||
pageY: 0,
|
pageY: 0,
|
||||||
@ -304,6 +310,7 @@ export const FieldItem = ({
|
|||||||
<div className="group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
|
<div className="group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
|
||||||
{advancedField && (
|
{advancedField && (
|
||||||
<button
|
<button
|
||||||
|
title={_(msg`Advanced settings`)}
|
||||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||||
onClick={onAdvancedSettings}
|
onClick={onAdvancedSettings}
|
||||||
onTouchEnd={onAdvancedSettings}
|
onTouchEnd={onAdvancedSettings}
|
||||||
@ -313,6 +320,7 @@ export const FieldItem = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
title={_(msg`Duplicate`)}
|
||||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||||
onClick={onDuplicate}
|
onClick={onDuplicate}
|
||||||
onTouchEnd={onDuplicate}
|
onTouchEnd={onDuplicate}
|
||||||
@ -321,6 +329,16 @@ export const FieldItem = ({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
title={_(msg`Duplicate on all pages`)}
|
||||||
|
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||||
|
onClick={onDuplicateAllPages}
|
||||||
|
onTouchEnd={onDuplicateAllPages}
|
||||||
|
>
|
||||||
|
<SquareStack className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
title={_(msg`Remove`)}
|
||||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
onTouchEnd={onRemove}
|
onTouchEnd={onRemove}
|
||||||
|
|||||||
@ -139,44 +139,64 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onFieldCopy = useCallback(
|
const onFieldCopy = useCallback(
|
||||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
|
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
||||||
const { duplicate = false } = options ?? {};
|
const { duplicate = false, duplicateAll = false } = options ?? {};
|
||||||
|
|
||||||
if (lastActiveField) {
|
if (lastActiveField) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
|
||||||
if (!duplicate) {
|
if (duplicate) {
|
||||||
setFieldClipboard(lastActiveField);
|
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
|
||||||
|
...structuredClone(lastActiveField),
|
||||||
|
nativeId: undefined,
|
||||||
|
formId: nanoid(12),
|
||||||
|
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||||
|
signerId: selectedSigner?.id ?? lastActiveField.signerId,
|
||||||
|
signerToken: selectedSigner?.token ?? lastActiveField.signerToken,
|
||||||
|
pageX: lastActiveField.pageX + 3,
|
||||||
|
pageY: lastActiveField.pageY + 3,
|
||||||
|
};
|
||||||
|
|
||||||
toast({
|
append(newField);
|
||||||
title: 'Copied field',
|
|
||||||
description: 'Copied field to clipboard',
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateAll) {
|
||||||
|
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
|
||||||
|
|
||||||
|
pages.forEach((_, index) => {
|
||||||
|
const pageNumber = index + 1;
|
||||||
|
|
||||||
|
if (pageNumber === lastActiveField.pageNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
|
||||||
|
...structuredClone(lastActiveField),
|
||||||
|
nativeId: undefined,
|
||||||
|
formId: nanoid(12),
|
||||||
|
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||||
|
signerId: selectedSigner?.id ?? lastActiveField.signerId,
|
||||||
|
signerToken: selectedSigner?.token ?? lastActiveField.signerToken,
|
||||||
|
pageNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
append(newField);
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
|
setFieldClipboard(lastActiveField);
|
||||||
...structuredClone(lastActiveField),
|
|
||||||
formId: nanoid(12),
|
|
||||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
|
||||||
signerId: selectedSigner?.id ?? lastActiveField.signerId,
|
|
||||||
signerToken: selectedSigner?.token ?? lastActiveField.signerToken,
|
|
||||||
pageX: lastActiveField.pageX + 3,
|
|
||||||
pageY: lastActiveField.pageY + 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
append(newField);
|
toast({
|
||||||
|
title: 'Copied field',
|
||||||
|
description: 'Copied field to clipboard',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[append, lastActiveField, selectedSigner?.email, selectedSigner?.id, toast],
|
||||||
append,
|
|
||||||
lastActiveField,
|
|
||||||
selectedSigner?.email,
|
|
||||||
selectedSigner?.id,
|
|
||||||
selectedSigner?.token,
|
|
||||||
toast,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFieldPaste = useCallback(
|
const onFieldPaste = useCallback(
|
||||||
@ -543,6 +563,7 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
onMove={(options) => onFieldMove(options, index)}
|
onMove={(options) => onFieldMove(options, index)}
|
||||||
onRemove={() => remove(index)}
|
onRemove={() => remove(index)}
|
||||||
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
||||||
|
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
||||||
onAdvancedSettings={() => {
|
onAdvancedSettings={() => {
|
||||||
setCurrentField(field);
|
setCurrentField(field);
|
||||||
handleAdvancedSettings();
|
handleAdvancedSettings();
|
||||||
|
|||||||
Reference in New Issue
Block a user