From f00e7cc67040b51692a10959b25bede1318d4be9 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Fri, 31 Jan 2025 18:00:40 +0100 Subject: [PATCH] Make variable nodes undeletable in a readonly tiptap editor (#9950) In this PR: - Refactor how we initialize the content of the tiptap editor; providing a default value for the editor makes node appear instantly - Hide the button to remove a variable tag when the editor is readonly | Editable | Readonly | |--------|--------| | ![CleanShot 2025-01-31 at 15 04 25@2x](https://github.com/user-attachments/assets/54b90c80-aab1-4ff0-93f9-a0550f031d82) | ![CleanShot 2025-01-31 at 15 05 51@2x](https://github.com/user-attachments/assets/0480a7dc-9d7a-4e3f-b1a5-0550548622c6) | --------- Co-authored-by: Lucas Bordeau --- .../form-types/components/VariableChip.tsx | 14 +- .../FormRawJsonFieldInput.stories.tsx | 39 +++ .../FormTextFieldInput.stories.tsx | 113 ++++++- .../form-types/hooks/useTextVariableEditor.ts | 17 +- .../WorkflowTextEditorVariableChip.tsx | 6 +- .../__tests__/getInitialEditorContent.test.ts | 306 ++++++++++++++++++ .../__tests__/initializeEditorContent.test.ts | 195 ----------- .../utils/getInitialEditorContent.ts | 44 +++ .../utils/initializeEditorContent.ts | 36 --- 9 files changed, 514 insertions(+), 256 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getInitialEditorContent.test.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/initializeEditorContent.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/getInitialEditorContent.ts delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/initializeEditorContent.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/VariableChip.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/VariableChip.tsx index 7ba6392a3..07f615af5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/VariableChip.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/VariableChip.tsx @@ -9,7 +9,6 @@ const StyledChip = styled.div<{ deletable: boolean }>` border-radius: 4px; height: 20px; box-sizing: border-box; - cursor: pointer; display: inline-flex; align-items: center; flex-direction: row; @@ -20,10 +19,13 @@ const StyledChip = styled.div<{ deletable: boolean }>` white-space: nowrap; ${({ theme, deletable }) => - !deletable && - css` - padding-right: ${theme.spacing(1)}; - `} + !deletable + ? css` + padding-right: ${theme.spacing(1)}; + ` + : css` + cursor: pointer; + `} `; const StyledLabel = styled.span` @@ -70,7 +72,7 @@ export const VariableChip = ({ {extractVariableLabel(rawVariableName)} {onRemove ? ( - + ) : null} diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormRawJsonFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormRawJsonFieldInput.stories.tsx index e334790be..21f1707ac 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormRawJsonFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormRawJsonFieldInput.stories.tsx @@ -83,6 +83,45 @@ export const SaveValidJson: Story = { }, }; +export const SaveValidMultilineJson: Story = { + args: { + placeholder: 'Enter valid json', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const editor = canvasElement.querySelector('.ProseMirror > p'); + expect(editor).toBeVisible(); + + await userEvent.type( + editor, + '{{{Enter} "a": {{{Enter} "b" : "d"{Enter} }{Enter}}', + ); + + await waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith( + '{\n "a": {\n "b" : "d"\n }\n}', + ); + }); + }, +}; + +export const MultilineWithDefaultValue: Story = { + args: { + placeholder: 'Enter valid json', + defaultValue: '{\n "a": {\n "b" : "d"\n }\n}', + }, + play: async ({ canvasElement }) => { + const editor = canvasElement.querySelector('.ProseMirror > p'); + expect(editor).toBeVisible(); + + await waitFor(() => { + expect((editor as HTMLElement).innerText).toBe( + '{\n "a": {\n "b" : "d"\n }\n}', + ); + }); + }, +}; + export const DoesNotIgnoreInvalidJson: Story = { args: { placeholder: 'Enter valid json', diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormTextFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormTextFieldInput.stories.tsx index f0cb749bc..1b3352ed1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormTextFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormTextFieldInput.stories.tsx @@ -1,5 +1,12 @@ import { Meta, StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, within } from '@storybook/test'; +import { + expect, + fn, + userEvent, + waitFor, + waitForElementToBeRemoved, + within, +} from '@storybook/test'; import { getUserDevice } from 'twenty-ui'; import { FormTextFieldInput } from '../FormTextFieldInput'; @@ -45,18 +52,92 @@ export const Multiline: Story = { }, }; -export const WithVariablePicker: Story = { +export const MultilineWithDefaultValue: Story = { args: { label: 'Text', + defaultValue: 'Line 1\nLine 2\n\nLine 4', placeholder: 'Text field...', - VariablePicker: () =>
VariablePicker
, + multiline: true, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const variablePicker = await canvas.findByText('VariablePicker'); + await canvas.findByText(/^Text$/); - expect(variablePicker).toBeVisible(); + const editor = canvasElement.querySelector('.ProseMirror > p'); + + expect((editor as HTMLElement).innerText).toBe('Line 1\nLine 2\n\nLine 4'); + }, +}; + +export const WithVariable: Story = { + args: { + label: 'Text', + placeholder: 'Text field...', + VariablePicker: ({ onVariableSelect }) => { + return ( + + ); + }, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const addVariableButton = await canvas.findByRole('button', { + name: 'Add variable', + }); + + await userEvent.click(addVariableButton); + + const variable = await canvas.findByText('test'); + expect(variable).toBeVisible(); + + await waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith('{{test}}'); + }); + expect(args.onPersist).toHaveBeenCalledTimes(1); + }, +}; + +export const WithDeletableVariable: Story = { + args: { + label: 'Text', + placeholder: 'Text field...', + defaultValue: 'test {{a.b.variable}} test', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const editor = canvasElement.querySelector('.ProseMirror > p'); + expect(editor).toBeVisible(); + + const variable = await canvas.findByText('variable'); + expect(variable).toBeVisible(); + + const deleteVariableButton = await canvas.findByRole('button', { + name: 'Remove variable', + }); + + await Promise.all([ + waitForElementToBeRemoved(variable), + + deleteVariableButton.click(), + ]); + + expect(editor).toHaveTextContent('test test'); + + await waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith('test test'); + }); + expect(args.onPersist).toHaveBeenCalledTimes(1); }, }; @@ -89,6 +170,28 @@ export const Disabled: Story = { }, }; +export const DisabledWithVariable: Story = { + args: { + label: 'Text', + defaultValue: 'test {{a.b.variable}} test', + readonly: true, + }, + play: async ({ canvasElement }) => { + const editor = canvasElement.querySelector('.ProseMirror > p'); + + expect(editor).toBeVisible(); + + await waitFor(() => { + expect(editor).toHaveTextContent('test variable test'); + }); + + const deleteVariableButton = within(editor as HTMLElement).queryByRole( + 'button', + ); + expect(deleteVariableButton).not.toBeInTheDocument(); + }, +}; + export const HasHistory: Story = { args: { label: 'Text', diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/hooks/useTextVariableEditor.ts b/packages/twenty-front/src/modules/object-record/record-field/form-types/hooks/useTextVariableEditor.ts index 232454437..d3125b5f2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/hooks/useTextVariableEditor.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/hooks/useTextVariableEditor.ts @@ -1,4 +1,4 @@ -import { initializeEditorContent } from '@/workflow/workflow-variables/utils/initializeEditorContent'; +import { getInitialEditorContent } from '@/workflow/workflow-variables/utils/getInitialEditorContent'; import { VariableTag } from '@/workflow/workflow-variables/utils/variableTag'; import Document from '@tiptap/extension-document'; import HardBreak from '@tiptap/extension-hard-break'; @@ -7,7 +7,6 @@ import Paragraph from '@tiptap/extension-paragraph'; import { default as Placeholder } from '@tiptap/extension-placeholder'; import Text from '@tiptap/extension-text'; import { Editor, useEditor } from '@tiptap/react'; -import { useState } from 'react'; import { isDefined } from 'twenty-ui'; type UseTextVariableEditorProps = { @@ -25,8 +24,6 @@ export const useTextVariableEditor = ({ defaultValue, onUpdate, }: UseTextVariableEditorProps) => { - const [isInitializing, setIsInitializing] = useState(true); - const editor = useEditor({ extensions: [ Document, @@ -45,17 +42,11 @@ export const useTextVariableEditor = ({ : []), History, ], + content: isDefined(defaultValue) + ? getInitialEditorContent(defaultValue) + : undefined, editable: !readonly, - onCreate: ({ editor }) => { - if (isDefined(defaultValue)) { - initializeEditorContent(editor, defaultValue); - } - setIsInitializing(false); - }, onUpdate: ({ editor }) => { - if (isInitializing) { - return; - } onUpdate(editor); }, editorProps: { diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowTextEditorVariableChip.tsx b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowTextEditorVariableChip.tsx index 885f9f5dd..bf4fa8a27 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowTextEditorVariableChip.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowTextEditorVariableChip.tsx @@ -12,6 +12,7 @@ type WorkflowTextEditorVariableChipProps = NodeViewProps; export const WorkflowTextEditorVariableChip = ({ deleteNode, node, + editor, }: WorkflowTextEditorVariableChipProps) => { const attrs = node.attrs as { variable: string; @@ -19,7 +20,10 @@ export const WorkflowTextEditorVariableChip = ({ return ( - + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getInitialEditorContent.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getInitialEditorContent.test.ts new file mode 100644 index 000000000..88000ea27 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getInitialEditorContent.test.ts @@ -0,0 +1,306 @@ +import { getInitialEditorContent } from '../getInitialEditorContent'; + +describe('getInitialEditorContent', () => { + it('should handle single line text', () => { + expect(getInitialEditorContent('Hello world')).toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [ + { + "text": "Hello world", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); + + it('should handle text with newlines', () => { + expect(getInitialEditorContent('Line 1\nLine 2')).toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [ + { + "text": "Line 1", + "type": "text", + }, + { + "type": "hardBreak", + }, + { + "text": "Line 2", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); + + it('should handle single variable', () => { + expect(getInitialEditorContent('{{user.name}}')).toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [ + { + "attrs": { + "variable": "{{user.name}}", + }, + "type": "variableTag", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); + + it('should handle text with variables', () => { + expect(getInitialEditorContent('Hello {{user.name}}, welcome!')) + .toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [ + { + "text": "Hello ", + "type": "text", + }, + { + "attrs": { + "variable": "{{user.name}}", + }, + "type": "variableTag", + }, + { + "text": ", welcome!", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); + + it('should handle text with multiple variables', () => { + expect( + getInitialEditorContent('Hello {{user.firstName}} {{user.lastName}}!'), + ).toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [ + { + "text": "Hello ", + "type": "text", + }, + { + "attrs": { + "variable": "{{user.firstName}}", + }, + "type": "variableTag", + }, + { + "text": " ", + "type": "text", + }, + { + "attrs": { + "variable": "{{user.lastName}}", + }, + "type": "variableTag", + }, + { + "text": "!", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); + + it('should handle newlines with variables', () => { + expect( + getInitialEditorContent('Hello {{user.name}}\nWelcome to {{app.name}}'), + ).toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [ + { + "text": "Hello ", + "type": "text", + }, + { + "attrs": { + "variable": "{{user.name}}", + }, + "type": "variableTag", + }, + { + "type": "hardBreak", + }, + { + "text": "Welcome to ", + "type": "text", + }, + { + "attrs": { + "variable": "{{app.name}}", + }, + "type": "variableTag", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); + + it('should handle empty strings', () => { + expect(getInitialEditorContent('')).toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); + + it('should handle multiple empty parts', () => { + expect(getInitialEditorContent('Hello {{user.name}} !')) + .toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [ + { + "text": "Hello ", + "type": "text", + }, + { + "attrs": { + "variable": "{{user.name}}", + }, + "type": "variableTag", + }, + { + "text": " !", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); + + it('should handle multiple newlines', () => { + expect(getInitialEditorContent('Line1\n\nLine3')).toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [ + { + "text": "Line1", + "type": "text", + }, + { + "type": "hardBreak", + }, + { + "type": "hardBreak", + }, + { + "text": "Line3", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); + + it('should ignore malformed variable tags', () => { + expect( + getInitialEditorContent('Hello {{user.name}} and {{invalid}more}} text'), + ).toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [ + { + "text": "Hello ", + "type": "text", + }, + { + "attrs": { + "variable": "{{user.name}}", + }, + "type": "variableTag", + }, + { + "text": " and {{invalid}more}} text", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); + + it('should handle trailing newlines', () => { + expect(getInitialEditorContent('Hello\n')).toMatchInlineSnapshot(` +{ + "content": [ + { + "content": [ + { + "text": "Hello", + "type": "text", + }, + { + "type": "hardBreak", + }, + ], + "type": "paragraph", + }, + ], + "type": "doc", +} +`); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/initializeEditorContent.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/initializeEditorContent.test.ts deleted file mode 100644 index e9056342c..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/initializeEditorContent.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { Editor } from '@tiptap/react'; -import { initializeEditorContent } from '../initializeEditorContent'; - -describe('initializeEditorContent', () => { - let mockEditor: Editor; - - beforeEach(() => { - mockEditor = { - commands: { - insertContent: jest.fn(), - }, - } as unknown as Editor; - }); - - it('should handle single line text', () => { - initializeEditorContent(mockEditor, 'Hello world'); - - expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(1); - expect(mockEditor.commands.insertContent).toHaveBeenCalledWith( - 'Hello world', - ); - }); - - it('should handle text with newlines', () => { - initializeEditorContent(mockEditor, 'Line 1\nLine 2'); - - expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(3); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 1, - 'Line 1', - ); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { - type: 'hardBreak', - }); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 3, - 'Line 2', - ); - }); - - it('should handle single variable', () => { - initializeEditorContent(mockEditor, '{{user.name}}'); - - expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(1); - expect(mockEditor.commands.insertContent).toHaveBeenCalledWith({ - type: 'variableTag', - attrs: { variable: '{{user.name}}' }, - }); - }); - - it('should handle text with variables', () => { - initializeEditorContent(mockEditor, 'Hello {{user.name}}, welcome!'); - - expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(3); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 1, - 'Hello ', - ); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { - type: 'variableTag', - attrs: { variable: '{{user.name}}' }, - }); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 3, - ', welcome!', - ); - }); - - it('should handle text with multiple variables', () => { - initializeEditorContent( - mockEditor, - 'Hello {{user.firstName}} {{user.lastName}}!', - ); - - expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(5); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 1, - 'Hello ', - ); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { - type: 'variableTag', - attrs: { variable: '{{user.firstName}}' }, - }); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(3, ' '); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(4, { - type: 'variableTag', - attrs: { variable: '{{user.lastName}}' }, - }); - }); - - it('should handle newlines with variables', () => { - initializeEditorContent( - mockEditor, - 'Hello {{user.name}}\nWelcome to {{app.name}}', - ); - - expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(5); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 1, - 'Hello ', - ); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { - type: 'variableTag', - attrs: { variable: '{{user.name}}' }, - }); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(3, { - type: 'hardBreak', - }); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 4, - 'Welcome to ', - ); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(5, { - type: 'variableTag', - attrs: { variable: '{{app.name}}' }, - }); - }); - - it('should handle empty strings', () => { - initializeEditorContent(mockEditor, ''); - expect(mockEditor.commands.insertContent).not.toHaveBeenCalled(); - }); - - it('should handle multiple empty parts', () => { - initializeEditorContent(mockEditor, 'Hello {{user.name}} !'); - - expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(3); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 1, - 'Hello ', - ); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { - type: 'variableTag', - attrs: { variable: '{{user.name}}' }, - }); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 3, - ' !', - ); - }); - - it('should handle multiple newlines', () => { - initializeEditorContent(mockEditor, 'Line1\n\nLine3'); - - expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(4); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 1, - 'Line1', - ); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { - type: 'hardBreak', - }); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(3, { - type: 'hardBreak', - }); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 4, - 'Line3', - ); - }); - - it('should ignore malformed variable tags', () => { - initializeEditorContent( - mockEditor, - 'Hello {{user.name}} and {{invalid}more}} text', - ); - - expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(3); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 1, - 'Hello ', - ); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { - type: 'variableTag', - attrs: { variable: '{{user.name}}' }, - }); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 3, - ' and {{invalid}more}} text', - ); - }); - - it('should handle trailing newlines', () => { - initializeEditorContent(mockEditor, 'Hello\n'); - - expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(2); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( - 1, - 'Hello', - ); - expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { - type: 'hardBreak', - }); - }); -}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getInitialEditorContent.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getInitialEditorContent.ts new file mode 100644 index 000000000..6e4a5475e --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getInitialEditorContent.ts @@ -0,0 +1,44 @@ +import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; +import { isNonEmptyString } from '@sniptt/guards'; +import { JSONContent } from '@tiptap/react'; + +export const CAPTURE_VARIABLE_TAG_REGEX = /({{[^{}]+}})/; + +export const getInitialEditorContent = (rawContent: string): JSONContent => { + const paragraphContent: JSONContent[] = []; + const lines = rawContent.split(/\n/); + + lines.forEach((line, index) => { + const parts = line.split(CAPTURE_VARIABLE_TAG_REGEX); + + parts.forEach((part) => { + if (isStandaloneVariableString(part)) { + paragraphContent.push({ + type: 'variableTag', + attrs: { variable: part }, + }); + } else if (isNonEmptyString(part)) { + paragraphContent.push({ + type: 'text', + text: part, + }); + } + }); + + if (index < lines.length - 1) { + paragraphContent.push({ + type: 'hardBreak', + }); + } + }); + + return { + type: 'doc', + content: [ + { + type: 'paragraph', + content: paragraphContent, + }, + ], + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/initializeEditorContent.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/initializeEditorContent.ts deleted file mode 100644 index a194de4ca..000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/initializeEditorContent.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; -import { Editor } from '@tiptap/react'; - -export const CAPTURE_VARIABLE_TAG_REGEX = /({{[^{}]+}})/; - -export const initializeEditorContent = (editor: Editor, content: string) => { - const lines = content.split(/\n/); - - lines.forEach((line, index) => { - const parts = line.split(CAPTURE_VARIABLE_TAG_REGEX); - parts.forEach((part) => { - if (part.length === 0) { - return; - } - - if (part.startsWith('{{') && part.endsWith('}}')) { - editor.commands.insertContent({ - type: 'variableTag', - attrs: { variable: part }, - }); - return; - } - - if (isNonEmptyString(part)) { - editor.commands.insertContent(part); - } - }); - - // Add hard break if it's not the last line - if (index < lines.length - 1) { - editor.commands.insertContent({ - type: 'hardBreak', - }); - } - }); -};