fix: tinymce render outside of editors (#1254)

This commit is contained in:
Kristin Aoki
2024-09-05 16:18:26 -04:00
committed by GitHub
parent 34f0bf5253
commit dcf05cde07
36 changed files with 339 additions and 175 deletions

View File

@@ -25,6 +25,9 @@ const AnswerOption = ({
intl,
// redux
problemType,
images,
isLibrary,
learningContextId,
}) => {
const dispatch = useDispatch();
const removeAnswer = hooks.removeAnswer({ answer, dispatch });
@@ -47,6 +50,11 @@ const AnswerOption = ({
setContent={setAnswerTitle}
placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
id={`answer-${answer.id}`}
{...{
images,
isLibrary,
learningContextId,
}}
/>
);
}
@@ -106,6 +114,11 @@ const AnswerOption = ({
setSelectedFeedback={setSelectedFeedback}
setUnselectedFeedback={setUnselectedFeedback}
intl={intl}
{...{
images,
isLibrary,
learningContextId,
}}
/>
</Collapsible.Body>
</div>
@@ -135,10 +148,16 @@ AnswerOption.propTypes = {
intl: intlShape.isRequired,
// redux
problemType: PropTypes.string.isRequired,
images: PropTypes.shape({}).isRequired,
learningContextId: PropTypes.string.isRequired,
isLibrary: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
problemType: selectors.problem.problemType(state),
images: selectors.app.images(state),
isLibrary: selectors.app.isLibrary(state),
learningContextId: selectors.app.learningContextId(state),
});
export const mapDispatchToProps = {};

View File

@@ -10,12 +10,18 @@ jest.mock('../../../../../data/redux', () => ({
default: jest.fn(),
selectors: {
problem: {
answers: jest.fn(state => ({ answers: state })),
problemType: jest.fn(state => ({ problemType: state })),
},
app: {
images: jest.fn(state => ({ images: state })),
isLibrary: jest.fn(state => ({ isLibrary: state })),
learningContextId: jest.fn(state => ({ learningContextId: state })),
},
},
thunkActions: {
video: jest.fn(),
video: {
importTranscripts: jest.fn(),
},
},
}));
@@ -49,6 +55,9 @@ describe('AnswerOption', () => {
intl: { formatMessage },
// redux
problemType: 'multiplechoiceresponse',
images: {},
isLibrary: false,
learningContextId: 'course+org+run',
};
describe('render', () => {
test('snapshot: renders correct option with feedback', () => {
@@ -72,5 +81,20 @@ describe('AnswerOption', () => {
mapStateToProps(testState).problemType,
).toEqual(selectors.problem.problemType(testState));
});
test('images from app.images', () => {
expect(
mapStateToProps(testState).images,
).toEqual(selectors.app.images(testState));
});
test('learningContextId from app.learningContextId', () => {
expect(
mapStateToProps(testState).learningContextId,
).toEqual(selectors.app.learningContextId(testState));
});
test('isLibrary from app.isLibrary', () => {
expect(
mapStateToProps(testState).isLibrary,
).toEqual(selectors.app.isLibrary(testState));
});
});
});

View File

@@ -30,6 +30,9 @@ exports[`AnswerOption render snapshot: renders correct option with feedback 1`]
error={false}
errorMessage={null}
id="answer-A"
images={{}}
isLibrary={false}
learningContextId="course+org+run"
placeholder="Enter an answer"
setContent={[Function]}
value="Answer 1"
@@ -44,11 +47,14 @@ exports[`AnswerOption render snapshot: renders correct option with feedback 1`]
"title": "Answer 1",
}
}
images={{}}
intl={
{
"formatMessage": [Function],
}
}
isLibrary={false}
learningContextId="course+org+run"
problemType="multiplechoiceresponse"
setSelectedFeedback={[Function]}
setUnselectedFeedback={[Function]}
@@ -121,11 +127,14 @@ exports[`AnswerOption render snapshot: renders correct option with numeric input
"title": "Answer 1",
}
}
images={{}}
intl={
{
"formatMessage": [Function],
}
}
isLibrary={false}
learningContextId="course+org+run"
problemType="numericalresponse"
setSelectedFeedback={[Function]}
setUnselectedFeedback={[Function]}
@@ -213,11 +222,14 @@ exports[`AnswerOption render snapshot: renders correct option with numeric input
"unselectedFeedback": "unselected feedback",
}
}
images={{}}
intl={
{
"formatMessage": [Function],
}
}
isLibrary={false}
learningContextId="course+org+run"
problemType="numericalresponse"
setSelectedFeedback={[Function]}
setUnselectedFeedback={[Function]}
@@ -276,6 +288,9 @@ exports[`AnswerOption render snapshot: renders correct option with selected unse
error={false}
errorMessage={null}
id="answer-A"
images={{}}
isLibrary={false}
learningContextId="course+org+run"
placeholder="Enter an answer"
setContent={[Function]}
value="Answer 1"
@@ -291,11 +306,14 @@ exports[`AnswerOption render snapshot: renders correct option with selected unse
"unselectedFeedback": "unselected feedback",
}
}
images={{}}
intl={
{
"formatMessage": [Function],
}
}
isLibrary={false}
learningContextId="course+org+run"
problemType="choiceresponse"
setSelectedFeedback={[Function]}
setUnselectedFeedback={[Function]}

View File

@@ -12,12 +12,18 @@ export const FeedbackBox = ({
problemType,
setSelectedFeedback,
setUnselectedFeedback,
images,
isLibrary,
learningContextId,
// injected
intl,
}) => {
const props = {
answer,
intl,
images,
isLibrary,
learningContextId,
};
return ((problemType === ProblemTypeKeys.MULTISELECT) ? (
@@ -61,6 +67,9 @@ FeedbackBox.propTypes = {
setAnswer: PropTypes.func.isRequired,
setSelectedFeedback: PropTypes.func.isRequired,
setUnselectedFeedback: PropTypes.func.isRequired,
images: PropTypes.shape({}).isRequired,
learningContextId: PropTypes.string.isRequired,
isLibrary: PropTypes.bool.isRequired,
intl: intlShape.isRequired,
};

View File

@@ -8,6 +8,9 @@ const answerWithFeedback = {
selectedFeedback: 'some feedback',
unselectedFeedback: 'unselectedFeedback',
problemType: 'sOMepRObleM',
images: {},
isLibrary: false,
learningContextId: 'course+org+run',
};
const props = {

View File

@@ -15,6 +15,9 @@ const FeedbackControl = ({
answer,
intl,
type,
images,
isLibrary,
learningContextId,
}) => (
<Form.Group>
<Form.Label className="mb-3">
@@ -31,6 +34,11 @@ const FeedbackControl = ({
value={feedback}
setContent={onChange}
placeholder={intl.formatMessage(messages.feedbackPlaceholder)}
{...{
images,
isLibrary,
learningContextId,
}}
/>
</Form.Group>
);
@@ -41,6 +49,9 @@ FeedbackControl.propTypes = {
labelMessageBoldUnderline: PropTypes.string.isRequired,
answer: answerOptionProps.isRequired,
type: PropTypes.string.isRequired,
images: PropTypes.shape({}).isRequired,
learningContextId: PropTypes.string.isRequired,
isLibrary: PropTypes.bool.isRequired,
intl: intlShape.isRequired,
};

View File

@@ -18,6 +18,9 @@ const props = {
onChange: jest.fn(),
labelMessage: 'msg',
labelMessageBoldUnderline: 'msg',
images: {},
isLibrary: false,
learningContextId: 'course+org+run',
};
describe('FeedbackControl component', () => {

View File

@@ -9,6 +9,9 @@ exports[`FeedbackBox component renders as expected with a multi select problem 1
{
"correct": true,
"id": "A",
"images": {},
"isLibrary": false,
"learningContextId": "course+org+run",
"problemType": "sOMepRObleM",
"selectedFeedback": "some feedback",
"title": "Answer 1",
@@ -39,6 +42,9 @@ exports[`FeedbackBox component renders as expected with a multi select problem 1
{
"correct": true,
"id": "A",
"images": {},
"isLibrary": false,
"learningContextId": "course+org+run",
"problemType": "sOMepRObleM",
"selectedFeedback": "some feedback",
"title": "Answer 1",
@@ -76,6 +82,9 @@ exports[`FeedbackBox component renders as expected with a numeric input problem
{
"correct": true,
"id": "A",
"images": {},
"isLibrary": false,
"learningContextId": "course+org+run",
"problemType": "sOMepRObleM",
"selectedFeedback": "some feedback",
"title": "Answer 1",
@@ -113,6 +122,9 @@ exports[`FeedbackBox component renders as expected with default props 1`] = `
{
"correct": true,
"id": "A",
"images": {},
"isLibrary": false,
"learningContextId": "course+org+run",
"problemType": "sOMepRObleM",
"selectedFeedback": "some feedback",
"title": "Answer 1",

View File

@@ -29,6 +29,9 @@ exports[`FeedbackControl component renders 1`] = `
error={false}
errorMessage={null}
id="undefinedFeedback-A"
images={{}}
isLibrary={false}
learningContextId="course+org+run"
placeholder={null}
setContent={[MockFunction]}
value="feedback"

View File

@@ -42,22 +42,22 @@ export const setAnswerTitle = ({
dispatch(actions.problem.updateAnswer({ id: answer.id, hasSingleAnswer, title }));
};
export const setSelectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (e) => {
if (e.target) {
export const setSelectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (value) => {
if (value) {
dispatch(actions.problem.updateAnswer({
id: answer.id,
hasSingleAnswer,
selectedFeedback: e.target.value,
selectedFeedback: value,
}));
}
};
export const setUnselectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (e) => {
if (e.target) {
export const setUnselectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (value) => {
if (value) {
dispatch(actions.problem.updateAnswer({
id: answer.id,
hasSingleAnswer,
unselectedFeedback: e.target.value,
unselectedFeedback: value,
}));
}
};

View File

@@ -130,12 +130,12 @@ describe('Answer Options Hooks', () => {
const answer = { id: 'A' };
const hasSingleAnswer = false;
const dispatch = useDispatch();
const e = { target: { value: 'string' } };
module.setSelectedFeedback({ answer, hasSingleAnswer, dispatch })(e);
const value = 'string';
module.setSelectedFeedback({ answer, hasSingleAnswer, dispatch })(value);
expect(dispatch).toHaveBeenCalledWith(actions.problem.updateAnswer({
id: answer.id,
hasSingleAnswer,
selectedFeedback: e.target.value,
selectedFeedback: value,
}));
});
});
@@ -144,12 +144,12 @@ describe('Answer Options Hooks', () => {
const answer = { id: 'A' };
const hasSingleAnswer = false;
const dispatch = useDispatch();
const e = { target: { value: 'string' } };
module.setUnselectedFeedback({ answer, hasSingleAnswer, dispatch })(e);
const value = 'string';
module.setUnselectedFeedback({ answer, hasSingleAnswer, dispatch })(value);
expect(dispatch).toHaveBeenCalledWith(actions.problem.updateAnswer({
id: answer.id,
hasSingleAnswer,
unselectedFeedback: e.target.value,
unselectedFeedback: value,
}));
});
});

View File

@@ -22,13 +22,21 @@ exports[`SolutionWidget render snapshot: renders correct default 1`] = `
id="authoring.problemEditor.solutionwidget.solutionDescriptionText"
/>
</div>
<[object Object]
<TinyMceWidget
disabled={false}
editorContentHtml="This is my solution"
editorRef={null}
editorType="solution"
id="solution"
images={{}}
isLibrary={false}
learningContextId="course+org+run"
lmsEndpointUrl=""
minHeight={150}
onChange={[Function]}
placeholder="Enter your explanation"
setEditorRef={[MockFunction prepareEditorRef.setEditorRef]}
studioEndpointUrl=""
/>
</div>
`;

View File

@@ -12,6 +12,8 @@ const ExplanationWidget = ({
// redux
settings,
learningContextId,
images,
isLibrary,
// injected
intl,
}) => {
@@ -39,6 +41,11 @@ const ExplanationWidget = ({
setEditorRef={setEditorRef}
minHeight={150}
placeholder={intl.formatMessage(messages.placeholder)}
{...{
images,
isLibrary,
learningContextId,
}}
/>
</div>
);
@@ -49,12 +56,16 @@ ExplanationWidget.propTypes = {
// eslint-disable-next-line
settings: PropTypes.any.isRequired,
learningContextId: PropTypes.string.isRequired,
images: PropTypes.shape({}).isRequired,
isLibrary: PropTypes.bool.isRequired,
// injected
intl: intlShape.isRequired,
};
export const mapStateToProps = (state) => ({
settings: selectors.problem.settings(state),
learningContextId: selectors.app.learningContextId(state),
images: selectors.app.images(state),
isLibrary: selectors.app.isLibrary(state),
});
export const ExplanationWidgetInternal = ExplanationWidget; // For testing only

View File

@@ -14,6 +14,8 @@ jest.mock('../../../../../data/redux', () => ({
},
app: {
learningContextId: jest.fn(state => ({ learningContextId: state })),
images: jest.fn(state => ({ images: state })),
isLibrary: jest.fn(state => ({ isLibrary: state })),
},
},
thunkActions: {
@@ -35,6 +37,8 @@ describe('SolutionWidget', () => {
const props = {
settings: { solutionExplanation: 'This is my solution' },
learningContextId: 'course+org+run',
images: {},
isLibrary: false,
// injected
intl: { formatMessage },
};
@@ -51,5 +55,15 @@ describe('SolutionWidget', () => {
test('learningContextId from app.learningContextId', () => {
expect(mapStateToProps(testState).learningContextId).toEqual(selectors.app.learningContextId(testState));
});
test('images from app.images', () => {
expect(
mapStateToProps(testState).images,
).toEqual(selectors.app.images(testState));
});
test('isLibrary from app.isLibrary', () => {
expect(
mapStateToProps(testState).isLibrary,
).toEqual(selectors.app.isLibrary(testState));
});
});
});

View File

@@ -13,13 +13,21 @@ exports[`QuestionWidget render snapshot: renders correct default 1`] = `
id="authoring.questionwidget.question.questionWidgetTitle"
/>
</div>
<[object Object]
<TinyMceWidget
disabled={false}
editorContentHtml="This is my question"
editorRef={null}
editorType="question"
id="question"
images={{}}
isLibrary={false}
learningContextId="course+org+run"
lmsEndpointUrl=""
minHeight={150}
onChange={[Function]}
placeholder="Enter your question"
setEditorRef={[MockFunction prepareEditorRef.setEditorRef]}
studioEndpointUrl=""
/>
</div>
`;

View File

@@ -12,6 +12,8 @@ const QuestionWidget = ({
// redux
question,
learningContextId,
images,
isLibrary,
// injected
intl,
}) => {
@@ -36,6 +38,11 @@ const QuestionWidget = ({
setEditorRef={setEditorRef}
minHeight={150}
placeholder={intl.formatMessage(messages.placeholder)}
{...{
images,
isLibrary,
learningContextId,
}}
/>
</div>
);
@@ -45,12 +52,16 @@ QuestionWidget.propTypes = {
// redux
question: PropTypes.string.isRequired,
learningContextId: PropTypes.string.isRequired,
images: PropTypes.shape({}).isRequired,
isLibrary: PropTypes.bool.isRequired,
// injected
intl: intlShape.isRequired,
};
export const mapStateToProps = (state) => ({
question: selectors.problem.question(state),
learningContextId: selectors.app.learningContextId(state),
images: selectors.app.images(state),
isLibrary: selectors.app.isLibrary(state),
});
export const QuestionWidgetInternal = QuestionWidget; // For testing only

View File

@@ -16,6 +16,8 @@ jest.mock('../../../../../data/redux', () => ({
selectors: {
app: {
learningContextId: jest.fn(state => ({ learningContextId: state })),
images: jest.fn(state => ({ images: state })),
isLibrary: jest.fn(state => ({ isLibrary: state })),
},
problem: {
question: jest.fn(state => ({ question: state })),
@@ -41,6 +43,8 @@ describe('QuestionWidget', () => {
question: 'This is my question',
updateQuestion: jest.fn(),
learningContextId: 'course+org+run',
images: {},
isLibrary: false,
// injected
intl: { formatMessage },
};
@@ -57,5 +61,15 @@ describe('QuestionWidget', () => {
test('learningContextId from app.learningContextId', () => {
expect(mapStateToProps(testState).learningContextId).toEqual(selectors.app.learningContextId(testState));
});
test('images from app.images', () => {
expect(
mapStateToProps(testState).images,
).toEqual(selectors.app.images(testState));
});
test('isLibrary from app.isLibrary', () => {
expect(
mapStateToProps(testState).isLibrary,
).toEqual(selectors.app.isLibrary(testState));
});
});
});

View File

@@ -22,6 +22,9 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget for Advanced
className="mt-3"
>
<HintsCard
images={{}}
isLibrary={false}
learningContextId="course+org+run"
problemType="stringresponse"
/>
</div>
@@ -106,6 +109,9 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page 1`] = `
className="mt-3"
>
<HintsCard
images={{}}
isLibrary={false}
learningContextId="course+org+run"
problemType="stringresponse"
/>
</div>
@@ -190,6 +196,9 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page advanced
className="mt-3"
>
<HintsCard
images={{}}
isLibrary={false}
learningContextId="course+org+run"
problemType="stringresponse"
/>
</div>

View File

@@ -36,6 +36,9 @@ const SettingsWidget = ({
updateField,
updateAnswer,
defaultSettings,
images,
isLibrary,
learningContextId,
}) => {
const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards();
@@ -85,7 +88,16 @@ const SettingsWidget = ({
/>
</div>
<div className="mt-3">
<HintsCard problemType={problemType} hints={settings.hints} updateSettings={updateSettings} />
<HintsCard
problemType={problemType}
hints={settings.hints}
updateSettings={updateSettings}
{...{
images,
isLibrary,
learningContextId,
}}
/>
</div>
{feedbackCard()}
<div>
@@ -172,6 +184,9 @@ SettingsWidget.propTypes = {
showResetButton: PropTypes.bool,
rerandomize: PropTypes.string,
}).isRequired,
images: PropTypes.shape({}).isRequired,
learningContextId: PropTypes.string.isRequired,
isLibrary: PropTypes.bool.isRequired,
// eslint-disable-next-line
settings: PropTypes.any.isRequired,
};
@@ -183,6 +198,9 @@ const mapStateToProps = (state) => ({
blockTitle: selectors.app.blockTitle(state),
correctAnswerCount: selectors.problem.correctAnswerCount(state),
defaultSettings: selectors.problem.defaultSettings(state),
images: selectors.app.images(state),
isLibrary: selectors.app.isLibrary(state),
learningContextId: selectors.app.learningContextId(state),
});
export const mapDispatchToProps = {

View File

@@ -30,6 +30,9 @@ describe('SettingsWidget', () => {
showanswer: 'finished',
showResetButton: false,
},
images: {},
isLibrary: false,
learningContextId: 'course+org+run',
};
describe('behavior', () => {

View File

@@ -16,6 +16,9 @@ const HintRow = ({
handleChange,
handleDelete,
id,
images,
isLibrary,
learningContextId,
// injected
intl,
}) => (
@@ -26,6 +29,11 @@ const HintRow = ({
setContent={handleChange}
placeholder={intl.formatMessage(messages.hintInputLabel)}
id={`hint-${id}`}
{...{
images,
isLibrary,
learningContextId,
}}
/>
</Container>
<div className="d-flex flex-row flex-nowrap">
@@ -45,6 +53,9 @@ HintRow.propTypes = {
handleChange: PropTypes.func.isRequired,
handleDelete: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
images: PropTypes.shape({}).isRequired,
learningContextId: PropTypes.string.isRequired,
isLibrary: PropTypes.bool.isRequired,
// injected
intl: intlShape.isRequired,
};

View File

@@ -11,6 +11,9 @@ describe('HintRow', () => {
handleDelete: jest.fn(),
id: '0',
intl: { formatMessage },
images: {},
isLibrary: false,
learningContextId: 'course+org+run',
};
describe('snapshot', () => {

View File

@@ -12,6 +12,9 @@ const HintsCard = ({
hints,
problemType,
updateSettings,
images,
isLibrary,
learningContextId,
// inject
intl,
}) => {
@@ -31,7 +34,12 @@ const HintsCard = ({
key={hint.id}
id={hint.id}
value={hint.value}
{...hintsRowHooks(hint.id, hints, updateSettings)}
{...{
...hintsRowHooks(hint.id, hints, updateSettings),
images,
isLibrary,
learningContextId,
}}
/>
))}
<Button
@@ -54,6 +62,9 @@ HintsCard.propTypes = {
})).isRequired,
problemType: PropTypes.string.isRequired,
updateSettings: PropTypes.func.isRequired,
images: PropTypes.shape({}).isRequired,
learningContextId: PropTypes.string.isRequired,
isLibrary: PropTypes.bool.isRequired,
};
export const HintsCardInternal = HintsCard; // For testing only

View File

@@ -26,6 +26,9 @@ describe('HintsCard', () => {
const hintsRowHooksProps = {
handleChange: jest.fn().mockName('hintsRowHooks.handleChange'),
handleDelete: jest.fn().mockName('hintsRowHooks.handleDelete'),
images: {},
isLibrary: false,
learningContextId: 'course+org+run',
};
hintsRowHooks.mockReturnValue(hintsRowHooksProps);

View File

@@ -12,6 +12,9 @@ exports[`HintRow snapshot snapshot: renders hints row 1`] = `
error={false}
errorMessage={null}
id="hint-0"
images={{}}
isLibrary={false}
learningContextId="course+org+run"
placeholder="Hint"
setContent={[MockFunction]}
value="hint_1"

View File

@@ -30,7 +30,8 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
id="authoring.texteditor.load.error"
/>
</Toast>
<[object Object]
<TinyMceWidget
disabled={false}
editorContentHtml="eDiTablE Text"
editorRef={
{
@@ -41,9 +42,16 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
}
editorType="text"
height="100%"
id={null}
images={{}}
initializeEditor={[MockFunction args.intializeEditor]}
isLibrary={null}
learningContextId="course+org+run"
lmsEndpointUrl=""
minHeight={500}
onChange={[Function]}
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
studioEndpointUrl=""
/>
</div>
</EditorContainer>
@@ -173,7 +181,8 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
id="authoring.texteditor.load.error"
/>
</Toast>
<[object Object]
<TinyMceWidget
disabled={false}
editorContentHtml="eDiTablE Text"
editorRef={
{
@@ -184,9 +193,16 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
}
editorType="text"
height="100%"
id={null}
images={{}}
initializeEditor={[MockFunction args.intializeEditor]}
isLibrary={null}
learningContextId="course+org+run"
lmsEndpointUrl=""
minHeight={500}
onChange={[Function]}
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
studioEndpointUrl=""
/>
</div>
</EditorContainer>
@@ -222,7 +238,8 @@ exports[`TextEditor snapshots renders static images with relative paths 1`] = `
id="authoring.texteditor.load.error"
/>
</Toast>
<[object Object]
<TinyMceWidget
disabled={false}
editorContentHtml="eDiTablE Text with <img src="/asset+org+run+type@asset+block@img.jpg" />"
editorRef={
{
@@ -233,9 +250,16 @@ exports[`TextEditor snapshots renders static images with relative paths 1`] = `
}
editorType="text"
height="100%"
id={null}
images={{}}
initializeEditor={[MockFunction args.intializeEditor]}
isLibrary={null}
learningContextId="course+org+run"
lmsEndpointUrl=""
minHeight={500}
onChange={[Function]}
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
studioEndpointUrl=""
/>
</div>
</EditorContainer>

View File

@@ -28,6 +28,8 @@ const TextEditor = ({
initializeEditor,
blockFinished,
learningContextId,
images,
isLibrary,
// inject
intl,
}) => {
@@ -59,6 +61,11 @@ const TextEditor = ({
minHeight={500}
height="100%"
initializeEditor={initializeEditor}
{...{
images,
isLibrary,
learningContextId,
}}
/>
);
};
@@ -105,6 +112,8 @@ TextEditor.propTypes = {
showRawEditor: PropTypes.bool.isRequired,
blockFinished: PropTypes.bool,
learningContextId: PropTypes.string.isRequired,
images: PropTypes.shape({}).isRequired,
isLibrary: PropTypes.bool.isRequired,
// inject
intl: intlShape.isRequired,
};
@@ -115,6 +124,8 @@ export const mapStateToProps = (state) => ({
showRawEditor: selectors.app.showRawEditor(state),
blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
learningContextId: selectors.app.learningContextId(state),
images: selectors.app.images(state),
isLibrary: selectors.app.isLibrary(state),
});
export const mapDispatchToProps = {

View File

@@ -57,6 +57,7 @@ jest.mock('../../data/redux', () => ({
lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })),
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
showRawEditor: jest.fn(state => ({ showRawEditor: state })),
images: jest.fn(state => ({ images: state })),
isLibrary: jest.fn(state => ({ isLibrary: state })),
learningContextId: jest.fn(state => ({ learningContextId: state })),
},
@@ -82,6 +83,7 @@ describe('TextEditor', () => {
showRawEditor: false,
blockFinished: true,
learningContextId: 'course+org+run',
images: {},
// inject
intl: { formatMessage },
};
@@ -129,6 +131,11 @@ describe('TextEditor', () => {
mapStateToProps(testState).learningContextId,
).toEqual(selectors.app.learningContextId(testState));
});
test('images from app.images', () => {
expect(
mapStateToProps(testState).images,
).toEqual(selectors.app.images(testState));
});
});
describe('mapDispatchToProps', () => {

View File

@@ -1,17 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TinyMceWidget snapshots ImageUploadModal is not rendered 1`] = `
<Provider
store={
{
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
>
<Fragment>
<SourceCodeModal
close={[MockFunction modal.closeModal]}
editorRef={
@@ -61,21 +51,11 @@ exports[`TinyMceWidget snapshots ImageUploadModal is not rendered 1`] = `
id="sOMeiD"
onEditorChange={[Function]}
/>
</Provider>
</Fragment>
`;
exports[`TinyMceWidget snapshots SourcecodeModal is not rendered 1`] = `
<Provider
store={
{
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
>
<Fragment>
<ImageUploadModal
clearSelection={[MockFunction hooks.selectedImage.clearSelection]}
close={[MockFunction modal.closeModal]}
@@ -97,7 +77,7 @@ exports[`TinyMceWidget snapshots SourcecodeModal is not rendered 1`] = `
}
}
isOpen={false}
lmsEndpointUrl="sOmEvaLue.cOm"
lmsEndpointUrl="http://localhost:18000"
selection="hooks.selectedImage.selection"
setSelection={[MockFunction hooks.selectedImage.setSelection]}
/>
@@ -139,21 +119,11 @@ exports[`TinyMceWidget snapshots SourcecodeModal is not rendered 1`] = `
id="sOMeiD"
onEditorChange={[Function]}
/>
</Provider>
</Fragment>
`;
exports[`TinyMceWidget snapshots renders as expected with default behavior 1`] = `
<Provider
store={
{
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
>
<Fragment>
<ImageUploadModal
clearSelection={[MockFunction hooks.selectedImage.clearSelection]}
close={[MockFunction modal.closeModal]}
@@ -175,7 +145,7 @@ exports[`TinyMceWidget snapshots renders as expected with default behavior 1`] =
}
}
isOpen={false}
lmsEndpointUrl="sOmEvaLue.cOm"
lmsEndpointUrl="http://localhost:18000"
selection="hooks.selectedImage.selection"
setSelection={[MockFunction hooks.selectedImage.setSelection]}
/>
@@ -228,5 +198,5 @@ exports[`TinyMceWidget snapshots renders as expected with default behavior 1`] =
id="sOMeiD"
onEditorChange={[Function]}
/>
</Provider>
</Fragment>
`;

View File

@@ -4,6 +4,7 @@ import {
useCallback,
useEffect,
} from 'react';
import { getConfig } from '@edx/frontend-platform';
import { getLocale, isRtl } from '@edx/frontend-platform/i18n';
import { a11ycheckerCss } from 'frontend-components-tinymce-advanced-plugins';
import { isEmpty } from 'lodash';
@@ -234,8 +235,6 @@ export const editorConfig = ({
setEditorRef,
editorContentHtml,
images,
lmsEndpointUrl,
studioEndpointUrl,
isLibrary,
placeholder,
initializeEditor,
@@ -247,6 +246,8 @@ export const editorConfig = ({
minHeight,
learningContextId,
}) => {
const lmsEndpointUrl = getConfig().LMS_BASE_URL;
const studioEndpointUrl = getConfig().STUDIO_BASE_URL;
const {
toolbar,
config,

View File

@@ -1,4 +1,5 @@
import 'CourseAuthoring/editors/setupEditorTest';
import { getConfig } from '@edx/frontend-platform';
import { MockUseState } from '../../testUtils';
import * as tinyMCE from '../../data/constants/tinyMCE';
@@ -125,7 +126,7 @@ describe('TinyMceEditor hooks', () => {
const setImage = jest.fn();
const updateContent = jest.fn();
const editorType = 'expandable';
const lmsEndpointUrl = 'sOmEvaLue.cOm';
const lmsEndpointUrl = getConfig().LMS_BASE_URL;
const editor = {
ui: { registry: { addButton, addToggleButton, addIcon } },
on: jest.fn(),
@@ -190,7 +191,7 @@ describe('TinyMceEditor hooks', () => {
describe('replaceStaticWithAsset', () => {
const initialContent = `<img src="/static/soMEImagEURl1.jpeg"/><a href="/assets/v1/${baseAssetUrl}/test.pdf">test</a><img src="/${baseAssetUrl}@correct.png" /><img src="/${baseAssetUrl}/correct.png" />`;
const learningContextId = 'course-v1:org+test+run';
const lmsEndpointUrl = 'sOmEvaLue.cOm';
const lmsEndpointUrl = getConfig().LMS_BASE_URL;
it('returns updated src for text editor to update content', () => {
const expected = `<img src="/${baseAssetUrl}@soMEImagEURl1.jpeg"/><a href="/${baseAssetUrl}@test.pdf">test</a><img src="/${baseAssetUrl}@correct.png" /><img src="/${baseAssetUrl}@correct.png" />`;
const actual = module.replaceStaticWithAsset({ initialContent, learningContextId });
@@ -216,7 +217,7 @@ describe('TinyMceEditor hooks', () => {
describe('setAssetToStaticUrl', () => {
it('returns content with updated img links', () => {
const editorValue = `<img src="/${baseAssetUrl}/soME_ImagE_URl1"/> <a href="/${baseAssetUrl}@soMEImagEURl">testing link</a>`;
const lmsEndpointUrl = 'sOmEvaLue.cOm';
const lmsEndpointUrl = getConfig().LMS_BASE_URL;
const content = module.setAssetToStaticUrl({ editorValue, lmsEndpointUrl });
expect(content).toEqual('<img src="/static/soME_ImagE_URl1"/> <a href="/static/soMEImagEURl">testing link</a>');
});
@@ -226,8 +227,8 @@ describe('TinyMceEditor hooks', () => {
const props = {
editorContentHtml: null,
editorType: 'text',
lmsEndpointUrl: 'sOmEuRl.cOm',
studioEndpointUrl: 'sOmEoThEruRl.cOm',
lmsEndpointUrl: getConfig().LMS_BASE_URL,
studioEndpointUrl: getConfig().STUDIO_BASE_URL,
images: mockImagesRef,
isLibrary: false,
learningContextId: 'course+org+run',

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Provider, connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Editor } from '@tinymce/tinymce-react';
import { getConfig } from '@edx/frontend-platform';
import 'tinymce';
import 'tinymce/themes/silver';
@@ -9,8 +9,6 @@ import 'tinymce/skins/ui/oxide/skin.css';
import 'tinymce/icons/default';
import 'frontend-components-tinymce-advanced-plugins';
import store from '../../data/store';
import { selectors } from '../../data/redux';
import ImageUploadModal from '../ImageUploadModal';
import SourceCodeModal from '../SourceCodeModal';
import * as hooks from './hooks';
@@ -42,41 +40,37 @@ const TinyMceWidget = ({
disabled,
id,
editorContentHtml, // editorContent in html form
// redux
learningContextId,
images,
isLibrary,
lmsEndpointUrl,
studioEndpointUrl,
onChange,
...editorConfig
}) => {
const { isImgOpen, openImgModal, closeImgModal } = hooks.imgModalToggle();
const { isSourceCodeOpen, openSourceCodeModal, closeSourceCodeModal } = hooks.sourceCodeModalToggle(editorRef);
const { imagesRef } = hooks.useImages({ images, editorContentHtml });
const imageSelection = hooks.selectedImage(null);
return (
<Provider store={store}>
{isLibrary ? null : (
<>
{!isLibrary && (
<ImageUploadModal
isOpen={isImgOpen}
close={closeImgModal}
editorRef={editorRef}
images={imagesRef}
editorType={editorType}
lmsEndpointUrl={lmsEndpointUrl}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
{...imageSelection}
/>
)}
{editorType === 'text' ? (
{editorType === 'text' && (
<SourceCodeModal
isOpen={isSourceCodeOpen}
close={closeSourceCodeModal}
editorRef={editorRef}
/>
) : null}
)}
<Editor
id={id}
disabled={disabled}
@@ -89,8 +83,6 @@ const TinyMceWidget = ({
editorRef,
isLibrary,
learningContextId,
lmsEndpointUrl,
studioEndpointUrl,
images: imagesRef,
editorContentHtml,
...imageSelection,
@@ -98,15 +90,15 @@ const TinyMceWidget = ({
})
}
/>
</Provider>
</>
);
};
TinyMceWidget.defaultProps = {
isLibrary: null,
editorType: null,
editorRef: null,
lmsEndpointUrl: null,
studioEndpointUrl: null,
lmsEndpointUrl: '',
studioEndpointUrl: '',
images: null,
id: null,
disabled: false,
@@ -116,7 +108,7 @@ TinyMceWidget.defaultProps = {
...editorConfigDefaultProps,
};
TinyMceWidget.propTypes = {
learningContextId: PropTypes.string,
learningContextId: PropTypes.string.isRequired,
editorType: PropTypes.string,
isLibrary: PropTypes.bool,
images: PropTypes.shape({}),
@@ -131,13 +123,5 @@ TinyMceWidget.propTypes = {
...editorConfigPropTypes,
};
export const mapStateToProps = (state) => ({
images: selectors.app.images(state),
lmsEndpointUrl: selectors.app.lmsEndpointUrl(state),
studioEndpointUrl: selectors.app.studioEndpointUrl(state),
isLibrary: selectors.app.isLibrary(state),
learningContextId: selectors.app.learningContextId(state),
});
export const TinyMceWidgetInternal = TinyMceWidget; // For testing only
export default (connect(mapStateToProps)(TinyMceWidget));
export default TinyMceWidget;

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { selectors } from '../../data/redux';
import SourceCodeModal from '../SourceCodeModal';
import ImageUploadModal from '../ImageUploadModal';
import { imgModalToggle, sourceCodeModalToggle } from './hooks';
import { TinyMceWidgetInternal as TinyMceWidget, mapStateToProps } from '.';
import { TinyMceWidgetInternal as TinyMceWidget } from '.';
const staticUrl = '/assets/sOmEaSsET';
@@ -22,20 +21,6 @@ jest.mock('@tinymce/tinymce-react', () => {
jest.mock('../ImageUploadModal', () => 'ImageUploadModal');
jest.mock('../SourceCodeModal', () => 'SourceCodeModal');
jest.mock('../../data/redux', () => ({
__esModule: true,
default: jest.fn(),
selectors: {
app: {
lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })),
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
isLibrary: jest.fn(state => ({ isLibrary: state })),
images: jest.fn(state => ({ images: state })),
learningContextId: jest.fn(state => ({ learningContextId: state })),
},
},
}));
jest.mock('./hooks', () => ({
editorConfig: jest.fn(args => ({ editorConfig: args })),
imgModalToggle: jest.fn(() => ({
@@ -56,15 +41,6 @@ jest.mock('./hooks', () => ({
useImages: jest.fn(() => ({ imagesRef: { current: [{ externalUrl: staticUrl }] } })),
}));
jest.mock('react-redux', () => ({
Provider: 'Provider',
connect: (mapStateToProp, mapDispatchToProps) => (component) => ({
mapStateToProp,
mapDispatchToProps,
component,
}),
}));
describe('TinyMceWidget', () => {
const props = {
editorType: 'text',
@@ -103,32 +79,4 @@ describe('TinyMceWidget', () => {
expect(wrapper.instance.findByType(ImageUploadModal).length).toBe(0);
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('lmsEndpointUrl from app.lmsEndpointUrl', () => {
expect(
mapStateToProps(testState).lmsEndpointUrl,
).toEqual(selectors.app.lmsEndpointUrl(testState));
});
test('studioEndpointUrl from app.studioEndpointUrl', () => {
expect(
mapStateToProps(testState).studioEndpointUrl,
).toEqual(selectors.app.studioEndpointUrl(testState));
});
test('images from app.images', () => {
expect(
mapStateToProps(testState).images,
).toEqual(selectors.app.images(testState));
});
test('isLibrary from app.isLibrary', () => {
expect(
mapStateToProps(testState).isLibrary,
).toEqual(selectors.app.isLibrary(testState));
});
test('learningContextId from app.learningContextId', () => {
expect(
mapStateToProps(testState).learningContextId,
).toEqual(selectors.app.learningContextId(testState));
});
});
});

View File

@@ -1,32 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect, Provider, useSelector } from 'react-redux';
import { createStore } from 'redux';
import { getConfig } from '@edx/frontend-platform';
import { useSelector } from 'react-redux';
import TinyMceWidget, { prepareEditorRef } from '../editors/sharedComponents/TinyMceWidget';
import { DEFAULT_EMPTY_WYSIWYG_VALUE } from '../constants';
const store = createStore(() => ({}));
export const SUPPORTED_TEXT_EDITORS = {
text: 'text',
expandable: 'expandable',
};
const mapStateToProps = () => ({
images: {},
lmsEndpointUrl: getConfig().LMS_BASE_URL,
studioEndpointUrl: getConfig().STUDIO_BASE_URL,
isLibrary: true,
onEditorChange: () => ({}),
});
const Editor = connect(mapStateToProps)(TinyMceWidget);
export const WysiwygEditor = ({
initialValue, editorType, onChange, minHeight,
}) => {
// const courseId = "course+test+test+test"
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
const { courseId } = useSelector((state) => state.courseDetail);
const isEquivalentCodeExtraSpaces = (first, second) => {
@@ -61,20 +47,21 @@ export const WysiwygEditor = ({
}
return (
<Provider store={store}>
<Editor
textValue={initialValue}
editorRef={editorRef}
editorType={editorType}
initialValue={initialValue}
minHeight={minHeight}
editorContentHtml={initialValue}
setEditorRef={setEditorRef}
onChange={handleUpdate}
initializeEditor={() => ({})}
learningContextId={courseId}
/>
</Provider>
<TinyMceWidget
textValue={initialValue}
editorRef={editorRef}
editorType={editorType}
initialValue={initialValue}
minHeight={minHeight}
editorContentHtml={initialValue}
setEditorRef={setEditorRef}
onChange={handleUpdate}
initializeEditor={() => ({})}
learningContextId={courseId}
images={{}}
isLibrary
onEditorChange={() => ({})}
/>
);
};

View File

@@ -111,6 +111,7 @@ initialize({
SUPPORT_URL: process.env.SUPPORT_URL || null,
SUPPORT_EMAIL: process.env.SUPPORT_EMAIL || null,
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL || null,
EXAMS_BASE_URL: process.env.EXAMS_BASE_URL || null,
CALCULATOR_HELP_URL: process.env.CALCULATOR_HELP_URL || null,
ENABLE_PROGRESS_GRAPH_SETTINGS: process.env.ENABLE_PROGRESS_GRAPH_SETTINGS || 'false',

View File

@@ -43,6 +43,7 @@ mergeConfig({
ENABLE_TEAM_TYPE_SETTING: process.env.ENABLE_TEAM_TYPE_SETTING === 'true',
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null,
LMS_BASE_URL: process.env.LMS_BASE_URL || null,
LIBRARY_MODE: process.env.LIBRARY_MODE || 'v1 only',
}, 'CourseAuthoringConfig');