From 0221ef991eddcf6ff358037924bf5b7fc6d099ea Mon Sep 17 00:00:00 2001 From: pvrn <78180340+pvrnn@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:17:36 +0200 Subject: [PATCH] Format pasted JSON in Workflow HttpRequest Action (#15119) ## Context The _expected response body_ does not use monaco-editor, so enabling `formatOnPaste `as mentioned in this [comment](https://github.com/twentyhq/twenty/issues/13506#issuecomment-3188746787) didn't fix the issue below. However, the `TextVariableEditor` uses tiptap editor which has a `handlePaste` option that we can use to format JSON. - Issue https://github.com/twentyhq/twenty/issues/13506 ## Implementation: 1. Insert the clipboard text at the current selection. 2. Retrieve the full editor content, parse it as JSON and stringify it with indentation. 3. Re-use the method that transforms the text into `JSONContent`. 4. Replace the root node with the newly formatted `JSONContent`. 5. Update the cursor position based on the type of pasted text. 6. Apply the transaction with `view.dispatch()` Note: If parsing fails (invalid JSON), the input will fall back to a normal paste. ## test Loom: https://www.loom.com/share/f2e84d078662481f9f9f71fc98b772a1?sid=a649c4e9-8c55-4cbe-b031-7aba60af0e05 --- .../form-types/hooks/useTextVariableEditor.ts | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) 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,