Feat: raw editor ingress and egress logic (#179)
* feat: conditional rendering of olx editor. * fix: open the raw editor if advanced is chosen * fix: add test fix * feat: add button to switch visual->advanced * fix: add tests + lint for visual->advanced button * feat: revert to advanced if parser fails * fix: improve coverage * feat: add confirm dialog to switch * fix: load settings with advanced * fix: refactor + lint fix
This commit is contained in:
@@ -77,6 +77,11 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page 1`] = `
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
</Row>
|
||||
<Row
|
||||
className="my-2"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
</Row>
|
||||
</Body>
|
||||
</Advanced>
|
||||
</Component>
|
||||
@@ -163,6 +168,11 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page advanced
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
</Row>
|
||||
<Row
|
||||
className="my-2"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
</Row>
|
||||
</Body>
|
||||
</Advanced>
|
||||
</Component>
|
||||
|
||||
@@ -13,6 +13,7 @@ import ResetCard from './settingsComponents/ResetCard';
|
||||
import MatlabCard from './settingsComponents/MatlabCard';
|
||||
import TimerCard from './settingsComponents/TimerCard';
|
||||
import TypeCard from './settingsComponents/TypeCard';
|
||||
import SwitchToAdvancedEditorCard from './settingsComponents/SwitchToAdvancedEditorCard';
|
||||
import messages from './messages';
|
||||
import { showAdvancedSettingsCards } from './hooks';
|
||||
|
||||
@@ -75,6 +76,9 @@ export const SettingsWidget = ({
|
||||
<Row className="my-2">
|
||||
<MatlabCard matLabApiKey={settings.matLabApiKey} updateSettings={updateSettings} />
|
||||
</Row>
|
||||
<Row className="my-2">
|
||||
<SwitchToAdvancedEditorCard />
|
||||
</Row>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
</Col>
|
||||
|
||||
@@ -149,6 +149,26 @@ export const messages = {
|
||||
defaultMessage: 'Type',
|
||||
description: 'Type settings card title',
|
||||
},
|
||||
SwitchButtonLabel: {
|
||||
id: 'authoring.problemeditor.settings.switchtoadvancededitor.label',
|
||||
defaultMessage: 'Switch To Advanced Editor',
|
||||
description: 'button to switch to the advanced mode of the editor.',
|
||||
},
|
||||
ConfirmSwitchMessage: {
|
||||
id: 'authoring.problemeditor.settings.switchtoadvancededitor.message',
|
||||
defaultMessage: 'If you use the advanced editor, this problem will be converted to OLX and you will not be able to return to the simple editor.',
|
||||
description: 'message to confirm that a user wants to use the advanced editor',
|
||||
},
|
||||
ConfirmSwitchMessageTitle: {
|
||||
id: 'authoring.problemeditor.settings.switchtoadvancededitor.message',
|
||||
defaultMessage: 'Convert to OLX?',
|
||||
description: 'message to confirm that a user wants to use the advanced editor',
|
||||
},
|
||||
ConfirmSwitchButtonLabel: {
|
||||
id: 'authoring.problemeditor.settings.switchtoadvancededitor.message',
|
||||
defaultMessage: 'Switch To Advanced Editor',
|
||||
description: 'message to confirm that a user wants to use the advanced editor',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Card } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import messages from '../messages';
|
||||
import { thunkActions } from '../../../../../../data/redux';
|
||||
import BaseModal from '../../../../../TextEditor/components/BaseModal';
|
||||
|
||||
export const SwitchToAdvancedEditorCard = ({
|
||||
switchToAdvancedEditor,
|
||||
}) => {
|
||||
const [isConfirmOpen, setConfirmOpen] = React.useState(false);
|
||||
return (
|
||||
<Card>
|
||||
<BaseModal
|
||||
isOpen={isConfirmOpen}
|
||||
close={() => { setConfirmOpen(false); }}
|
||||
title={(<FormattedMessage {...messages.ConfirmSwitchMessageTitle} />)}
|
||||
confirmAction={(
|
||||
<Button
|
||||
onClick={switchToAdvancedEditor}
|
||||
>
|
||||
<FormattedMessage {...messages.ConfirmSwitchButtonLabel} />
|
||||
</Button>
|
||||
)}
|
||||
size="md"
|
||||
>
|
||||
<FormattedMessage {...messages.ConfirmSwitchMessage} />
|
||||
</BaseModal>
|
||||
<Button
|
||||
className="my-3 ml-2"
|
||||
variant="link"
|
||||
size="inline"
|
||||
onClick={() => { setConfirmOpen(true); }}
|
||||
>
|
||||
<FormattedMessage {...messages.SwitchButtonLabel} />
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
SwitchToAdvancedEditorCard.propTypes = {
|
||||
switchToAdvancedEditor: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = () => ({
|
||||
});
|
||||
export const mapDispatchToProps = {
|
||||
switchToAdvancedEditor: thunkActions.problem.switchToAdvancedEditor,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SwitchToAdvancedEditorCard));
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { SwitchToAdvancedEditorCard } from './SwitchToAdvancedEditorCard';
|
||||
|
||||
describe('SwitchToAdvancedEditorCard snapshot', () => {
|
||||
const mockSwitchToAdvancedEditor = jest.fn().mockName('switchToAdvancedEditor');
|
||||
test('snapshot: SwitchToAdvancedEditorCard', () => {
|
||||
expect(
|
||||
shallow(<SwitchToAdvancedEditorCard switchToAdvancedEditor={mockSwitchToAdvancedEditor} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwitchToAdvancedEditorCard snapshot snapshot: SwitchToAdvancedEditorCard 1`] = `
|
||||
<Card>
|
||||
<BaseModal
|
||||
close={[Function]}
|
||||
confirmAction={
|
||||
<Button
|
||||
onClick={[MockFunction switchToAdvancedEditor]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Switch To Advanced Editor"
|
||||
description="message to confirm that a user wants to use the advanced editor"
|
||||
id="authoring.problemeditor.settings.switchtoadvancededitor.message"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
isOpen={false}
|
||||
size="md"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Convert to OLX?"
|
||||
description="message to confirm that a user wants to use the advanced editor"
|
||||
id="authoring.problemeditor.settings.switchtoadvancededitor.message"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="If you use the advanced editor, this problem will be converted to OLX and you will not be able to return to the simple editor."
|
||||
description="message to confirm that a user wants to use the advanced editor"
|
||||
id="authoring.problemeditor.settings.switchtoadvancededitor.message"
|
||||
/>
|
||||
</BaseModal>
|
||||
<Button
|
||||
className="my-3 ml-2"
|
||||
onClick={[Function]}
|
||||
size="inline"
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Switch To Advanced Editor"
|
||||
description="button to switch to the advanced mode of the editor."
|
||||
id="authoring.problemeditor.settings.switchtoadvancededitor.label"
|
||||
/>
|
||||
</Button>
|
||||
</Card>
|
||||
`;
|
||||
@@ -10,7 +10,7 @@ import { EditorContainer } from '../../../EditorContainer';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import ReactStateSettingsParser from '../../data/ReactStateSettingsParser';
|
||||
import ReactStateOLXParser from '../../data/ReactStateOLXParser';
|
||||
import { AdvanceProblemKeys } from '../../../../data/constants/problem';
|
||||
import { ProblemTypeKeys } from '../../../../data/constants/problem';
|
||||
|
||||
export const EditProblemView = ({
|
||||
problemType,
|
||||
@@ -24,8 +24,8 @@ export const EditProblemView = ({
|
||||
olx: reactOLXParser.buildOLX(),
|
||||
};
|
||||
};
|
||||
if (Object.values(AdvanceProblemKeys).includes(problemType)) {
|
||||
return `hello raw editor with ${problemType}`;
|
||||
if (problemType === ProblemTypeKeys.ADVANCED) {
|
||||
return 'hello raw editor';
|
||||
}
|
||||
return (
|
||||
<EditorContainer getContent={parseState(problemState)}>
|
||||
|
||||
@@ -19,7 +19,8 @@ export const selectHooks = () => {
|
||||
|
||||
export const onSelect = (setProblemType, selected, updateField) => () => {
|
||||
if (Object.values(AdvanceProblemKeys).includes(selected)) {
|
||||
updateField({ rawOLX: AdvanceProblems[selected].template });
|
||||
updateField({ problemType: ProblemTypeKeys.ADVANCED, rawOLX: AdvanceProblems[selected].template });
|
||||
return;
|
||||
}
|
||||
setProblemType({ selected });
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('SelectTypeModal hooks', () => {
|
||||
test('updateField is called with selected templated if selected is an Advanced Problem', () => {
|
||||
module.onSelect(mockSetProblemType, mockAdvancedSelected, mockUpdateField)();
|
||||
expect(mockUpdateField).toHaveBeenCalledWith({
|
||||
problemType: ProblemTypeKeys.ADVANCED,
|
||||
rawOLX: AdvanceProblems[mockAdvancedSelected].template,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -331,10 +331,16 @@ export class OLXParser {
|
||||
getProblemType() {
|
||||
const problemKeys = Object.keys(this.problem);
|
||||
const intersectedProblems = _.intersection(Object.values(ProblemTypeKeys), problemKeys);
|
||||
|
||||
if (intersectedProblems.length === 0) {
|
||||
return null;
|
||||
// a blank problem is a problem which contains only `<problem></problem>` as it's olx.
|
||||
// blank problems are not given types, so that a type may be selected.
|
||||
if (problemKeys.length === 1 && problemKeys[0] === '#text' && this.problem[problemKeys[0]] === '') {
|
||||
return null;
|
||||
}
|
||||
// if we have no matching problem type, the problem is advanced.
|
||||
return ProblemTypeKeys.ADVANCED;
|
||||
}
|
||||
// make sure compound problems are treated as advanced
|
||||
if (intersectedProblems.length > 1) {
|
||||
return ProblemTypeKeys.ADVANCED;
|
||||
}
|
||||
@@ -369,7 +375,10 @@ export class OLXParser {
|
||||
answersObject = this.parseMultipleChoiceAnswers(ProblemTypeKeys.SINGLESELECT, 'choicegroup', 'choice');
|
||||
break;
|
||||
case ProblemTypeKeys.ADVANCED:
|
||||
break;
|
||||
return {
|
||||
problemType,
|
||||
settings: {},
|
||||
};
|
||||
default:
|
||||
// if problem is unset, return null
|
||||
return {};
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
textInputWithFeedbackAndHintsOLX,
|
||||
mutlipleChoiceWithFeedbackAndHintsOLX,
|
||||
textInputWithFeedbackAndHintsOLXWithMultipleAnswers,
|
||||
advancedProblemOlX,
|
||||
blankProblemOLX,
|
||||
} from './mockData/olxTestData';
|
||||
import { ProblemTypeKeys } from '../../../data/constants/problem';
|
||||
|
||||
@@ -36,6 +38,16 @@ describe('Check OLXParser problem type', () => {
|
||||
const problemType = olxparser.getProblemType();
|
||||
expect(problemType).toBe(ProblemTypeKeys.TEXTINPUT);
|
||||
});
|
||||
test('Test Advanced Problem Type', () => {
|
||||
const olxparser = new OLXParser(advancedProblemOlX.rawOLX);
|
||||
const problemType = olxparser.getProblemType();
|
||||
expect(problemType).toBe(ProblemTypeKeys.ADVANCED);
|
||||
});
|
||||
test('Test Blank Problem Type', () => {
|
||||
const olxparser = new OLXParser(blankProblemOLX.rawOLX);
|
||||
const problemType = olxparser.getProblemType();
|
||||
expect(problemType).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Check OLXParser hints', () => {
|
||||
|
||||
@@ -553,3 +553,17 @@ export const numericInputWithFeedbackAndHintsOLXException = {
|
||||
</problem>
|
||||
`,
|
||||
};
|
||||
export const advancedProblemOlX = {
|
||||
rawOLX: `<problem>
|
||||
<formularesponse type="ci" samples="R_1,R_2,R_3@1,2,3:3,4,5#10" answer="R_1*R_2/R_3">
|
||||
<p>You can use this template as a guide to the OLX markup to use for math expression problems. Edit this component to replace the example with your own assessment.</p>
|
||||
<label>Add the question text, or prompt, here. This text is required. Example: Write an expression for the product of R_1, R_2, and the inverse of R_3.</label>
|
||||
<description>You can add an optional tip or note related to the prompt like this. Example: To test this example, the correct answer is R_1*R_2/R_3</description>
|
||||
<responseparam type="tolerance" default="0.00001"/>
|
||||
<formulaequationinput size="40"/>
|
||||
</formularesponse>
|
||||
</problem>`,
|
||||
};
|
||||
export const blankProblemOLX = {
|
||||
rawOLX: '<problem></problem>',
|
||||
};
|
||||
|
||||
@@ -17,8 +17,6 @@ export const ProblemEditor = ({
|
||||
blockValue,
|
||||
initializeProblemEditor,
|
||||
}) => {
|
||||
React.useEffect(() => initializeProblemEditor(blockValue), [blockValue]);
|
||||
// TODO: INTL MSG, Add LOAD FAILED ERROR using BLOCKFAILED
|
||||
if (!blockFinished || !studioViewFinished) {
|
||||
return (
|
||||
<div className="text-center p-6">
|
||||
@@ -31,6 +29,8 @@ export const ProblemEditor = ({
|
||||
);
|
||||
}
|
||||
// once data is loaded, init store
|
||||
React.useEffect(() => initializeProblemEditor(blockValue), [blockValue]);
|
||||
// TODO: INTL MSG, Add LOAD FAILED ERROR using BLOCKFAILED
|
||||
|
||||
if (problemType === null) {
|
||||
return (<SelectTypeModal onClose={onClose} />);
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
import _ from 'lodash-es';
|
||||
import * as requests from './requests';
|
||||
import { actions } from '..';
|
||||
import { OLXParser } from '../../../containers/ProblemEditor/data/OLXParser';
|
||||
import { parseSettings } from '../../../containers/ProblemEditor/data/SettingsParser';
|
||||
import { ProblemTypeKeys } from '../../constants/problem';
|
||||
import ReactStateOLXParser from '../../../containers/ProblemEditor/data/ReactStateOLXParser';
|
||||
import { blankProblemOLX } from '../../../containers/ProblemEditor/data/mockData/olxTestData';
|
||||
|
||||
export const switchToAdvancedEditor = () => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const reactOLXParser = new ReactStateOLXParser({ problem: state.problem });
|
||||
const rawOlx = reactOLXParser.buildOLX();
|
||||
dispatch(actions.problem.updateField({ problemType: ProblemTypeKeys.ADVANCED, rawOlx }));
|
||||
};
|
||||
|
||||
export const isBlankProblem = ({ rawOLX }) => {
|
||||
if (rawOLX === blankProblemOLX.rawOLX) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getDataFromOlx = ({ rawOLX, rawSettings }) => {
|
||||
let olxParser;
|
||||
let parsedProblem;
|
||||
try {
|
||||
olxParser = new OLXParser(rawOLX);
|
||||
parsedProblem = olxParser.getParsedOLXData();
|
||||
} catch {
|
||||
console.error('The Problem Could Not Be Parsed from OLX. redirecting to Advanced editor.');
|
||||
return { problemType: ProblemTypeKeys.ADVANCED, rawOLX, settings: parseSettings(rawSettings) };
|
||||
}
|
||||
if (parsedProblem?.problemType === ProblemTypeKeys.ADVANCED) {
|
||||
return { problemType: ProblemTypeKeys.ADVANCED, rawOLX, settings: parseSettings(rawSettings) };
|
||||
}
|
||||
const { settings, ...data } = parsedProblem;
|
||||
const parsedSettings = { ...settings, ...parseSettings(rawSettings) };
|
||||
if (!_.isEmpty(rawOLX) && !_.isEmpty(data)) {
|
||||
return { ...data, rawOLX, settings: parsedSettings };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const initializeProblem = (blockValue) => (dispatch) => {
|
||||
const rawOLX = _.get(blockValue, 'data.data', {});
|
||||
const olxParser = new OLXParser(rawOLX);
|
||||
const rawSettings = _.get(blockValue, 'data.metadata', {});
|
||||
|
||||
const parsedProblem = olxParser.getParsedOLXData();
|
||||
if (_.isEmpty(parsedProblem)) {
|
||||
// if problem is blank, enable selection.
|
||||
if (isBlankProblem({ rawOLX })) {
|
||||
dispatch(actions.problem.setEnableTypeSelection());
|
||||
} else {
|
||||
dispatch(actions.problem.load(getDataFromOlx({ rawOLX, rawSettings })));
|
||||
}
|
||||
const { settings, ...data } = parsedProblem;
|
||||
const parsedSettings = { ...settings, ...parseSettings(_.get(blockValue, 'data.metadata', {})) };
|
||||
if (!_.isEmpty(rawOLX) && !_.isEmpty(data)) {
|
||||
dispatch(actions.problem.load({ ...data, rawOLX, settings: parsedSettings }));
|
||||
}
|
||||
dispatch(requests.fetchAdvanceSettings({
|
||||
onSuccess: (response) => {
|
||||
console.log(response);
|
||||
if (response.data.allow_unsupported_xblocks.value) {
|
||||
console.log(response.allow_unsupported_xblocks.value);
|
||||
}
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export default { initializeProblem };
|
||||
export default { initializeProblem, switchToAdvancedEditor };
|
||||
|
||||
51
src/editors/data/redux/thunkActions/problem.test.js
Normal file
51
src/editors/data/redux/thunkActions/problem.test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { actions } from '..';
|
||||
import { initializeProblem, switchToAdvancedEditor } from './problem';
|
||||
import { checkboxesOLXWithFeedbackAndHintsOLX, advancedProblemOlX, blankProblemOLX } from '../../../containers/ProblemEditor/data/mockData/olxTestData';
|
||||
import { ProblemTypeKeys } from '../../constants/problem';
|
||||
|
||||
const mockOlx = 'SOmEVALue';
|
||||
const mockBuildOlx = jest.fn(() => mockOlx);
|
||||
jest.mock('../../../containers/ProblemEditor/data/ReactStateOLXParser', () => jest.fn().mockImplementation(() => ({ buildOLX: mockBuildOlx })));
|
||||
|
||||
jest.mock('..', () => ({
|
||||
actions: {
|
||||
problem: {
|
||||
load: () => {},
|
||||
setEnableTypeSelection: () => {},
|
||||
updateField: (args) => args,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('problem thunkActions', () => {
|
||||
let dispatch;
|
||||
let getState;
|
||||
beforeEach(() => {
|
||||
dispatch = jest.fn((action) => ({ dispatch: action }));
|
||||
getState = jest.fn(() => ({
|
||||
problem: {
|
||||
},
|
||||
}));
|
||||
});
|
||||
test('initializeProblem visual Problem :', () => {
|
||||
const blockValue = { data: { data: checkboxesOLXWithFeedbackAndHintsOLX.rawOLX } };
|
||||
initializeProblem(blockValue)(dispatch);
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.problem.load());
|
||||
});
|
||||
test('initializeProblem advanced Problem', () => {
|
||||
const blockValue = { data: { data: advancedProblemOlX.rawOLX } };
|
||||
initializeProblem(blockValue)(dispatch);
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.problem.load());
|
||||
});
|
||||
test('initializeProblem blank Problem', () => {
|
||||
const blockValue = { data: { data: blankProblemOLX.rawOLX } };
|
||||
initializeProblem(blockValue)(dispatch);
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.problem.setEnableTypeSelection());
|
||||
});
|
||||
test('switchToAdvancedEditor visual Problem', () => {
|
||||
switchToAdvancedEditor()(dispatch, getState);
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
actions.problem.updateField({ problemType: ProblemTypeKeys.ADVANCED, rawOlx: mockOlx }),
|
||||
);
|
||||
});
|
||||
});
|
||||
1
www/package-lock.json
generated
1
www/package-lock.json
generated
@@ -26,6 +26,7 @@
|
||||
}
|
||||
},
|
||||
"..": {
|
||||
"name": "@edx/frontend-lib-content-components",
|
||||
"version": "1.0.0-semantically-released",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user