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