feat: add labels and blockquotes to clear format (#261)
This commit is contained in:
@@ -10,11 +10,11 @@ import './index.scss';
|
||||
import TinyMceWidget from '../../../../../sharedComponents/TinyMceWidget';
|
||||
|
||||
export const QuestionWidget = ({
|
||||
assets,
|
||||
// redux
|
||||
isLibrary,
|
||||
question,
|
||||
updateQuestion,
|
||||
assets,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
// injected
|
||||
@@ -46,21 +46,20 @@ export const QuestionWidget = ({
|
||||
|
||||
QuestionWidget.defaultProps = {
|
||||
isLibrary: null,
|
||||
assets: null,
|
||||
};
|
||||
|
||||
QuestionWidget.propTypes = {
|
||||
assets: PropTypes.shape({}).isRequired,
|
||||
// redux
|
||||
isLibrary: PropTypes.bool,
|
||||
lmsEndpointUrl: PropTypes.string.isRequired,
|
||||
studioEndpointUrl: PropTypes.string.isRequired,
|
||||
assets: PropTypes.shape({}),
|
||||
question: PropTypes.string.isRequired,
|
||||
updateQuestion: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
assets: selectors.app.assets(state),
|
||||
isLibrary: selectors.app.isLibrary(state),
|
||||
question: selectors.problem.question(state),
|
||||
lmsEndpointUrl: selectors.app.lmsEndpointUrl(state),
|
||||
|
||||
@@ -16,7 +16,6 @@ jest.mock('../../../../../data/redux', () => ({
|
||||
isLibrary: jest.fn(state => ({ isLibrary: state })),
|
||||
lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })),
|
||||
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
|
||||
assets: jest.fn(state => ({ assets: state })),
|
||||
},
|
||||
problem: {
|
||||
question: jest.fn(state => ({ question: state })),
|
||||
@@ -53,11 +52,6 @@ describe('QuestionWidget', () => {
|
||||
test('isLibrary from app.isLibrary', () => {
|
||||
expect(mapStateToProps(testState).isLibrary).toEqual(selectors.app.isLibrary(testState));
|
||||
});
|
||||
test('assets from app.assets', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).assets,
|
||||
).toEqual(selectors.app.assets(testState));
|
||||
});
|
||||
test('question from problem.question', () => {
|
||||
expect(mapStateToProps(testState).question).toEqual(selectors.problem.question(testState));
|
||||
});
|
||||
|
||||
@@ -42,7 +42,9 @@ exports[`EditorProblemView component renders simple view 1`] = `
|
||||
<span
|
||||
className="flex-grow-1"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
assets={Object {}}
|
||||
/>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
problemType="multiplechoiceresponse"
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import ReactStateSettingsParser from '../../data/ReactStateSettingsParser';
|
||||
import ReactStateOLXParser from '../../data/ReactStateOLXParser';
|
||||
import { setAssetToStaticUrl } from '../../../../sharedComponents/TinyMceWidget/hooks';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const parseState = (problem, isAdvanced, ref) => () => {
|
||||
export const parseState = ({
|
||||
problem,
|
||||
isAdvanced,
|
||||
ref,
|
||||
assets,
|
||||
}) => () => {
|
||||
const reactSettingsParser = new ReactStateSettingsParser(problem);
|
||||
const reactOLXParser = new ReactStateOLXParser({ problem });
|
||||
const reactBuiltOlx = setAssetToStaticUrl({ editorValue: reactOLXParser.buildOLX(), assets });
|
||||
const rawOLX = ref?.current?.state.doc.toString();
|
||||
|
||||
return {
|
||||
settings: reactSettingsParser.getSettings(),
|
||||
olx: isAdvanced ? rawOLX : reactOLXParser.buildOLX(),
|
||||
olx: isAdvanced ? rawOLX : reactBuiltOlx,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,11 +15,21 @@ describe('EditProblemView hooks parseState', () => {
|
||||
const refMock = { current: { state: { doc: { toString: toStringMock } } } };
|
||||
|
||||
test('default problem', () => {
|
||||
const res = hooks.parseState('problem', false, refMock)();
|
||||
const res = hooks.parseState({
|
||||
problem: 'problem',
|
||||
isAdvanced: false,
|
||||
ref: refMock,
|
||||
assets: {},
|
||||
})();
|
||||
expect(res.olx).toBe(mockBuiltOLX);
|
||||
});
|
||||
test('advanced problem', () => {
|
||||
const res = hooks.parseState('problem', true, refMock)();
|
||||
const res = hooks.parseState({
|
||||
problem: 'problem',
|
||||
isAdvanced: true,
|
||||
ref: refMock,
|
||||
assets: {},
|
||||
})();
|
||||
expect(res.olx).toBe(mockRawOLX);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,11 +19,17 @@ export const EditProblemView = ({
|
||||
// redux
|
||||
problemType,
|
||||
problemState,
|
||||
assets,
|
||||
}) => {
|
||||
const editorRef = useRef(null);
|
||||
const isAdvancedProblemType = problemType === ProblemTypeKeys.ADVANCED;
|
||||
|
||||
const getContent = parseState(problemState, isAdvancedProblemType, editorRef);
|
||||
const getContent = parseState({
|
||||
problem: problemState,
|
||||
isAdvanced: isAdvancedProblemType,
|
||||
ref: editorRef,
|
||||
assets,
|
||||
});
|
||||
|
||||
return (
|
||||
<EditorContainer getContent={getContent}>
|
||||
@@ -34,7 +40,7 @@ export const EditProblemView = ({
|
||||
</Container>
|
||||
) : (
|
||||
<span className="flex-grow-1">
|
||||
<QuestionWidget />
|
||||
<QuestionWidget assets={assets} />
|
||||
<AnswerWidget problemType={problemType} />
|
||||
</span>
|
||||
)}
|
||||
@@ -46,13 +52,19 @@ export const EditProblemView = ({
|
||||
);
|
||||
};
|
||||
|
||||
EditProblemView.defaultProps = {
|
||||
assets: null,
|
||||
};
|
||||
|
||||
EditProblemView.propTypes = {
|
||||
problemType: PropTypes.string.isRequired,
|
||||
// eslint-disable-next-line
|
||||
problemState: PropTypes.any.isRequired,
|
||||
assets: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
assets: selectors.app.assets(state),
|
||||
problemType: selectors.problem.problemType(state),
|
||||
problemState: selectors.problem.completeState(state),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { EditProblemView } from '.';
|
||||
import AnswerWidget from './AnswerWidget';
|
||||
@@ -9,13 +10,15 @@ describe('EditorProblemView component', () => {
|
||||
const wrapper = shallow(<EditProblemView
|
||||
problemType={ProblemTypeKeys.SINGLESELECT}
|
||||
problemState={{}}
|
||||
assets={{}}
|
||||
/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find(AnswerWidget).length).toBe(1);
|
||||
expect(wrapper.find(RawEditor).length).toBe(0);
|
||||
});
|
||||
|
||||
test('renders raw editor', () => {
|
||||
const wrapper = shallow(<EditProblemView problemType={ProblemTypeKeys.ADVANCED} problemState={{}} />);
|
||||
const wrapper = shallow(<EditProblemView problemType={ProblemTypeKeys.ADVANCED} problemState={{}} assets={{}} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find(AnswerWidget).length).toBe(0);
|
||||
expect(wrapper.find(RawEditor).length).toBe(1);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import { StrictDict } from '../../utils';
|
||||
import * as appHooks from '../../hooks';
|
||||
import * as module from './hooks';
|
||||
import { setAssetToStaticUrl } from '../../sharedComponents/TinyMceWidget/hooks';
|
||||
|
||||
export const { nullMethod, navigateCallback, navigateTo } = appHooks;
|
||||
|
||||
@@ -22,41 +23,6 @@ export const prepareEditorRef = () => {
|
||||
return { editorRef, refReady, setEditorRef };
|
||||
};
|
||||
|
||||
export const setAssetToStaticUrl = ({ editorValue, assets }) => {
|
||||
/* For assets to remain usable across course instances, we convert their url to be course-agnostic.
|
||||
* For example, /assets/course/<asset hash>/filename gets converted to /static/filename. This is
|
||||
* important for rerunning courses and importing/exporting course as the /static/ part of the url
|
||||
* allows the asset to be mapped to the new course run.
|
||||
*/
|
||||
let content = editorValue;
|
||||
const assetUrls = [];
|
||||
const assetsList = Object.values(assets);
|
||||
assetsList.forEach(asset => {
|
||||
assetUrls.push({ portableUrl: asset.portableUrl, displayName: asset.displayName });
|
||||
});
|
||||
const assetSrcs = typeof content === 'string' ? content.split(/(src="|href=")/g) : [];
|
||||
assetSrcs.forEach(src => {
|
||||
if (src.startsWith('/asset') && assetUrls.length > 0) {
|
||||
const assetBlockName = src.substring(src.indexOf('@') + 1, src.indexOf('"'));
|
||||
const nameFromEditorSrc = assetBlockName.substring(assetBlockName.indexOf('@') + 1);
|
||||
const nameFromStudioSrc = assetBlockName.substring(assetBlockName.indexOf('/') + 1);
|
||||
let portableUrl;
|
||||
assetUrls.forEach((url) => {
|
||||
const displayName = url.displayName.replace(/\s/g, '_');
|
||||
if (displayName === nameFromEditorSrc || displayName === nameFromStudioSrc) {
|
||||
portableUrl = url.portableUrl;
|
||||
}
|
||||
});
|
||||
if (portableUrl) {
|
||||
const currentSrc = src.substring(0, src.indexOf('"'));
|
||||
const updatedContent = content.replace(currentSrc, portableUrl);
|
||||
content = updatedContent;
|
||||
}
|
||||
}
|
||||
});
|
||||
return content;
|
||||
};
|
||||
|
||||
export const getContent = ({ editorRef, isRaw, assets }) => () => {
|
||||
const content = (isRaw && editorRef && editorRef.current
|
||||
? editorRef.current.state.doc.toString()
|
||||
|
||||
@@ -63,18 +63,6 @@ describe('TextEditor hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAssetToStaticUrl', () => {
|
||||
it('returns content with updated img links', () => {
|
||||
const editorValue = '<img src="/asset@asset-block/soME_ImagE_URl1"/> <a href="/asset@soMEImagEURl">testing link</a>';
|
||||
const assets = [
|
||||
{ portableUrl: '/static/soMEImagEURl', displayName: 'soMEImagEURl' },
|
||||
{ portableUrl: '/static/soME_ImagE_URl1', displayName: 'soME ImagE URl1' },
|
||||
];
|
||||
const content = module.setAssetToStaticUrl({ editorValue, assets });
|
||||
expect(content).toEqual('<img src="/static/soME_ImagE_URl1"/> <a href="/static/soMEImagEURl">testing link</a>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContent', () => {
|
||||
const visualContent = 'sOmEViSualContent';
|
||||
const rawContent = 'soMeRawContent';
|
||||
|
||||
@@ -73,6 +73,7 @@ export const setupCustomBehavior = ({
|
||||
openSourceCodeModal,
|
||||
setImage,
|
||||
editorType,
|
||||
imageUrls,
|
||||
}) => (editor) => {
|
||||
// image upload button
|
||||
editor.ui.registry.addButton(tinyMCE.buttons.imageUploadButton, {
|
||||
@@ -129,13 +130,14 @@ export const setupCustomBehavior = ({
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const checkRelativeUrl = (imageUrls) => (editor) => {
|
||||
editor.on('ExecCommand', (e) => {
|
||||
if (e.command === 'mceFocus') {
|
||||
module.replaceStaticwithAsset(editor, imageUrls);
|
||||
}
|
||||
if (e.command === 'RemoveFormat') {
|
||||
editor.formatter.remove('blockquote');
|
||||
editor.formatter.remove('label');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -180,7 +182,6 @@ export const editorConfig = ({
|
||||
min_height: minHeight,
|
||||
contextmenu: 'link table',
|
||||
document_base_url: lmsEndpointUrl,
|
||||
init_instance_callback: module.checkRelativeUrl(module.fetchImageUrls(images)),
|
||||
imagetools_cors_hosts: [removeProtocolFromUrl(lmsEndpointUrl), removeProtocolFromUrl(studioEndpointUrl)],
|
||||
imagetools_toolbar: imageToolbar,
|
||||
formats: { label: { inline: 'label' } },
|
||||
@@ -190,6 +191,7 @@ export const editorConfig = ({
|
||||
openImgModal,
|
||||
openSourceCodeModal,
|
||||
setImage: setSelection,
|
||||
imageUrls: module.fetchImageUrls(images),
|
||||
}),
|
||||
toolbar,
|
||||
plugins,
|
||||
|
||||
@@ -120,14 +120,6 @@ describe('TinyMceEditor hooks', () => {
|
||||
expect(content).toEqual('<img src="/static/soME_ImagE_URl1"/> <a href="/static/soMEImagEURl">testing link</a>');
|
||||
});
|
||||
});
|
||||
describe('checkRelativeUrl', () => {
|
||||
test('it calls editor.on', () => {
|
||||
const editor = { on: jest.fn() };
|
||||
const imageUrls = ['soMEImagEURl1'];
|
||||
module.checkRelativeUrl(imageUrls)(editor);
|
||||
expect(editor.on).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('editorConfig', () => {
|
||||
const props = {
|
||||
@@ -211,10 +203,6 @@ describe('TinyMceEditor hooks', () => {
|
||||
expect(output.initialValue).toBe(textValue);
|
||||
});
|
||||
|
||||
// test('It configures plugins and toolbars correctly', () => {
|
||||
// expect(output.init.plugins).toEqual('autoresize');
|
||||
// expect(output.init.toolbar).toEqual(`${pluginConfig().toolbar} | customLabelButton`);
|
||||
// });
|
||||
it('calls setupCustomBehavior on setup', () => {
|
||||
expect(output.init.setup).toEqual(
|
||||
setupCustomBehavior({
|
||||
@@ -223,6 +211,7 @@ describe('TinyMceEditor hooks', () => {
|
||||
openImgModal: props.openImgModal,
|
||||
openSourceCodeModal: props.openSourceCodeModal,
|
||||
setImage: props.setSelection,
|
||||
imageUrls: module.fetchImageUrls(props.images),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user