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 | |--------|--------| |  |  | --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
committed by
GitHub
parent
fa3ea4bb68
commit
f00e7cc670
@ -9,7 +9,6 @@ const StyledChip = styled.div<{ deletable: boolean }>`
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -20,10 +19,13 @@ const StyledChip = styled.div<{ deletable: boolean }>`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
${({ theme, deletable }) =>
|
${({ theme, deletable }) =>
|
||||||
!deletable &&
|
!deletable
|
||||||
css`
|
? css`
|
||||||
padding-right: ${theme.spacing(1)};
|
padding-right: ${theme.spacing(1)};
|
||||||
`}
|
`
|
||||||
|
: css`
|
||||||
|
cursor: pointer;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledLabel = styled.span`
|
const StyledLabel = styled.span`
|
||||||
@ -70,7 +72,7 @@ export const VariableChip = ({
|
|||||||
<StyledLabel>{extractVariableLabel(rawVariableName)}</StyledLabel>
|
<StyledLabel>{extractVariableLabel(rawVariableName)}</StyledLabel>
|
||||||
|
|
||||||
{onRemove ? (
|
{onRemove ? (
|
||||||
<StyledDelete onClick={onRemove}>
|
<StyledDelete onClick={onRemove} aria-label="Remove variable">
|
||||||
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
|
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
|
||||||
</StyledDelete>
|
</StyledDelete>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -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 = {
|
export const DoesNotIgnoreInvalidJson: Story = {
|
||||||
args: {
|
args: {
|
||||||
placeholder: 'Enter valid json',
|
placeholder: 'Enter valid json',
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
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 { getUserDevice } from 'twenty-ui';
|
||||||
import { FormTextFieldInput } from '../FormTextFieldInput';
|
import { FormTextFieldInput } from '../FormTextFieldInput';
|
||||||
|
|
||||||
@ -45,18 +52,92 @@ export const Multiline: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithVariablePicker: Story = {
|
export const MultilineWithDefaultValue: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: 'Text',
|
label: 'Text',
|
||||||
|
defaultValue: 'Line 1\nLine 2\n\nLine 4',
|
||||||
placeholder: 'Text field...',
|
placeholder: 'Text field...',
|
||||||
VariablePicker: () => <div>VariablePicker</div>,
|
multiline: true,
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(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 (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onVariableSelect('{{test}}');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add variable
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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 = {
|
export const HasHistory: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: 'Text',
|
label: 'Text',
|
||||||
|
|||||||
@ -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 { VariableTag } from '@/workflow/workflow-variables/utils/variableTag';
|
||||||
import Document from '@tiptap/extension-document';
|
import Document from '@tiptap/extension-document';
|
||||||
import HardBreak from '@tiptap/extension-hard-break';
|
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 { default as Placeholder } from '@tiptap/extension-placeholder';
|
||||||
import Text from '@tiptap/extension-text';
|
import Text from '@tiptap/extension-text';
|
||||||
import { Editor, useEditor } from '@tiptap/react';
|
import { Editor, useEditor } from '@tiptap/react';
|
||||||
import { useState } from 'react';
|
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
type UseTextVariableEditorProps = {
|
type UseTextVariableEditorProps = {
|
||||||
@ -25,8 +24,6 @@ export const useTextVariableEditor = ({
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
}: UseTextVariableEditorProps) => {
|
}: UseTextVariableEditorProps) => {
|
||||||
const [isInitializing, setIsInitializing] = useState(true);
|
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
Document,
|
Document,
|
||||||
@ -45,17 +42,11 @@ export const useTextVariableEditor = ({
|
|||||||
: []),
|
: []),
|
||||||
History,
|
History,
|
||||||
],
|
],
|
||||||
|
content: isDefined(defaultValue)
|
||||||
|
? getInitialEditorContent(defaultValue)
|
||||||
|
: undefined,
|
||||||
editable: !readonly,
|
editable: !readonly,
|
||||||
onCreate: ({ editor }) => {
|
|
||||||
if (isDefined(defaultValue)) {
|
|
||||||
initializeEditorContent(editor, defaultValue);
|
|
||||||
}
|
|
||||||
setIsInitializing(false);
|
|
||||||
},
|
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
if (isInitializing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onUpdate(editor);
|
onUpdate(editor);
|
||||||
},
|
},
|
||||||
editorProps: {
|
editorProps: {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ type WorkflowTextEditorVariableChipProps = NodeViewProps;
|
|||||||
export const WorkflowTextEditorVariableChip = ({
|
export const WorkflowTextEditorVariableChip = ({
|
||||||
deleteNode,
|
deleteNode,
|
||||||
node,
|
node,
|
||||||
|
editor,
|
||||||
}: WorkflowTextEditorVariableChipProps) => {
|
}: WorkflowTextEditorVariableChipProps) => {
|
||||||
const attrs = node.attrs as {
|
const attrs = node.attrs as {
|
||||||
variable: string;
|
variable: string;
|
||||||
@ -19,7 +20,10 @@ export const WorkflowTextEditorVariableChip = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper as={StyledWrapper} style={{ whiteSpace: 'nowrap' }}>
|
<NodeViewWrapper as={StyledWrapper} style={{ whiteSpace: 'nowrap' }}>
|
||||||
<VariableChip rawVariableName={attrs.variable} onRemove={deleteNode} />
|
<VariableChip
|
||||||
|
rawVariableName={attrs.variable}
|
||||||
|
onRemove={editor.isEditable ? deleteNode : undefined}
|
||||||
|
/>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user