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;
|
||||
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 = ({
|
||||
<StyledLabel>{extractVariableLabel(rawVariableName)}</StyledLabel>
|
||||
|
||||
{onRemove ? (
|
||||
<StyledDelete onClick={onRemove}>
|
||||
<StyledDelete onClick={onRemove} aria-label="Remove variable">
|
||||
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
|
||||
</StyledDelete>
|
||||
) : 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 = {
|
||||
args: {
|
||||
placeholder: 'Enter valid json',
|
||||
|
||||
@ -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: () => <div>VariablePicker</div>,
|
||||
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 (
|
||||
<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 = {
|
||||
args: {
|
||||
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 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: {
|
||||
|
||||
Reference in New Issue
Block a user