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
@ -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 (
|
||||
<NodeViewWrapper as={StyledWrapper} style={{ whiteSpace: 'nowrap' }}>
|
||||
<VariableChip rawVariableName={attrs.variable} onRemove={deleteNode} />
|
||||
<VariableChip
|
||||
rawVariableName={attrs.variable}
|
||||
onRemove={editor.isEditable ? deleteNode : undefined}
|
||||
/>
|
||||
</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