diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/hooks/useTextVariableEditor.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/hooks/useTextVariableEditor.ts index f6011e83cc..a5db116e60 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/hooks/useTextVariableEditor.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/hooks/useTextVariableEditor.ts @@ -5,8 +5,10 @@ import HardBreak from '@tiptap/extension-hard-break'; import Paragraph from '@tiptap/extension-paragraph'; import Text from '@tiptap/extension-text'; import { Placeholder, UndoRedo } from '@tiptap/extensions'; +import { AllSelection, TextSelection } from '@tiptap/pm/state'; import { type Editor, useEditor } from '@tiptap/react'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, parseJson } from 'twenty-shared/utils'; +import { type JsonValue } from 'type-fest'; type UseTextVariableEditorProps = { placeholder: string | undefined; @@ -16,6 +18,18 @@ type UseTextVariableEditorProps = { onUpdate: (editor: Editor) => void; }; +/** + * Checks if the given text is a valid JSON object (not array, primitive, or null) + */ +const isJsonObject = (text: string): boolean => { + try { + const parsed = JSON.parse(text); + return parsed !== null && typeof parsed === 'object'; + } catch { + return false; + } +}; + export const useTextVariableEditor = ({ placeholder, multiline, @@ -68,6 +82,41 @@ export const useTextVariableEditor = ({ } return false; }, + handlePaste: (view, _, slice) => { + try { + const { + state: { schema, tr }, + } = view; + const originalPos = tr.selection.from; + const pastedText = slice.content.firstChild?.textContent ?? ''; + + // Apply the clipboard text to the document without formatting + tr.replaceSelection(slice); + + const newPos = tr.selection.from; + + // Parse the entire document content as JSON and create formatted document node + const parsedJson = parseJson(tr.doc.textContent); + const formattedJson = JSON.stringify(parsedJson, null, 2); + const formattedDocNode = schema.nodeFromJSON( + getInitialEditorContent(formattedJson), + ); + + // Replace entire document with formatted JSON + const rootDocSelection = new AllSelection(tr.doc); + tr.setSelection(rootDocSelection); + tr.replaceSelectionWith(formattedDocNode); + + // Restore cursor position based on pasted content type + const finalPos = isJsonObject(pastedText) ? originalPos : newPos; + tr.setSelection(TextSelection.create(tr.doc, finalPos)); + + view.dispatch(tr); + return true; + } catch { + return false; + } + }, }, enableInputRules: false, enablePasteRules: false,