Skip to content

Commit

Permalink
fix: limit insert link only for course sections
Browse files Browse the repository at this point in the history
  • Loading branch information
johnvente committed Feb 22, 2024
1 parent 08b1e75 commit 71172ae
Show file tree
Hide file tree
Showing 14 changed files with 204 additions and 93 deletions.
2 changes: 2 additions & 0 deletions src/editors/data/redux/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as requests from './requests';
import * as video from './video';
import * as problem from './problem';
import * as game from './game';
import * as insertlink from './insertlink';

/* eslint-disable import/no-cycle */
export { default as thunkActions } from './thunkActions';
Expand All @@ -17,6 +18,7 @@ const modules = {
video,
problem,
game,
insertlink,
};

const moduleProps = (propName) => Object.keys(modules).reduce(
Expand Down
2 changes: 2 additions & 0 deletions src/editors/data/redux/insertlink/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { actions, reducer } from './reducers';
export { default as selectors } from './selectors';
27 changes: 27 additions & 0 deletions src/editors/data/redux/insertlink/reducers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createSlice } from '@reduxjs/toolkit';
import { StrictDict } from '../../../utils';

const initialState = {
selectedBlocks: {},
};

// eslint-disable-next-line no-unused-vars
const insertlink = createSlice({
name: 'insertlink',
initialState,
reducers: {
addBlock: (state, { payload }) => {
state.selectedBlocks = { ...state.selectedBlocks, ...payload };
},
},
});

const actions = StrictDict(insertlink.actions);

const { reducer } = insertlink;

export {
actions,
initialState,
reducer,
};
28 changes: 28 additions & 0 deletions src/editors/data/redux/insertlink/reducers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { reducer, actions, initialState } from './reducers';

describe('insertlink reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(initialState);
});

it('should handle addBlock', () => {
const payload = {
block123: { id: 'block123', content: 'Block 123 content' },
block456: { id: 'block456', content: 'Block 456 content' },
};
const action = actions.addBlock(payload);

const previousState = {
selectedBlocks: { block789: { id: 'block789', content: 'Block 789 content' } },
};

const expectedState = {
selectedBlocks: {
...previousState.selectedBlocks,
...payload,
},
};

expect(reducer(previousState, action)).toEqual(expectedState);
});
});
5 changes: 5 additions & 0 deletions src/editors/data/redux/insertlink/selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const insertlinkState = (state) => state.insertlink;

export default {
insertlinkState,
};
19 changes: 19 additions & 0 deletions src/editors/data/redux/insertlink/selectors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { insertlinkState } from './selectors';

describe('insertlink selectors', () => {
describe('insertlinkState selector', () => {
it('should return the insertlink slice of the state', () => {
const state = {
insertlink: {
selectedBlocks: {
block123: { id: 'block123', url: 'https://www.example.com' },
block456: { id: 'block456', url: 'https://www.example.com' },
},
},
};

const { selectedBlocks } = insertlinkState(state);
expect(selectedBlocks).toEqual(state.insertlink.selectedBlocks);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,15 @@ const BlocksList = ({ blocks, onBlockSelected }) => {
<span className="w-100 text-left">{block.displayName}</span>
</Button>
{!isBlockSelectedUnit && (
<Button
variant="tertiary"
className="col-1 py-4"
onClick={() => handleSelectBlock(block, true)}
data-testid="block-navigation"
iconAfter={ArrowForwardIos}
>
<Button
variant="tertiary"
className="col-1 py-4"
onClick={() => handleSelectBlock(block, true)}
data-testid="block-navigation"
iconAfter={ArrowForwardIos}
>
&nbsp;
</Button>
</Button>
)}
</ActionRow>
</TransitionReplace>
Expand Down
103 changes: 75 additions & 28 deletions src/editors/sharedComponents/InsertLinkModal/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { logError } from '@edx/frontend-platform/logging';
import { useIntl } from '@edx/frontend-platform/i18n';
Expand All @@ -8,6 +9,7 @@ import {
Tab,
Form,
} from '@openedx/paragon';
import { actions, selectors } from '../../data/redux/insertlink';
import BaseModal from '../BaseModal';
import BlocksList from './BlocksList';
import BlockLink from './BlockLink';
Expand All @@ -29,8 +31,11 @@ const InsertLinkModal = ({
const [blocksSearched, setBlocksSearched] = useState(false);
const [blockSelected, setBlocksSelected] = useState(null);
const [blocksList, setBlocksList] = useState(null);
const [invalidUrlInput, setInvalidUrlInput] = useState(false);
const [, setInvalidUrlInput] = useState(false);
const [inputUrlValue, setInputUrlValue] = useState('');
const [errorUrlNotSelected, setErrorUrlNotSelected] = useState(false);
const dispatch = useDispatch();
const { selectedBlocks } = useSelector(selectors.insertlinkState);

const handleSearchedBlocks = (isSearched) => {
setBlocksSearched(isSearched);
Expand All @@ -46,14 +51,11 @@ const InsertLinkModal = ({
setBlocksSelected(null);

Check warning on line 51 in src/editors/sharedComponents/InsertLinkModal/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/editors/sharedComponents/InsertLinkModal/index.jsx#L50-L51

Added lines #L50 - L51 were not covered by tests
};

const handleChangeInputUrl = ({ target: { value } }) => {
setInputUrlValue(value);
};

/* istanbul ignore next */
const handleSave = () => {
const editor = editorRef.current;
const urlPath = blockSelected?.lmsWebUrl || inputUrlValue;
const blockId = blockSelected?.blockId;
if (editor && urlPath) {
const validateUrl = isValidURL(urlPath);

Expand All @@ -62,12 +64,26 @@ const InsertLinkModal = ({
return;
}

const selectedRange = editor.selection.getRng();
const selectedText = editor.selection.getContent({ format: 'text' });

if (selectedText.trim() !== '') {
const linkHtml = `<a href="${urlPath}" data-mce-href="${urlPath}" target="_blank">${selectedText}</a>`;
editor.selection.setContent(linkHtml);
}
const newLinkNode = editor.dom.create('a', {
href: urlPath,
'data-mce-href': urlPath,
'data-block-id': blockId,
target: '_blank',
});

newLinkNode.textContent = selectedText;

selectedRange.deleteContents();
selectedRange.insertNode(newLinkNode);
// Remove empty "a" tags after replacing URLs
const editorContent = editor.getContent();
const modifiedContent = editorContent.replace(/<a\b[^>]*><\/a>/gi, '');
editor.setContent(modifiedContent);

dispatch(actions.addBlock({ [blockId]: blockSelected }));
}

onClose();
Expand All @@ -91,13 +107,48 @@ const InsertLinkModal = ({
getBlocksList();
}, []);

useEffect(() => {
/* istanbul ignore next */
const editor = editorRef.current;
if (editor) {
const selectedHTML = editor.selection.getContent({ format: 'html' });
const regex = /data-block-id="([^"]+)"/;
const match = selectedHTML.match(regex);

// Extracting the value from the match
const dataBlockId = match ? match[1] : null;
if (selectedHTML && !dataBlockId) {
const selectedNode = editor.selection.getNode();
const parentNode = editor.dom.getParent(selectedNode, 'a');

Check warning on line 122 in src/editors/sharedComponents/InsertLinkModal/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/editors/sharedComponents/InsertLinkModal/index.jsx#L121-L122

Added lines #L121 - L122 were not covered by tests
if (parentNode) {
const dataBlockIdParent = parentNode.getAttribute('data-block-id');
const blockIsValid = dataBlockIdParent in selectedBlocks;

Check warning on line 125 in src/editors/sharedComponents/InsertLinkModal/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/editors/sharedComponents/InsertLinkModal/index.jsx#L124-L125

Added lines #L124 - L125 were not covered by tests
if (dataBlockIdParent && blockIsValid) {
setBlocksSelected(selectedBlocks[dataBlockIdParent]);

Check warning on line 127 in src/editors/sharedComponents/InsertLinkModal/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/editors/sharedComponents/InsertLinkModal/index.jsx#L127

Added line #L127 was not covered by tests
}
}
}

if (dataBlockId) {
const blockIsValid = dataBlockId in selectedBlocks;
if (dataBlockId && blockIsValid) {
setBlocksSelected(selectedBlocks[dataBlockId]);

Check warning on line 135 in src/editors/sharedComponents/InsertLinkModal/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/editors/sharedComponents/InsertLinkModal/index.jsx#L135

Added line #L135 was not covered by tests
}
}

if (!selectedHTML) {
setErrorUrlNotSelected(true);

Check warning on line 140 in src/editors/sharedComponents/InsertLinkModal/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/editors/sharedComponents/InsertLinkModal/index.jsx#L140

Added line #L140 was not covered by tests
}
}
}, []);

return (
<BaseModal
isOpen={isOpen}
close={onClose}
title={intl.formatMessage(messages.insertLinkModalTitle)}
confirmAction={(
<Button variant="primary" onClick={handleSave}>
<Button variant="primary" onClick={handleSave} disabled={errorUrlNotSelected}>
{intl.formatMessage(messages.insertLinkModalButtonSave)}
</Button>
)}
Expand All @@ -116,6 +167,12 @@ const InsertLinkModal = ({
title={intl.formatMessage(messages.insertLinkModalCoursePagesTabTitle)}
className="col-12 w-100 tabs-container"
>
{errorUrlNotSelected && (
<Form.Control.Feedback type="invalid" className="mt-4">

Check warning on line 171 in src/editors/sharedComponents/InsertLinkModal/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/editors/sharedComponents/InsertLinkModal/index.jsx#L171

Added line #L171 was not covered by tests
{intl.formatMessage(messages.insertLinkModalUrlNotSelectedErrorMessage)}
</Form.Control.Feedback>
)}

<SearchBlocks
blocks={blocksList || {}}
onSearchFilter={handleSearchedBlocks}
Expand All @@ -129,24 +186,6 @@ const InsertLinkModal = ({
/>
)}
</Tab>
<Tab
eventKey="url"
title={intl.formatMessage(messages.insertLinkModalUrlTabTitle)}
className="col-12 tabs-container"
>
<Form.Group isInvalid={invalidUrlInput} className="my-4">
<Form.Control
placeholder={intl.formatMessage(messages.insertLinkModalInputPlaceholder)}
onChange={handleChangeInputUrl}
data-testid="url-input"
/>
{invalidUrlInput && (
<Form.Control.Feedback type="invalid">
{intl.formatMessage(messages.insertLinkModalInputErrorMessage)}
</Form.Control.Feedback>
)}
</Form.Group>
</Tab>
</Tabs>
)}
</BaseModal>
Expand All @@ -162,6 +201,14 @@ InsertLinkModal.propTypes = {
selection: PropTypes.shape({
getContent: PropTypes.func,
setContent: PropTypes.func,
getRng: PropTypes.func, // Add this line
getNode: PropTypes.func, // Add this line
}),
getContent: PropTypes.func,
setContent: PropTypes.func,
dom: PropTypes.shape({
create: PropTypes.func,
getParent: PropTypes.func,
}),
}),
}).isRequired,
Expand Down
47 changes: 11 additions & 36 deletions src/editors/sharedComponents/InsertLinkModal/index.test.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { useSelector } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging';

Expand All @@ -25,6 +26,11 @@ jest.mock('./utils', () => ({
isValidURL: jest.fn(),
}));

jest.mock('react-redux', () => ({
useSelector: jest.fn(),
useDispatch: jest.fn(),
}));

jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@openedx/paragon');
jest.unmock('@openedx/paragon/icons');
Expand All @@ -37,7 +43,7 @@ describe('InsertLinkModal', () => {
editorRef: {
current: {
selection: {
getContent: jest.fn(),
getContent: () => '<a href="http://example.com" data-block-id="block123">Sample content</a>',
setContent: jest.fn(),
},
},
Expand All @@ -51,6 +57,7 @@ describe('InsertLinkModal', () => {

beforeEach(() => {
jest.clearAllMocks();
useSelector.mockReturnValue({ selectedBlocks: {} });
});

const renderComponent = (overrideProps = {}) => render(
Expand All @@ -69,14 +76,12 @@ describe('InsertLinkModal', () => {
expect(screen.getByText('Link to')).toBeInTheDocument();
});

test('should show Course pages and URL tabs', () => {
test('should show Course pages tab', () => {
renderComponent();

const tabs = screen.getAllByRole('tab');
const [coursePagesTab, urlTab] = tabs;

const [coursePagesTab] = tabs;
expect(coursePagesTab).toHaveTextContent('Course pages');
expect(urlTab).toHaveTextContent('URL');
});

test('should find Cancel and Save buttons', () => {
Expand All @@ -89,36 +94,6 @@ describe('InsertLinkModal', () => {
expect(saveButton).toBeInTheDocument();
});

test('should show input for url when URL tab is clicked', () => {
const { getByTestId } = renderComponent();

const tabs = screen.getAllByRole('tab');
const [, urlTab] = tabs;

fireEvent.click(urlTab);

const urlInput = getByTestId('url-input');
expect(urlInput).toBeInTheDocument();
});

test('should show message when the url is invalid', () => {
const { getByTestId, getByText } = renderComponent();

const tabs = screen.getAllByRole('tab');
const [, urlTab] = tabs;

fireEvent.click(urlTab);

const urlInput = getByTestId('url-input');
fireEvent.change(urlInput, { target: { value: 'invalid-url' } });

const saveButton = getByText('Save');
fireEvent.click(saveButton);

const errorMessage = getByText('The url provided is invalid');
expect(errorMessage).toBeInTheDocument();
});

test('should call logError on API error', async () => {
api.getBlocksFromCourse.mockRejectedValue(new Error('API error'));

Expand Down
Loading

0 comments on commit 71172ae

Please sign in to comment.