feat: move explanation to main body of editor (#287)

This commit is contained in:
Kristin Aoki
2023-03-22 09:36:01 -04:00
committed by GitHub
parent da9cb6054c
commit 16003a7f4a
21 changed files with 221 additions and 113 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
exports[`QuestionWidget render snapshot: renders correct default 1`] = `
<div
className="question-widget"
className="tinyMceWidget"
>
<div
className="h4 mb-3"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -93,7 +93,6 @@ export const SettingsWidget = ({
<ShowAnswerCard
showAnswer={settings.showAnswer}
updateSettings={updateSettings}
solutionExplanation={settings.solutionExplanation}
/>
</div>
{

View File

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

View File

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

View File

@@ -42,6 +42,7 @@ exports[`EditorProblemView component renders simple view 1`] = `
<span
className="flex-grow-1"
>
<injectIntl(ShimmedIntlComponent) />
<injectIntl(ShimmedIntlComponent) />
<injectIntl(ShimmedIntlComponent)
problemType="multiplechoiceresponse"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -137,6 +137,7 @@ export const apiMethods = {
metadata: { display_name: title },
};
} else if (blockType === 'problem') {
// console.log(type);
response = {
data: content.olx,
category: blockType,