Merge branch 'main' of https://github.com/openedx/frontend-lib-content-components into mashal-m/react-upgrade-to-v17

This commit is contained in:
mashal-m
2023-06-21 14:10:01 +05:00
18 changed files with 403 additions and 129 deletions

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: edx/.github/.github/workflows/lockfileversion-check-v3.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

52
package-lock.json generated
View File

@@ -14,6 +14,9 @@
"@codemirror/lint": "^6.2.1",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
"@edx/browserslist-config": "^1.1.1",
"@reduxjs/toolkit": "^1.8.1",
"@tinymce/tinymce-react": "^3.14.0",
@@ -2159,6 +2162,55 @@
"node": ">=10.0.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"dependencies": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.7",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@edx/browserslist-config": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@edx/browserslist-config/-/browserslist-config-1.2.0.tgz",

View File

@@ -67,6 +67,9 @@
"@codemirror/lint": "^6.2.1",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
"@edx/browserslist-config": "^1.1.1",
"@reduxjs/toolkit": "^1.8.1",
"@tinymce/tinymce-react": "^3.14.0",

View File

@@ -28,6 +28,18 @@ export const nonQuestionKeys = [
'textline',
];
export const richTextFormats = [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'div',
'p',
'pre',
];
export const responseKeys = [
'multiplechoiceresponse',
'numericalresponse',
@@ -62,57 +74,52 @@ export const stripNonTextTags = ({ input, tag }) => {
export class OLXParser {
constructor(olxString) {
this.problem = {};
this.questionData = {};
this.richTextProblem = {};
const richTextOptions = {
// There are two versions of the parsed XLM because the fields using tinymce require the order
// of the parsed data and spacing values to be preserved. However, all the other widgets need
// the data grouped by the wrapping tag. Examples of the parsed format can be found here:
// https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/docs/v4/2.XMLparseOptions.md
const baseParserOptions = {
ignoreAttributes: false,
alwaysCreateTextNode: true,
numberParseOptions: {
leadingZeros: false,
hex: false,
},
preserveOrder: true,
processEntities: false,
};
// Base Parser
this.problem = {};
const parserOptions = {
ignoreAttributes: false,
...baseParserOptions,
alwaysCreateTextNode: true,
numberParseOptions: {
leadingZeros: false,
hex: false,
},
processEntities: false,
};
const builderOptions = {
ignoreAttributes: false,
numberParseOptions: {
leadingZeros: false,
hex: false,
},
processEntities: false,
...baseParserOptions,
};
const richTextBuilderOptions = {
ignoreAttributes: false,
numberParseOptions: {
leadingZeros: false,
hex: false,
},
preserveOrder: true,
processEntities: false,
};
// There are two versions of the parsed XLM because the fields using tinymce require the order
// of the parsed data to be preserved. However, all the other widgets need the data grouped by
// the wrapping tag.
const richTextParser = new XMLParser(richTextOptions);
const parser = new XMLParser(parserOptions);
this.builder = new XMLBuilder(builderOptions);
this.richTextBuilder = new XMLBuilder(richTextBuilderOptions);
this.parsedOLX = parser.parse(olxString);
this.richTextOLX = richTextParser.parse(olxString);
if (_.has(this.parsedOLX, 'problem')) {
this.problem = this.parsedOLX.problem;
this.questionData = this.richTextOLX[0].problem;
}
// Parser with `preservedOrder: true` and `trimValues: false`
this.richTextProblem = [];
const richTextOptions = {
...baseParserOptions,
alwaysCreateTextNode: true,
preserveOrder: true,
trimValues: false,
};
const richTextBuilderOptions = {
...baseParserOptions,
preserveOrder: true,
trimValues: false,
};
const richTextParser = new XMLParser(richTextOptions);
this.richTextBuilder = new XMLBuilder(richTextBuilderOptions);
this.richTextOLX = richTextParser.parse(olxString);
if (_.has(this.parsedOLX, 'problem')) {
this.richTextProblem = this.richTextOLX[0].problem;
}
}
@@ -462,7 +469,7 @@ export class OLXParser {
* @return {string} string of OLX
*/
parseQuestions(problemType) {
const problemArray = _.get(this.questionData[0], problemType) || this.questionData;
const problemArray = _.get(this.richTextProblem[0], problemType) || this.richTextProblem;
const questionArray = [];
problemArray.forEach(tag => {
@@ -478,7 +485,7 @@ export class OLXParser {
*/
tag[tagName].forEach(subTag => {
const subTagName = Object.keys(subTag)[0];
if (subTagName === 'label' || subTagName === 'description') {
if (subTagName === 'label' || subTagName === 'description' || richTextFormats.includes(subTagName)) {
questionArray.push(subTag);
}
});
@@ -503,11 +510,13 @@ export class OLXParser {
if (objKeys.includes('demandhint')) {
const currentDemandHint = obj.demandhint;
currentDemandHint.forEach(hint => {
const hintValue = this.richTextBuilder.build(hint.hint);
hintsObject.push({
id: hintsObject.length,
value: hintValue,
});
if (Object.keys(hint).includes('hint')) {
const hintValue = this.richTextBuilder.build(hint.hint);
hintsObject.push({
id: hintsObject.length,
value: hintValue,
});
}
});
}
});
@@ -526,21 +535,17 @@ export class OLXParser {
getSolutionExplanation(problemType) {
if (!_.has(this.problem, `${problemType}.solution`) && !_.has(this.problem, 'solution')) { return null; }
const [problemBody] = this.richTextProblem.filter(section => Object.keys(section).includes(problemType));
let { solution } = problemBody[problemType].pop();
const { div } = solution[0];
if (solution.length === 1 && div) {
div.forEach((block) => {
const [key] = Object.keys(block);
const [value] = block[key];
if ((key === 'p' || key === 'h2')
&& (_.get(value, '#text', null) === 'Explanation')
) {
div.shift();
const [solutionBody] = problemBody[problemType].filter(section => Object.keys(section).includes('solution'));
const [divBody] = solutionBody.solution.filter(section => Object.keys(section).includes('div'));
const solutionArray = [];
if (divBody && divBody.div) {
divBody.div.forEach(tag => {
if (_.get(Object.values(tag)[0][0], '#text', null) !== 'Explanation') {
solutionArray.push(tag);
}
});
solution = div;
}
const solutionString = this.richTextBuilder.build(solution);
const solutionString = this.richTextBuilder.build(solutionArray);
return solutionString;
}

View File

@@ -1,7 +1,6 @@
import { OLXParser } from './OLXParser';
import {
checkboxesOLXWithFeedbackAndHintsOLX,
getCheckboxesOLXWithFeedbackAndHintsOLX,
dropdownOLXWithFeedbackAndHintsOLX,
numericInputWithFeedbackAndHintsOLX,
textInputWithFeedbackAndHintsOLX,
@@ -21,6 +20,7 @@ import {
labelDescriptionQuestionOLX,
htmlEntityTestOLX,
numberParseTestOLX,
solutionExplanationTest,
} from './mockData/olxTestData';
import { ProblemTypeKeys } from '../../../data/constants/problem';
@@ -261,13 +261,13 @@ describe('OLXParser', () => {
const problemType = olxparser.getProblemType();
const question = olxparser.parseQuestions(problemType);
it('should return an empty string for question', () => {
expect(question).toBe(blankQuestionOLX.question);
expect(question.trim()).toBe(blankQuestionOLX.question);
});
});
describe('given a simple problem olx', () => {
const question = textInputOlxParser.parseQuestions('stringresponse');
it('should return a string of HTML', () => {
expect(question).toEqual(textInputWithFeedbackAndHintsOLX.question);
expect(question.trim()).toEqual(textInputWithFeedbackAndHintsOLX.question);
});
});
describe('given olx with html entities', () => {
@@ -275,7 +275,7 @@ describe('OLXParser', () => {
const problemType = olxparser.getProblemType();
const question = olxparser.parseQuestions(problemType);
it('should not encode html entities', () => {
expect(question).toEqual(htmlEntityTestOLX.question);
expect(question.trim()).toEqual(htmlEntityTestOLX.question);
});
});
describe('given olx with styled content', () => {
@@ -283,7 +283,7 @@ describe('OLXParser', () => {
const problemType = olxparser.getProblemType();
const question = olxparser.parseQuestions(problemType);
it('should pase/build correct styling', () => {
expect(question).toBe(styledQuestionOLX.question);
expect(question.trim()).toBe(styledQuestionOLX.question);
});
});
describe('given olx with label and description tags inside response tag', () => {
@@ -291,20 +291,25 @@ describe('OLXParser', () => {
const problemType = olxparser.getProblemType();
const question = olxparser.parseQuestions(problemType);
it('should append the label/description to the question', () => {
expect(question).toBe(labelDescriptionQuestionOLX.question);
expect(question.trim()).toBe(labelDescriptionQuestionOLX.question);
});
});
});
describe('getSolutionExplanation()', () => {
describe('for checkbox questions', () => {
test('should parse text in p tags', () => {
const { rawOLX } = getCheckboxesOLXWithFeedbackAndHintsOLX();
const olxparser = new OLXParser(rawOLX);
const olxparser = new OLXParser(checkboxesOLXWithFeedbackAndHintsOLX.rawOLX);
const problemType = olxparser.getProblemType();
const explanation = olxparser.getSolutionExplanation(problemType);
const expected = getCheckboxesOLXWithFeedbackAndHintsOLX().solutionExplanation;
const expected = checkboxesOLXWithFeedbackAndHintsOLX.solutionExplanation;
expect(explanation.replace(/\s/g, '')).toBe(expected.replace(/\s/g, ''));
});
});
it('should parse text with proper spacing', () => {
const olxparser = new OLXParser(solutionExplanationTest.rawOLX);
const problemType = olxparser.getProblemType();
const explanation = olxparser.getSolutionExplanation(problemType);
expect(explanation).toBe(solutionExplanationTest.solutionExplanation);
});
});
});

View File

@@ -1,4 +1,7 @@
export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({
/* eslint-disable */
// lint is disabled for this file due to strict spacing
export const checkboxesOLXWithFeedbackAndHintsOLX = {
rawOLX: `<problem>
<choiceresponse>
<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>
@@ -8,12 +11,12 @@ export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({
<choice correct="true"><p>a correct answer</p>
<choicehint selected="true"><p>You can specify optional feedback that appears after the learner selects and submits this answer.</p></choicehint>
<choicehint selected="false"><p>You can specify optional feedback that appears after the learner clears and submits this answer.</p></choicehint>
</choice>
</choice>
<choice correct="false"><p>an incorrect answer</p></choice>
<choice correct="false"><p>an incorrect answer</p>
<choicehint selected="true"><p>You can specify optional feedback for none, all, or a subset of the answers.</p></choicehint>
<choicehint selected="false"><p>You can specify optional feedback for selected answers, cleared answers, or both.</p></choicehint>
</choice>
</choice>
<choice correct="true"><p>a correct answer</p></choice>
<compoundhint value="A B D">You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted.</compoundhint>
<compoundhint value="A B C D">You can specify optional feedback for one, several, or all answer combinations.</compoundhint>
@@ -56,7 +59,7 @@ export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({
answers: [
{
id: 'A',
title: '<p>a correct answer</p>',
title: `<p>a correct answer</p>\n \n \n `,
correct: true,
selectedFeedback: '<p>You can specify optional feedback that appears after the learner selects and submits this answer.</p>',
unselectedFeedback: '<p>You can specify optional feedback that appears after the learner clears and submits this answer.</p>',
@@ -68,7 +71,7 @@ export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({
},
{
id: 'C',
title: '<p>an incorrect answer</p>',
title: `<p>an incorrect answer</p>\n \n \n `,
correct: false,
selectedFeedback: '<p>You can specify optional feedback for none, all, or a subset of the answers.</p>',
unselectedFeedback: '<p>You can specify optional feedback for selected answers, cleared answers, or both.</p>',
@@ -103,45 +106,43 @@ export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({
},
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>',
buildOLX: `<problem>
<choiceresponse>
<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>
<checkboxgroup>
<choice correct="true">
<p>a correct answer </p> <choicehint selected="true"><p>You can specify optional feedback that appears after the learner selects and submits this answer.</p></choicehint>
<choicehint selected="false"><p>You can specify optional feedback that appears after the learner clears and submits this answer.</p></choicehint>
</choice>
<choice correct="false"><p>an incorrect answer</p></choice>
<choice correct="false">
<p>an incorrect answer</p> <choicehint selected="true"><p>You can specify optional feedback for none, all, or a subset of the answers.</p></choicehint>
<choicehint selected="false"><p>You can specify optional feedback for selected answers, cleared answers, or both.</p></choicehint>
</choice>
<choice correct="true"><p>a correct answer</p></choice>
<compoundhint value="A B D">You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted.</compoundhint>
<compoundhint value="A B C D">You can specify optional feedback for one, several, or all answer combinations.</compoundhint>
</checkboxgroup>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>
<choiceresponse>
<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>
<checkboxgroup>
<choice correct="true">
<p>a correct answer </p> <choicehint selected="true"><p>You can specify optional feedback that appears after the learner selects and submits this answer.</p></choicehint>
<choicehint selected="false"><p>You can specify optional feedback that appears after the learner clears and submits this answer.</p></choicehint>
</choice>
<choice correct="false"><p>an incorrect answer</p></choice>
<choice correct="false">
<p>an incorrect answer</p> <choicehint selected="true"><p>You can specify optional feedback for none, all, or a subset of the answers.</p></choicehint>
<choicehint selected="false"><p>You can specify optional feedback for selected answers, cleared answers, or both.</p></choicehint>
</choice>
<choice correct="true"><p>a correct answer</p></choice>
<compoundhint value="A B D">You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted.</compoundhint>
<compoundhint value="A B C D">You can specify optional feedback for one, several, or all answer combinations.</compoundhint>
</checkboxgroup>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<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>
</solution>
</choiceresponse>
<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>
<hint><p>If you add more than one hint, a different hint appears each time learners select the hint button.</p></hint>
</demandhint>
</problem>
`,
});
export const checkboxesOLXWithFeedbackAndHintsOLX = getCheckboxesOLXWithFeedbackAndHintsOLX({});
</p>
<p><img src="/static/images/voltage_divider.png" alt=""></img></p>
</div>
</solution>
</choiceresponse>
<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>
<hint><p>If you add more than one hint, a different hint appears each time learners select the hint button.</p></hint>
</demandhint>
</problem>
`,
};
export const multipleChoiceWithoutAnswers = {
rawOLX: `<problem>
@@ -199,7 +200,7 @@ export const multipleChoiceSingleAnswer = {
answers: [
{
id: 'A',
title: '<p>a correct answer</p><div><img src="#"></img>image with<strong>caption</strong>.</div>',
title: `<p>a correct answer</p><div><img src="#"></img>image with <strong>caption</strong>.</div>\n \n \n `,
correct: true,
selectedFeedback: '<p>You can specify optional feedback that appears after the learner selects and submits this answer.</p>',
unselectedFeedback: '<p>You can specify optional feedback that appears after the learner clears and submits this answer.</p>',
@@ -314,11 +315,9 @@ export const multipleChoiceWithFeedbackAndHintsOLX = {
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<choicegroup type="MultipleChoice">
<choice correct="false"><p>an incorrect answer</p><choicehint><p>You can specify optional feedback like this, which appears after this answer is submitted.</p></choicehint>
</choice>
<choice correct="false"><p>an incorrect answer</p><choicehint><p>You can specify optional feedback like this, which appears after this answer is submitted.</p></choicehint></choice>
<choice correct="true"><p>the correct answer</p></choice>
<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>
<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>
@@ -540,7 +539,7 @@ export const textInputWithFeedbackAndHintsOLX = {
},
},
},
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input 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>',
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input 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>',
buildOLX: `<problem>
<stringresponse answer="the correct answer" type="ci">
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
@@ -726,7 +725,13 @@ export const styledQuestionOLX = {
<textline size="20"/>
</stringresponse>
</problem>`,
question: '<p><strong><span style="background-color: #e03e2d;">test</span></strong></p>',
question: `<p>
<strong>
<span style="background-color: #e03e2d;">
test
</span>
</strong>
</p>`,
};
export const shuffleProblemOLX = {
@@ -763,7 +768,8 @@ export const labelDescriptionQuestionOLX = {
</solution>
</problem>`,
question: '<p style="text-align: center;"><img height="274" width="" src="/static/boiling_eggs_water_system.png" alt="boiling eggs: water system"></img></p><label>Taking the system as just the<b>water</b>, as indicated by the red dashed line, what would be the correct expression for the first law of thermodynamics applied to this system?</label><em>Watch out, boiling water is hot</em>',
question: `<p style="text-align: center;"><img height="274" width="" src="/static/boiling_eggs_water_system.png" alt="boiling eggs: water system"></img></p>
<label>Taking the system as just the <b>water</b>, as indicated by the red dashed line, what would be the correct expression for the first law of thermodynamics applied to this system?</label><em>Watch out, boiling water is hot</em>`,
};
export const htmlEntityTestOLX = {
@@ -798,9 +804,7 @@ export const htmlEntityTestOLX = {
},
],
},
// eslint-disable-next-line
question: `<p>What is the content of the register x2 after executing the following three lines of instructions?</p><p><span style="font-family: 'courier new', courier;"><strong>Address&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;assembly instructions<br></br>0x0&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;addi x1, x0, 1<br></br>0x4&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;slli x2, x1, 4<br></br>0x8&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;sub x1, x2, x1</strong></span></p>`,
// eslint-disable-next-line
question: `<p>What is the content of the register x2 after executing the following three lines of instructions?</p><p><span style="font-family: 'courier new', courier;"><strong>Address&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;assembly instructions <br></br>0x0&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;addi x1, x0, 1<br></br>0x4&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;slli x2, x1, 4<br></br>0x8&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;sub x1, x2, x1</strong></span></p>`,
solutionExplanation: `<p><span style="font-family: 'courier new', courier;"><strong>Address&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;assembly instructions&#160;&#160;&#160;&#160;comment<br></br>0x0&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;addi x1, x0, 1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;x1 = 0x1<br></br>0x4&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;slli x2, x1, 4&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;x2 = x1 &lt;&lt; 4 = 0x10<br></br>0x8&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;sub x1, x2, x1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;x1 = x2 - x1 = 0x10 - 0x01 = 0xf</strong></span></p>`,
};
@@ -820,22 +824,22 @@ export const numberParseTestOLX = {
answers: [
{
id: 'A',
title: `<span style="font-family: 'courier new', courier;"><strong>0x10</strong></span>`, // eslint-disable-line
title: `<span style="font-family: 'courier new', courier;"><strong>0x10</strong></span>`,
correct: false,
},
{
id: 'B',
title: `<span style="font-family: 'courier new', courier;"><strong>0x0f</strong></span>`, // eslint-disable-line
title: `<span style="font-family: 'courier new', courier;"><strong>0x0f</strong></span>`,
correct: true,
},
{
id: 'C',
title: `<span style="font-family: 'courier new', courier;"><strong>0x07</strong></span>`, // eslint-disable-line
title: `<span style="font-family: 'courier new', courier;"><strong>0x07</strong></span>`,
correct: false,
},
{
id: 'D',
title: `<span style="font-family: 'courier new', courier;"><strong>0009</strong></span>`, // eslint-disable-line
title: `<span style="font-family: 'courier new', courier;"><strong>0009</strong></span>`,
correct: false,
},
],
@@ -853,3 +857,26 @@ export const numberParseTestOLX = {
</multiplechoiceresponse>
</problem>`,
};
export const solutionExplanationTest = {
rawOLX: `<problem>
How <code class="lang-matlab">99</code> long is the array <code class="lang-matlab">q</code> after the following loop runs?
<pre><code class="lang-matlab">for i = 1:99
q(2*i - 1) = i;
end</code></pre>
<numericalresponse answer="197">
<label/>
<description>Enter your answer below. Type "e" if this code would produce an error</description>
<formulaequationinput/>
<responseparam default="2%" type="tolerance"/>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
This loop will iterate <code class="lang-matlab">99</code> times, but the length of <code class="lang-matlab">q</code> will not be <code class="lang-matlab">99</code> due to indexing with the value <code class="lang-matlab">2*i -1</code>. On the last iteration, <code class="lang-matlab">i = 99</code>, so <code class="lang-matlab">2*i - 1 = 2*78 - 1 = 197</code>. This will be the last position filled in <code class="lang-matlab">q</code>, so the answer is <code class="lang-matlab">197</code>.
</div>
</solution>
</numericalresponse>
</problem>`,
solutionExplanation: `\n
This loop will iterate <code class="lang-matlab">99</code> times, but the length of <code class="lang-matlab">q</code> will not be <code class="lang-matlab">99</code> due to indexing with the value <code class="lang-matlab">2*i -1</code>. On the last iteration, <code class="lang-matlab">i = 99</code>, so <code class="lang-matlab">2*i - 1 = 2*78 - 1 = 197</code>. This will be the last position filled in <code class="lang-matlab">q</code>, so the answer is <code class="lang-matlab">197</code>.\n `,
};

View File

@@ -70,7 +70,7 @@ export const saveBlock = ({ content, returnToUnit }) => (dispatch) => {
content,
onSuccess: (response) => {
dispatch(actions.app.setSaveResponse(response));
returnToUnit();
returnToUnit(response.data)();
},
}));
};

View File

@@ -138,7 +138,7 @@ describe('app thunkActions', () => {
let returnToUnit;
let calls;
beforeEach(() => {
returnToUnit = jest.fn();
returnToUnit = jest.fn((response) => () => response);
thunkActions.saveBlock({ content: testValue, returnToUnit })(dispatch);
calls = dispatch.mock.calls;
});

View File

@@ -36,7 +36,21 @@ export const fetchBlockById = ({ blockId, studioEndpointUrl }) => {
} else if (blockId === 'problem-block-id') {
data = {
data: `<problem>
</problem>`,
<multiplechoiceresponse>
<p>What is the content of the register x2 after executing the following three lines of instructions?</p>
<p><span style="font-family: 'courier new', courier;"><strong>Address&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;assembly instructions <br />0x0&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;addi x1, x0, 1<br />0x4&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;slli x2, x1, 4<br />0x8&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;sub x1, x2, x1</strong></span></p>
<choicegroup type="MultipleChoice">
<choice correct="false">answerA</choice>
<choice correct="true">answerB</choice>
</choicegroup>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p><span style="font-family: 'courier new', courier;"><strong>Address&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;assembly instructions&#160;&#160;&#160;&#160;comment<br />0x0&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;addi x1, x0, 1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;x1 = 0x1<br />0x4&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;slli x2, x1, 4&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;x2 = x1 &lt;&lt; 4 = 0x10<br />0x8&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;sub x1, x2, x1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;x1 = x2 - x1 = 0x10 - 0x01 = 0xf</strong></span></p>
</div>
</solution>
</multiplechoiceresponse>
</problem>`,
display_name: 'Dropdown',
metadata: {
markdown: `You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown problems. Edit this component to replace this template with your own assessment.

View File

@@ -16,7 +16,9 @@ export const returnUrl = ({ studioEndpointUrl, unitUrl, learningContextId }) =>
};
export const block = ({ studioEndpointUrl, blockId }) => (
`${studioEndpointUrl}/xblock/${blockId}`
blockId.includes('block-v1')
? `${studioEndpointUrl}/xblock/${blockId}`
: `${studioEndpointUrl}/api/xblock/v2/xblocks/${blockId}`
);
export const blockAncestor = ({ studioEndpointUrl, blockId }) => (

View File

@@ -21,7 +21,8 @@ import {
describe('cms url methods', () => {
const studioEndpointUrl = 'urLgoeStOstudiO';
const blockId = 'blOckIDTeST123';
const blockId = 'block-v1-blOckIDTeST123';
const v2BlockId = 'blOckIDTeST123';
const learningContextId = 'lEarnIngCOntextId123';
const courseId = 'course-v1:courseId123';
const libraryV1Id = 'library-v1:libaryId123';
@@ -62,10 +63,14 @@ describe('cms url methods', () => {
});
});
describe('block', () => {
it('returns url with studioEndpointUrl and blockId', () => {
it('returns v1 url with studioEndpointUrl and blockId', () => {
expect(block({ studioEndpointUrl, blockId }))
.toEqual(`${studioEndpointUrl}/xblock/${blockId}`);
});
it('returns v2 url with studioEndpointUrl and v2BlockId', () => {
expect(block({ studioEndpointUrl, blockId: v2BlockId }))
.toEqual(`${studioEndpointUrl}/api/xblock/v2/xblocks/${v2BlockId}`);
});
});
describe('blockAncestor', () => {
it('returns url with studioEndpointUrl, blockId and ancestor query', () => {

View File

@@ -21,12 +21,12 @@ export const navigateCallback = ({
destination,
analyticsEvent,
analytics,
}) => () => {
}) => (response) => {
if (process.env.NODE_ENV !== 'development' && analyticsEvent && analytics) {
sendTrackEvent(analyticsEvent, analytics);
}
if (returnFunction) {
returnFunction();
returnFunction()(response);
return;
}
module.navigateTo(destination);

View File

@@ -74,7 +74,6 @@ describe('hooks', () => {
let output;
const SAVED_ENV = process.env;
const destination = 'hOmE';
const returnFunction = jest.fn();
beforeEach(() => {
jest.resetModules();
process.env = { ...SAVED_ENV };
@@ -102,6 +101,7 @@ describe('hooks', () => {
expect(spy).toHaveBeenCalledWith(destination);
});
it('should call returnFunction and return null', () => {
const returnFunction = jest.fn(() => (response) => response);
output = hooks.navigateCallback({
destination,
returnFunction,

View File

@@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
const DraggableList = ({
itemList,
setState,
updateOrder,
children,
}) => {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragEnd = (event) => {
const { active, over } = event;
if (active.id !== over.id) {
let updatedArray;
setState(() => {
const [activeElement] = itemList.filter(item => item.id === active.id);
const [overElement] = itemList.filter(item => item.id === over.id);
const oldIndex = itemList.indexOf(activeElement);
const newIndex = itemList.indexOf(overElement);
updatedArray = arrayMove(itemList, oldIndex, newIndex);
return updatedArray;
});
updateOrder()(updatedArray);
}
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={itemList}
strategy={verticalListSortingStrategy}
>
{children}
</SortableContext>
</DndContext>
);
};
DraggableList.propTypes = {
itemList: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
})).isRequired,
setState: PropTypes.func.isRequired,
updateOrder: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
};
export default DraggableList;

View File

@@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import { intlShape, injectIntl } from '@edx/frontend-platform/i18n';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Icon, IconButtonWithTooltip, Row } from '@edx/paragon';
import { DragIndicator } from '@edx/paragon/icons';
import messages from './messages';
const SortableItem = ({
id,
componentStyle,
children,
// injected
intl,
}) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
...componentStyle,
};
return (
<Row
ref={setNodeRef}
style={style}
className="mx-0"
>
{children}
<IconButtonWithTooltip
key="drag-to-reorder-icon"
tooltipPlacement="top"
tooltipContent={intl.formatMessage(messages.tooltipContent)}
src={DragIndicator}
iconAs={Icon}
variant="secondary"
alt={intl.formatMessage(messages.tooltipContent)}
{...attributes}
{...listeners}
/>
</Row>
);
};
SortableItem.defaultProps = {
componentStyle: null,
};
SortableItem.propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
componentStyle: PropTypes.shape({}),
// injected
intl: intlShape.isRequired,
};
export default injectIntl(SortableItem);

View File

@@ -0,0 +1,5 @@
import DraggableList from './DraggableList';
import SortableItem from './SortableItem';
export { SortableItem };
export default DraggableList;

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
tooltipContent: {
id: 'authoring.draggableList.tooltip.content',
defaultMessage: 'Drag to reorder',
description: 'Tooltip content for drag indicator icon',
},
});
export default messages;

View File

@@ -2,6 +2,15 @@ import Placeholder from './Placeholder';
import messages from './i18n/index';
import EditorPage from './editors/EditorPage';
import VideoSelectorPage from './editors/VideoSelectorPage';
import DraggableList, { SortableItem } from './editors/sharedComponents/DraggableList';
import ErrorAlert from './editors/sharedComponents/ErrorAlerts/ErrorAlert';
export { messages, EditorPage, VideoSelectorPage };
export {
messages,
EditorPage,
VideoSelectorPage,
DraggableList,
SortableItem,
ErrorAlert,
};
export default Placeholder;