fix(#17016): apply Read-Only Mode to All Custom and Built-In Nodes in Block Editor (#17080)

* fix(#17016): apply Read-Only Mode to All Custom and Built-In Nodes in Block Editor

* test: add unit test

* fix playwright test

---------

Co-authored-by: Ashish Gupta <ashish@getcollate.io>
This commit is contained in:
Sachin Chaurasiya 2024-07-19 18:52:01 +05:30 committed by GitHub
parent 7ef90c3628
commit 8434fe2c50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 95 additions and 11 deletions

View File

@ -186,7 +186,7 @@ test.describe('Activity feed', () => {
test('Update Description Task on Columns', async ({ page }) => {
const firstTaskValue: TaskDetails = {
term: entity.entity.name,
assignee: `${user.data.firstName}.${user.data.lastName}`,
assignee: user.responseData.name,
description: 'Column Description 1',
columnName: entity.entity.columns[0].name,
oldDescription: entity.entity.columns[0].description,

View File

@ -33,7 +33,7 @@ interface BlockMenuProps {
export const BlockMenu = (props: BlockMenuProps) => {
const { t } = useTranslation();
const { editor } = props;
const { view } = editor;
const { view, isEditable } = editor;
const menuRef = useRef<HTMLDivElement>(null);
const popup = useRef<Instance | null>(null);
@ -127,7 +127,10 @@ export const BlockMenu = (props: BlockMenuProps) => {
}, [editor]);
useEffect(() => {
if (menuRef.current) {
/**
* Create a new tippy instance for the block menu if the editor is editable
*/
if (menuRef.current && isEditable) {
menuRef.current.remove();
menuRef.current.style.visibility = 'visible';
@ -150,7 +153,7 @@ export const BlockMenu = (props: BlockMenuProps) => {
popup.current?.destroy();
popup.current = null;
};
}, []);
}, [isEditable]);
useEffect(() => {
document.addEventListener('click', handleClickDragHandle);

View File

@ -111,12 +111,14 @@ const BubbleMenu: FC<BubbleMenuProps> = ({ editor, toggleLink }) => {
// - the selection is empty
// - the selection is a node selection (for drag handles)
// - link is active
// - editor is not editable
if (
editor.isActive('image') ||
empty ||
isNodeSelection(selection) ||
editor.isActive('link') ||
editor.isActive('table')
editor.isActive('table') ||
!editor.isEditable
) {
return false;
}

View File

@ -78,6 +78,11 @@ const EditorSlots = forwardRef<EditorSlotsRef, EditorSlotsProps>(
const handleLinkPopup = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
// if editor is not editable, do not show the link popup
if (!editor?.isEditable) {
return;
}
let popup: Instance<Props>[] = [];
let component: ReactRenderer;
const target = e.target as HTMLElement;

View File

@ -32,6 +32,9 @@ const mockNodeViewProps = {
node: mockNode,
extension: mockExtension,
updateAttributes: mockUpdateAttributes,
editor: {
isEditable: true,
},
} as unknown as NodeViewProps;
describe('CalloutComponent', () => {
@ -70,4 +73,29 @@ describe('CalloutComponent', () => {
expect(screen.getByTestId('callout-note')).toBeInTheDocument();
expect(screen.getByTestId('callout-danger')).toBeInTheDocument();
});
it('should not render the popover when callout button is clicked and editor is not editable', async () => {
const nodeViewProps = {
node: mockNode,
extension: mockExtension,
updateAttributes: mockUpdateAttributes,
editor: {
isEditable: false,
},
} as unknown as NodeViewProps;
await act(async () => {
render(<CalloutComponent {...nodeViewProps} />);
});
const calloutButton = screen.getByTestId('callout-info-btn');
await act(async () => {
userEvent.click(calloutButton);
});
const popover = screen.queryByRole('tooltip');
expect(popover).not.toBeInTheDocument();
});
});

View File

@ -45,12 +45,18 @@ const CalloutComponent: FC<NodeViewProps> = ({
node,
extension,
updateAttributes,
editor,
}) => {
const { calloutType } = node.attrs;
const [isPopupVisible, setIsPopupVisible] = useState<boolean>(false);
const CallOutIcon =
CALLOUT_CONTENT[calloutType as keyof typeof CALLOUT_CONTENT];
const handlePopoverVisibleChange = (visible: boolean) => {
// Only show the popover when the editor is in editable mode
setIsPopupVisible(visible && editor.isEditable);
};
return (
<NodeViewWrapper as="div" className="om-react-node">
<div
@ -73,7 +79,7 @@ const CalloutComponent: FC<NodeViewProps> = ({
placement="bottomRight"
showArrow={false}
trigger="click"
onOpenChange={setIsPopupVisible}>
onOpenChange={handlePopoverVisibleChange}>
<Button
className="callout-type-btn"
data-testid={`callout-${calloutType}-btn`}

View File

@ -25,6 +25,7 @@ import './math-equation.less';
export const MathEquationComponent: FC<NodeViewProps> = ({
node,
updateAttributes,
editor,
}) => {
const { t } = useTranslation();
const inputRef = React.useRef<TextAreaRef>(null);
@ -78,7 +79,8 @@ export const MathEquationComponent: FC<NodeViewProps> = ({
) : (
<Latex>{equation}</Latex>
)}
{!isEditing && (
{/* Show edit button only when the editor is editable */}
{!isEditing && editor.isEditable && (
<Tooltip
title={t('label.edit-entity', { entity: t('label.equation') })}>
<Button

View File

@ -26,10 +26,15 @@ const mockNode = {
const mockDeleteNode = jest.fn();
const mockUpdateAttributes = jest.fn();
const mockEditor = {
isEditable: true,
};
const mockNodeViewProps = {
node: mockNode,
updateAttributes: mockUpdateAttributes,
deleteNode: mockDeleteNode,
editor: mockEditor,
} as unknown as NodeViewProps;
describe('ImageComponent', () => {
@ -68,6 +73,33 @@ describe('ImageComponent', () => {
expect(screen.getByText('label.embed-link')).toBeInTheDocument();
});
it("should not render the popover when image node is clicked and the editor isn't editable", async () => {
const nonEditableEditor = {
isEditable: false,
};
const nonEditableNodeViewProps = {
node: mockNode,
updateAttributes: mockUpdateAttributes,
deleteNode: mockDeleteNode,
editor: nonEditableEditor,
} as unknown as NodeViewProps;
await act(async () => {
render(<ImageComponent {...nonEditableNodeViewProps} />);
});
const imageNode = screen.getByTestId('uploaded-image-node');
await act(async () => {
userEvent.click(imageNode);
});
const popover = screen.queryByRole('tooltip');
expect(popover).not.toBeInTheDocument();
});
it('should render the upload tab by default', async () => {
await act(async () => {
render(<ImageComponent {...mockNodeViewProps} />);

View File

@ -173,6 +173,7 @@ const ImageComponent: FC<NodeViewProps> = ({
node,
updateAttributes,
deleteNode,
editor,
}) => {
const { t } = useTranslation();
const { src, alt } = node.attrs;
@ -181,6 +182,11 @@ const ImageComponent: FC<NodeViewProps> = ({
const [isUploading, setIsUploading] = useState<boolean>(false);
const [isPopupVisible, setIsPopupVisible] = useState<boolean>(!isValidSource);
const handlePopoverVisibleChange = (visible: boolean) => {
// Only show the popover when the editor is in editable mode
setIsPopupVisible(visible && editor.isEditable);
};
return (
<NodeViewWrapper as="div" className="om-react-node">
<div className={classNames({ 'om-image-node-wrapper': isPopupVisible })}>
@ -203,7 +209,7 @@ const ImageComponent: FC<NodeViewProps> = ({
placement="bottom"
showArrow={false}
trigger="click"
onOpenChange={setIsPopupVisible}>
onOpenChange={handlePopoverVisibleChange}>
{isValidSource ? (
<div className="om-image-node-uploaded">
<img

View File

@ -26,7 +26,7 @@ interface TableMenuProps {
const TableMenu = (props: TableMenuProps) => {
const { editor } = props;
const { view } = editor;
const { view, isEditable } = editor;
const menuRef = useRef<HTMLDivElement>(null);
const tableMenuPopup = useRef<Instance | null>(null);
@ -44,7 +44,7 @@ const TableMenu = (props: TableMenuProps) => {
}, []);
useEffect(() => {
if (menuRef.current) {
if (menuRef.current && isEditable) {
menuRef.current.remove();
menuRef.current.style.visibility = 'visible';
@ -67,7 +67,7 @@ const TableMenu = (props: TableMenuProps) => {
tableMenuPopup.current?.destroy();
tableMenuPopup.current = null;
};
}, []);
}, [isEditable]);
useEffect(() => {
document.addEventListener('mousedown', handleMouseDown);