feat: move explanation to main body of editor (#287)
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SolutionWidget render snapshot: renders correct default 1`] = `
|
||||
<div
|
||||
className="tinyMceWidget mt-4 text-primary-500"
|
||||
>
|
||||
<div
|
||||
className="h4 mb-3"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Explanation"
|
||||
description="Explanation Title"
|
||||
id="authoring.problemEditor.explanationwidget.explanationWidgetTitle"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="small mb-3"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Provide an explantion for the correct answer"
|
||||
description="Description of the solution widget"
|
||||
id="authoring.problemEditor.solutionwidget.solutionDescriptionText"
|
||||
/>
|
||||
</div>
|
||||
<[object Object]
|
||||
editorType="solution"
|
||||
id="solution"
|
||||
minHeight={150}
|
||||
placeholder="Enter your explanation"
|
||||
setEditorRef={[MockFunction prepareEditorRef.setEditorRef]}
|
||||
textValue="This is my question"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { selectors } from '../../../../../data/redux';
|
||||
import { messages } from './messages';
|
||||
|
||||
import TinyMceWidget from '../../../../../sharedComponents/TinyMceWidget';
|
||||
import { prepareEditorRef } from '../../../../../sharedComponents/TinyMceWidget/hooks';
|
||||
|
||||
export const ExplanationWidget = ({
|
||||
// redux
|
||||
settings,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
|
||||
if (!refReady) { return null; }
|
||||
return (
|
||||
<div className="tinyMceWidget mt-4 text-primary-500">
|
||||
<div className="h4 mb-3">
|
||||
<FormattedMessage {...messages.solutionWidgetTitle} />
|
||||
</div>
|
||||
<div className="small mb-3">
|
||||
<FormattedMessage {...messages.solutionDescriptionText} />
|
||||
</div>
|
||||
<TinyMceWidget
|
||||
id="solution"
|
||||
editorType="solution"
|
||||
editorRef={editorRef}
|
||||
textValue={settings?.solutionExplanation}
|
||||
setEditorRef={setEditorRef}
|
||||
minHeight={150}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ExplanationWidget.propTypes = {
|
||||
// redux
|
||||
// eslint-disable-next-line
|
||||
settings: PropTypes.any.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
settings: selectors.problem.settings(state),
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps)(ExplanationWidget));
|
||||
@@ -0,0 +1,40 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { formatMessage } from '../../../../../../testUtils';
|
||||
import { selectors } from '../../../../../data/redux';
|
||||
import { ExplanationWidget, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('../../../../../data/redux', () => ({
|
||||
selectors: {
|
||||
problem: {
|
||||
settings: jest.fn(state => ({ question: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({
|
||||
prepareEditorRef: jest.fn(() => ({
|
||||
refReady: true,
|
||||
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('SolutionWidget', () => {
|
||||
const props = {
|
||||
settings: { solutionExplanation: 'This is my question' },
|
||||
// injected
|
||||
intl: { formatMessage },
|
||||
};
|
||||
describe('render', () => {
|
||||
test('snapshot: renders correct default', () => {
|
||||
expect(shallow(<ExplanationWidget {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('question from problem.question', () => {
|
||||
expect(mapStateToProps(testState).settings).toEqual(selectors.problem.settings(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
export const messages = {
|
||||
solutionWidgetTitle: {
|
||||
id: 'authoring.problemEditor.explanationwidget.explanationWidgetTitle',
|
||||
defaultMessage: 'Explanation',
|
||||
description: 'Explanation Title',
|
||||
},
|
||||
solutionDescriptionText: {
|
||||
id: 'authoring.problemEditor.solutionwidget.solutionDescriptionText',
|
||||
defaultMessage: 'Provide an explantion for the correct answer',
|
||||
description: 'Description of the solution widget',
|
||||
},
|
||||
placeholder: {
|
||||
id: 'authoring.problemEditor.questionwidget.placeholder',
|
||||
defaultMessage: 'Enter your explanation',
|
||||
description: 'Placeholder text for tinyMCE editor',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`QuestionWidget render snapshot: renders correct default 1`] = `
|
||||
<div
|
||||
className="question-widget"
|
||||
className="tinyMceWidget"
|
||||
>
|
||||
<div
|
||||
className="h4 mb-3"
|
||||
|
||||
@@ -2,9 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { selectors, actions } from '../../../../../data/redux';
|
||||
import { selectors } from '../../../../../data/redux';
|
||||
import { messages } from './messages';
|
||||
import './index.scss';
|
||||
|
||||
import TinyMceWidget from '../../../../../sharedComponents/TinyMceWidget';
|
||||
import { prepareEditorRef } from '../../../../../sharedComponents/TinyMceWidget/hooks';
|
||||
@@ -18,7 +17,7 @@ export const QuestionWidget = ({
|
||||
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
|
||||
if (!refReady) { return null; }
|
||||
return (
|
||||
<div className="question-widget">
|
||||
<div className="tinyMceWidget">
|
||||
<div className="h4 mb-3">
|
||||
<FormattedMessage {...messages.questionWidgetTitle} />
|
||||
</div>
|
||||
@@ -45,8 +44,4 @@ export const mapStateToProps = (state) => ({
|
||||
question: selectors.problem.question(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
updateQuestion: actions.problem.updateQuestion,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(QuestionWidget));
|
||||
export default injectIntl(connect(mapStateToProps)(QuestionWidget));
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
.question-widget {
|
||||
.tox-tinymce {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.tox {
|
||||
.tox-toolbar__primary {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tox-statusbar {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.tox-toolbar__group:not(:last-of-type) {
|
||||
// TODO: Find a way to override the border without !important
|
||||
border-right: none !important;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: relative;
|
||||
left: 5px;
|
||||
border: 1px solid #eae6e5;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { formatMessage } from '../../../../../../testUtils';
|
||||
import { actions, selectors } from '../../../../../data/redux';
|
||||
import { QuestionWidget, mapStateToProps, mapDispatchToProps } from '.';
|
||||
import { selectors } from '../../../../../data/redux';
|
||||
import { QuestionWidget, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('../../../../../data/redux', () => ({
|
||||
actions: {
|
||||
@@ -49,9 +49,4 @@ describe('QuestionWidget', () => {
|
||||
expect(mapStateToProps(testState).question).toEqual(selectors.problem.question(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('updateField from actions.problem.updateQuestion', () => {
|
||||
expect(mapDispatchToProps.updateQuestion).toEqual(actions.problem.updateQuestion);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,14 +171,9 @@ export const useAnswerSettings = (showAnswer, updateSettings) => {
|
||||
updateSettings({ showAnswer: { ...showAnswer, afterAttempts: attempts } });
|
||||
};
|
||||
|
||||
const handleExplanationChange = (content) => {
|
||||
updateSettings({ solutionExplanation: content });
|
||||
};
|
||||
|
||||
return {
|
||||
handleShowAnswerChange,
|
||||
handleAttemptsChange,
|
||||
handleExplanationChange,
|
||||
showAttempts,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -236,11 +236,6 @@ describe('Problem settings hooks', () => {
|
||||
output.handleAttemptsChange({ target: { value } });
|
||||
expect(updateSettings).toHaveBeenCalledWith({ showAnswer: { ...showAnswer, afterAttempts: parseInt(value) } });
|
||||
});
|
||||
test('handleExplanationChange should update settings', () => {
|
||||
const value = 'explanation';
|
||||
output.handleExplanationChange(value);
|
||||
expect(updateSettings).toHaveBeenCalledWith({ solutionExplanation: value });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Timer card hooks', () => {
|
||||
|
||||
@@ -93,7 +93,6 @@ export const SettingsWidget = ({
|
||||
<ShowAnswerCard
|
||||
showAnswer={settings.showAnswer}
|
||||
updateSettings={updateSettings}
|
||||
solutionExplanation={settings.solutionExplanation}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
|
||||
@@ -8,11 +8,9 @@ import { ShowAnswerTypes, ShowAnswerTypesKeys } from '../../../../../../data/con
|
||||
import { selectors } from '../../../../../../data/redux';
|
||||
import messages from '../messages';
|
||||
import { useAnswerSettings } from '../hooks';
|
||||
import ExpandableTextArea from '../../../../../../sharedComponents/ExpandableTextArea';
|
||||
|
||||
export const ShowAnswerCard = ({
|
||||
showAnswer,
|
||||
solutionExplanation,
|
||||
updateSettings,
|
||||
// inject
|
||||
intl,
|
||||
@@ -23,7 +21,6 @@ export const ShowAnswerCard = ({
|
||||
const {
|
||||
handleShowAnswerChange,
|
||||
handleAttemptsChange,
|
||||
handleExplanationChange,
|
||||
showAttempts,
|
||||
} = useAnswerSettings(showAnswer, updateSettings);
|
||||
|
||||
@@ -69,28 +66,10 @@ export const ShowAnswerCard = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const explanationSection = (
|
||||
<>
|
||||
<div className="pb-3">
|
||||
<span>
|
||||
<FormattedMessage {...messages.explanationSettingText} />
|
||||
</span>
|
||||
</div>
|
||||
<ExpandableTextArea
|
||||
value={solutionExplanation}
|
||||
setContent={handleExplanationChange}
|
||||
id="solution"
|
||||
placeholder={intl.formatMessage(messages.explanationInputLabel)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsOption
|
||||
title={intl.formatMessage(messages.showAnswerSettingsTitle)}
|
||||
summary={intl.formatMessage(ShowAnswerTypes[showAnswer.on])}
|
||||
extraSections={[{ children: explanationSection }]}
|
||||
hasExpandableTextArea
|
||||
>
|
||||
{showAnswerSection}
|
||||
</SettingsOption>
|
||||
|
||||
@@ -3,33 +3,8 @@
|
||||
exports[`ShowAnswerCard snapshot snapshot: show answer setting card 1`] = `
|
||||
<SettingsOption
|
||||
className=""
|
||||
extraSections={
|
||||
Array [
|
||||
Object {
|
||||
"children": <React.Fragment>
|
||||
<div
|
||||
className="pb-3"
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Provide an explanation for the correct answer."
|
||||
description="Solution Explanation text"
|
||||
id="authoring.problemeditor.settings.showAnswer.explanation.text"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<ExpandableTextArea
|
||||
error={false}
|
||||
errorMessage={null}
|
||||
id="solution"
|
||||
placeholder="Explanation"
|
||||
value=""
|
||||
/>
|
||||
</React.Fragment>,
|
||||
},
|
||||
]
|
||||
}
|
||||
hasExpandableTextArea={true}
|
||||
extraSections={Array []}
|
||||
hasExpandableTextArea={false}
|
||||
summary="After Some Number of Attempts"
|
||||
title="Show answer"
|
||||
>
|
||||
|
||||
@@ -42,6 +42,7 @@ exports[`EditorProblemView component renders simple view 1`] = `
|
||||
<span
|
||||
className="flex-grow-1"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
problemType="multiplechoiceresponse"
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ProblemTypeKeys } from '../../../../data/constants/problem';
|
||||
|
||||
import { parseState } from './hooks';
|
||||
import './index.scss';
|
||||
import ExplanationWidget from './ExplanationWidget';
|
||||
|
||||
export const EditProblemView = ({
|
||||
// redux
|
||||
@@ -41,6 +42,7 @@ export const EditProblemView = ({
|
||||
) : (
|
||||
<span className="flex-grow-1">
|
||||
<QuestionWidget />
|
||||
<ExplanationWidget />
|
||||
<AnswerWidget problemType={problemType} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tinyMceWidget {
|
||||
.tox-tinymce {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.tox {
|
||||
.tox-toolbar__primary {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tox-statusbar {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.tox-toolbar__group:not(:last-of-type) {
|
||||
// TODO: Find a way to override the border without !important
|
||||
border-right: none !important;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: relative;
|
||||
left: 5px;
|
||||
border: 1px solid #eae6e5;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// overrides paragon in order to make checked radio and checkboxes green
|
||||
$checked-color: '%230D7D4D';
|
||||
|
||||
|
||||
@@ -350,7 +350,20 @@ export class OLXParser {
|
||||
getSolutionExplanation(problemType) {
|
||||
if (!_.has(this.problem, `${problemType}.solution`)) { return null; }
|
||||
|
||||
const solution = _.get(this.problem, `${problemType}.solution`);
|
||||
let solution = _.get(this.problem, `${problemType}.solution`);
|
||||
const wrapper = Object.keys(solution)[0];
|
||||
if (Object.keys(solution).length === 1 && wrapper === 'div') {
|
||||
const parsedSolution = {};
|
||||
Object.entries(solution.div).forEach(([key, value]) => {
|
||||
if (key !== '@_class') {
|
||||
if (key === 'p') {
|
||||
value.shift();
|
||||
}
|
||||
parsedSolution[key] = value;
|
||||
}
|
||||
});
|
||||
solution = parsedSolution;
|
||||
}
|
||||
const solutionString = this.builder.build(solution);
|
||||
return solutionString;
|
||||
}
|
||||
|
||||
@@ -56,10 +56,16 @@ class ReactStateOLXParser {
|
||||
addSolution() {
|
||||
const { solution } = this.editorObject;
|
||||
if (!solution || solution.length <= 0) { return {}; }
|
||||
const solutionTitle = { '#text': 'Explanation' };
|
||||
const parsedSolution = this.parser.parse(solution);
|
||||
const paragraphs = parsedSolution.p;
|
||||
const withWrapper = _.isArray(paragraphs) ? [solutionTitle, ...paragraphs] : [solutionTitle, paragraphs];
|
||||
const solutionObject = {
|
||||
solution: {
|
||||
...parsedSolution,
|
||||
div: {
|
||||
'@_class': 'detailed-solution',
|
||||
p: withWrapper,
|
||||
},
|
||||
},
|
||||
};
|
||||
return solutionObject;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
export const checkboxesWithFeedbackAndHints = {
|
||||
solution: `<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
solution: `
|
||||
<p>
|
||||
You can form a voltage divider that evenly divides the input
|
||||
voltage with two identically valued resistors, with the sampled
|
||||
voltage taken in between the two.
|
||||
</p>
|
||||
<p><img src="/static/images/voltage_divider.png" alt=""></img></p>
|
||||
</div>`,
|
||||
`,
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><label>Add the question text, or prompt, here. This text is required.</label><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
answers: {
|
||||
A: '<p>a correct answer</p>',
|
||||
@@ -52,7 +51,7 @@ export const dropdownWithFeedbackAndHints = {
|
||||
};
|
||||
|
||||
export const multipleChoiceWithFeedbackAndHints = {
|
||||
solution: '',
|
||||
solution: '<p>You can add a solution</p>',
|
||||
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for multiple choice with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><label>Add the question text, or prompt, here. This text is required.</label><em>You can add an optional tip or note related to the prompt like this.</em>',
|
||||
answers: {
|
||||
A: '<p>an incorrect answer</p>',
|
||||
|
||||
@@ -45,15 +45,13 @@ export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({
|
||||
value: '<p>If you add more than one hint, a different hint appears each time learners select the hint button.</p>',
|
||||
},
|
||||
],
|
||||
solutionExplanation: `<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
solutionExplanation: `
|
||||
<p>
|
||||
You can form a voltage divider that evenly divides the input
|
||||
voltage with two identically valued resistors, with the sampled
|
||||
voltage taken in between the two.
|
||||
</p>
|
||||
<p><img src="/static/images/voltage_divider.png" alt=""></img></p>
|
||||
</div>`,
|
||||
<p><img src="/static/images/voltage_divider.png" alt=""></img></p>`,
|
||||
data: {
|
||||
answers: [
|
||||
{
|
||||
@@ -251,6 +249,9 @@ export const multipleChoiceWithFeedbackAndHintsOLX = {
|
||||
<choice correct="false"><p>an incorrect answer</p><choicehint><p>You can specify optional feedback for none, a subset, or all of the answers.</></choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
<solution>
|
||||
<p>You can add a solution</p>
|
||||
</solution>
|
||||
</multiplechoiceresponse>
|
||||
<demandhint>
|
||||
<hint><p>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</p></hint>
|
||||
@@ -266,6 +267,7 @@ export const multipleChoiceWithFeedbackAndHintsOLX = {
|
||||
value: '<p>If you add more than one hint, a different hint appears each time learners select the hint button.</p>',
|
||||
},
|
||||
],
|
||||
solutionExplanation: '<p>You can add a solution</p>',
|
||||
data: {
|
||||
answers: [
|
||||
{
|
||||
@@ -302,6 +304,12 @@ export const multipleChoiceWithFeedbackAndHintsOLX = {
|
||||
<p>an incorrect answer </p> <choicehint><p>You can specify optional feedback for none, a subset, or all of the answers.</p></choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>You can add a solution</p>
|
||||
</div>
|
||||
</solution>
|
||||
</multiplechoiceresponse>
|
||||
<demandhint>
|
||||
<hint><p>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</p></hint>
|
||||
|
||||
@@ -137,6 +137,7 @@ export const apiMethods = {
|
||||
metadata: { display_name: title },
|
||||
};
|
||||
} else if (blockType === 'problem') {
|
||||
// console.log(type);
|
||||
response = {
|
||||
data: content.olx,
|
||||
category: blockType,
|
||||
|
||||
Reference in New Issue
Block a user