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
This commit is contained in:
pvrn
2025-10-22 14:17:36 +02:00
committed by GitHub
parent 2d9e870395
commit 0221ef991e
@@ -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<JsonValue>(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,