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:
+50
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user