feat: add labels and blockquotes to clear format (#261)

This commit is contained in:
Kristin Aoki
2023-03-02 11:43:49 -05:00
committed by GitHub
parent 77f030c3fe
commit 5d77dddaf6
11 changed files with 53 additions and 81 deletions

View File

@@ -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),

View File

@@ -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));
});

View File

@@ -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"
/>

View File

@@ -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,
};
};

View File

@@ -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);
});
});

View File

@@ -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),
});

View File

@@ -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);

View File

@@ -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()

View File

@@ -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';

View File

@@ -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,

View File

@@ -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),
}),
);
});