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:
connorhaugh
2023-01-10 09:42:44 -05:00
committed by GitHub
parent 09bb1dab2b
commit 880d205cbb
16 changed files with 288 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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', () => {

View File

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

View File

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

View File

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

View 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
View File

@@ -26,6 +26,7 @@
}
},
"..": {
"name": "@edx/frontend-lib-content-components",
"version": "1.0.0-semantically-released",
"license": "AGPL-3.0",
"dependencies": {