diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js index 7f040f6fd..0d7b423f6 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js @@ -39,19 +39,23 @@ export const setAnswerTitle = ({ }; export const setSelectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (e) => { - dispatch(actions.problem.updateAnswer({ - id: answer.id, - hasSingleAnswer, - selectedFeedback: e.target.value, - })); + if (e.target) { + dispatch(actions.problem.updateAnswer({ + id: answer.id, + hasSingleAnswer, + selectedFeedback: e.target.value, + })); + } }; export const setUnselectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (e) => { - dispatch(actions.problem.updateAnswer({ - id: answer.id, - hasSingleAnswer, - unselectedFeedback: e.target.value, - })); + if (e.target) { + dispatch(actions.problem.updateAnswer({ + id: answer.id, + hasSingleAnswer, + unselectedFeedback: e.target.value, + })); + } }; export const useFeedback = (answer) => { diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/__snapshots__/index.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/__snapshots__/index.test.jsx.snap index 468b4b388..8a9deb930 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/__snapshots__/index.test.jsx.snap @@ -23,7 +23,7 @@ exports[`SolutionWidget render snapshot: renders correct default 1`] = ` /> <[object Object] - editorContentHtml="This is my question" + editorContentHtml="This is my solution" editorType="solution" id="solution" minHeight={150} diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.jsx index 0dccc5a95..308b1165c 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.jsx @@ -6,15 +6,20 @@ import { selectors } from '../../../../../data/redux'; import messages from './messages'; import TinyMceWidget from '../../../../../sharedComponents/TinyMceWidget'; -import { prepareEditorRef } from '../../../../../sharedComponents/TinyMceWidget/hooks'; +import { prepareEditorRef, replaceStaticWithAsset } from '../../../../../sharedComponents/TinyMceWidget/hooks'; export const ExplanationWidget = ({ // redux settings, + learningContextId, // injected intl, }) => { const { editorRef, refReady, setEditorRef } = prepareEditorRef(); + const solutionContent = replaceStaticWithAsset({ + initialContent: settings?.solutionExplanation, + learningContextId, + }); if (!refReady) { return null; } return (
- props.img.displayName
+
- props.img.displayName
+
- props.img.displayName
+
- props.img.displayName
+
useState(val),
});
-export const addImagesAndDimensionsToRef = ({ imagesRef, assets, editorContentHtml }) => {
- const imagesWithDimensions = module.filterAssets({ assets }).map((image) => {
+export const addImagesAndDimensionsToRef = ({ imagesRef, images, editorContentHtml }) => {
+ const imagesWithDimensions = Object.values(images).map((image) => {
const imageFragment = module.getImageFromHtmlString(editorContentHtml, image.url);
return { ...image, width: imageFragment?.width, height: imageFragment?.height };
});
-
imagesRef.current = imagesWithDimensions;
};
-export const useImages = ({ assets, editorContentHtml }) => {
+export const useImages = ({ images, editorContentHtml }) => {
const imagesRef = useRef([]);
useEffect(() => {
- module.addImagesAndDimensionsToRef({ imagesRef, assets, editorContentHtml });
- }, []);
+ module.addImagesAndDimensionsToRef({ imagesRef, images, editorContentHtml });
+ }, [images]);
return { imagesRef };
};
@@ -69,45 +70,45 @@ export const parseContentForLabels = ({ editor, updateContent }) => {
}
};
-export const replaceStaticwithAsset = ({
- editor,
- imageUrls,
+export const replaceStaticWithAsset = ({
+ initialContent,
+ learningContextId,
editorType,
lmsEndpointUrl,
- updateContent,
}) => {
- let content = editor.getContent();
- const imageSrcs = content.split('src="');
- imageSrcs.forEach(src => {
+ let content = initialContent;
+ const srcs = content.split(/(src="|src="|href="|href=")/g).filter(
+ src => src.startsWith('/static') || src.startsWith('/asset'),
+ );
+ if (isEmpty(srcs)) {
+ return initialContent;
+ }
+ srcs.forEach(src => {
const currentContent = content;
let staticFullUrl;
const isStatic = src.startsWith('/static/');
- const isExpandableAsset = src.startsWith('/assets/') && editorType === 'expandable';
- if ((isStatic || isExpandableAsset) && imageUrls.length > 0) {
- const assetSrc = src.substring(0, src.indexOf('"'));
- const assetName = assetSrc.replace(/\/assets\/.+[^/]\//g, '');
- const staticName = assetSrc.substring(8);
- imageUrls.forEach((url) => {
- if (isExpandableAsset && assetName === url.displayName) {
- staticFullUrl = `${lmsEndpointUrl}${url.staticFullUrl}`;
- } else if (staticName === url.displayName) {
- staticFullUrl = url.staticFullUrl;
- if (isExpandableAsset) {
- staticFullUrl = `${lmsEndpointUrl}${url.staticFullUrl}`;
- }
- }
- });
- if (staticFullUrl) {
- const currentSrc = src.substring(0, src.indexOf('"'));
- content = currentContent.replace(currentSrc, staticFullUrl);
- if (editorType === 'expandable') {
- updateContent(content);
- } else {
- editor.setContent(content);
- }
+ const assetSrc = src.substring(0, src.indexOf('"'));
+ const staticName = assetSrc.substring(8);
+ const assetName = assetSrc.replace(/\/assets\/.+[^/]\//g, '');
+ const displayName = isStatic ? staticName : assetName;
+ const isCorrectAssetFormat = assetSrc.match(/\/asset-v1:\S+[+]\S+[@]\S+[+]\S+[@]/g)?.length >= 1;
+ // assets in expandable text areas so not support relative urls so all assets must have the lms
+ // endpoint prepended to the relative url
+ if (editorType === 'expandable') {
+ if (isCorrectAssetFormat) {
+ staticFullUrl = `${lmsEndpointUrl}${assetSrc}`;
+ } else {
+ staticFullUrl = `${lmsEndpointUrl}${getRelativeUrl({ courseId: learningContextId, displayName })}`;
}
+ } else if (!isCorrectAssetFormat) {
+ staticFullUrl = getRelativeUrl({ courseId: learningContextId, displayName });
+ }
+ if (staticFullUrl) {
+ const currentSrc = src.substring(0, src.indexOf('"'));
+ content = currentContent.replace(currentSrc, staticFullUrl);
}
});
+ return content;
};
export const getImageResizeHandler = ({ editor, imagesRef, setImage }) => () => {
@@ -132,10 +133,10 @@ export const setupCustomBehavior = ({
openImgModal,
openSourceCodeModal,
editorType,
- imageUrls,
images,
setImage,
lmsEndpointUrl,
+ learningContextId,
}) => (editor) => {
// image upload button
editor.ui.registry.addButton(tinyMCE.buttons.imageUploadButton, {
@@ -188,18 +189,24 @@ export const setupCustomBehavior = ({
});
if (editorType === 'expandable') {
editor.on('init', () => {
- module.replaceStaticwithAsset({
- editor,
- imageUrls,
+ const initialContent = editor.getContent();
+ const newContent = module.replaceStaticWithAsset({
+ initialContent,
editorType,
lmsEndpointUrl,
- updateContent,
+ learningContextId,
});
+ updateContent(newContent);
});
}
editor.on('ExecCommand', (e) => {
if (editorType === 'text' && e.command === 'mceFocus') {
- module.replaceStaticwithAsset({ editor, imageUrls });
+ const initialContent = editor.getContent();
+ const newContent = module.replaceStaticWithAsset({
+ initialContent,
+ learningContextId,
+ });
+ editor.setContent(newContent);
}
if (e.command === 'RemoveFormat') {
editor.formatter.remove('blockquote');
@@ -229,6 +236,7 @@ export const editorConfig = ({
updateContent,
content,
minHeight,
+ learningContextId,
}) => {
const {
toolbar,
@@ -267,7 +275,7 @@ export const editorConfig = ({
setImage: setSelection,
content,
images,
- imageUrls: module.fetchImageUrls(images),
+ learningContextId,
}),
quickbars_insert_toolbar: quickbarsInsertToolbar,
quickbars_selection_toolbar: quickbarsSelectionToolbar,
@@ -380,16 +388,7 @@ export const openModalWithSelectedImage = ({
openImgModal();
};
-export const filterAssets = ({ assets }) => {
- let images = [];
- const assetsList = Object.values(assets);
- if (assetsList.length > 0) {
- images = assetsList.filter(asset => asset?.contentType?.startsWith('image/'));
- }
- return images;
-};
-
-export const setAssetToStaticUrl = ({ editorValue, assets, lmsEndpointUrl }) => {
+export const setAssetToStaticUrl = ({ editorValue, lmsEndpointUrl }) => {
/* For assets to remain usable across course instances, we convert their url to be course-agnostic.
* For example, /assets/course/
'), setContent: jest.fn() };
- const imageUrls = [{ staticFullUrl: '/assets/soMEImagEURl1.jpeg', displayName: 'soMEImagEURl1.jpeg' }];
- const lmsEndpointUrl = 'sOmEvaLue.cOm';
- module.replaceStaticwithAsset({ editor, imageUrls, lmsEndpointUrl });
- expect(editor.getContent).toHaveBeenCalled();
- expect(editor.setContent).toHaveBeenCalled();
+ describe('replaceStaticWithAsset', () => {
+ const initialContent = '
test';
+ const learningContextId = 'course+test+run';
+ const lmsEndpointUrl = 'sOmEvaLue.cOm';
+ it('it returns updated src for text editor to update content', () => {
+ const expected = '
test';
+ const actual = module.replaceStaticWithAsset({ initialContent, learningContextId });
+ expect(actual).toEqual(expected);
});
- test('it calls getContent and updateContent for expandable editor', () => {
- const editor = { getContent: jest.fn(() => '
') };
- const imageUrls = [{ staticFullUrl: '/assets/soMEImagEURl1.jpeg', displayName: 'soMEImagEURl1.jpeg' }];
- const lmsEndpointUrl = 'sOmEvaLue.cOm';
+ it('it returs updated src with absolute url for expandable editor to update content', () => {
const editorType = 'expandable';
- const updateContent = jest.fn();
- module.replaceStaticwithAsset({
- editor,
- imageUrls,
+ const expected = `
test`;
+ const actual = module.replaceStaticWithAsset({
+ initialContent,
editorType,
lmsEndpointUrl,
- updateContent,
+ learningContextId,
});
- expect(editor.getContent).toHaveBeenCalled();
- expect(updateContent).toHaveBeenCalled();
+ expect(actual).toEqual(expected);
});
});
describe('setAssetToStaticUrl', () => {
it('returns content with updated img links', () => {
- const editorValue = ' testing link';
- const assets = [
- { portableUrl: '/static/soMEImagEURl', displayName: 'soMEImagEURl' },
- { portableUrl: '/static/soME_ImagE_URl1', displayName: 'soME ImagE URl1' },
- ];
+ const editorValue = '
testing link';
const lmsEndpointUrl = 'sOmEvaLue.cOm';
- const content = module.setAssetToStaticUrl({ editorValue, assets, lmsEndpointUrl });
+ const content = module.setAssetToStaticUrl({ editorValue, lmsEndpointUrl });
expect(content).toEqual('
testing link');
});
});
@@ -228,6 +219,7 @@ describe('TinyMceEditor hooks', () => {
studioEndpointUrl: 'sOmEoThEruRl.cOm',
images: mockImagesRef,
isLibrary: false,
+ learningContextId: 'course+org+run',
};
const evt = 'fakeEvent';
const editor = 'myEditor';
@@ -344,27 +336,14 @@ describe('TinyMceEditor hooks', () => {
openImgModal: props.openImgModal,
openSourceCodeModal: props.openSourceCodeModal,
setImage: props.setSelection,
- imageUrls: module.fetchImageUrls(props.images),
images: mockImagesRef,
lmsEndpointUrl: props.lmsEndpointUrl,
+ learningContextId: props.learningContextId,
}),
);
});
});
- describe('filterAssets', () => {
- const emptyAssets = {};
- const assets = { sOmEaSsET: { contentType: 'image/' } };
- test('returns an empty array', () => {
- const emptyFilterAssets = module.filterAssets({ assets: emptyAssets });
- expect(emptyFilterAssets).toEqual([]);
- });
- test('returns filtered array of images', () => {
- const FilteredAssets = module.filterAssets({ assets });
- expect(FilteredAssets).toEqual([{ contentType: 'image/' }]);
- });
- });
-
describe('imgModalToggle', () => {
const hookKey = state.keys.isImageModalOpen;
beforeEach(() => {
@@ -522,11 +501,10 @@ describe('TinyMceEditor hooks', () => {
describe('addImagesAndDimensionsToRef', () => {
it('should add images to ref', () => {
const imagesRef = { current: null };
- const assets = { ...mockAssets, height: undefined, width: undefined };
module.addImagesAndDimensionsToRef(
{
imagesRef,
- assets,
+ images: mockImages,
editorContentHtml: mockEditorContentHtml,
},
);
diff --git a/src/editors/sharedComponents/TinyMceWidget/index.jsx b/src/editors/sharedComponents/TinyMceWidget/index.jsx
index d2c8a3ff1..151d08cba 100644
--- a/src/editors/sharedComponents/TinyMceWidget/index.jsx
+++ b/src/editors/sharedComponents/TinyMceWidget/index.jsx
@@ -41,7 +41,8 @@ export const TinyMceWidget = ({
id,
editorContentHtml, // editorContent in html form
// redux
- assets,
+ learningContextId,
+ images,
isLibrary,
lmsEndpointUrl,
studioEndpointUrl,
@@ -50,7 +51,7 @@ export const TinyMceWidget = ({
}) => {
const { isImgOpen, openImgModal, closeImgModal } = hooks.imgModalToggle();
const { isSourceCodeOpen, openSourceCodeModal, closeSourceCodeModal } = hooks.sourceCodeModalToggle(editorRef);
- const { imagesRef } = hooks.useImages({ assets, editorContentHtml });
+ const { imagesRef } = hooks.useImages({ images, editorContentHtml });
const imageSelection = hooks.selectedImage(null);
@@ -85,6 +86,7 @@ export const TinyMceWidget = ({
editorType,
editorRef,
isLibrary,
+ learningContextId,
lmsEndpointUrl,
studioEndpointUrl,
images: imagesRef,
@@ -103,7 +105,7 @@ TinyMceWidget.defaultProps = {
editorRef: null,
lmsEndpointUrl: null,
studioEndpointUrl: null,
- assets: null,
+ images: null,
id: null,
disabled: false,
editorContentHtml: undefined,
@@ -112,9 +114,10 @@ TinyMceWidget.defaultProps = {
...editorConfigDefaultProps,
};
TinyMceWidget.propTypes = {
+ learningContextId: PropTypes.string,
editorType: PropTypes.string,
isLibrary: PropTypes.bool,
- assets: PropTypes.shape({}),
+ images: PropTypes.shape({}),
editorRef: PropTypes.shape({}),
lmsEndpointUrl: PropTypes.string,
studioEndpointUrl: PropTypes.string,
@@ -127,10 +130,11 @@ TinyMceWidget.propTypes = {
};
export const mapStateToProps = (state) => ({
- assets: selectors.app.assets(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 default (connect(mapStateToProps)(TinyMceWidget));
diff --git a/src/editors/sharedComponents/TinyMceWidget/index.test.jsx b/src/editors/sharedComponents/TinyMceWidget/index.test.jsx
index 4bfb7a081..1b2a51b55 100644
--- a/src/editors/sharedComponents/TinyMceWidget/index.test.jsx
+++ b/src/editors/sharedComponents/TinyMceWidget/index.test.jsx
@@ -30,7 +30,8 @@ jest.mock('../../data/redux', () => ({
lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })),
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
isLibrary: jest.fn(state => ({ isLibrary: state })),
- assets: jest.fn(state => ({ assets: state })),
+ images: jest.fn(state => ({ images: state })),
+ learningContextId: jest.fn(state => ({ learningContextId: state })),
},
},
}));
@@ -52,7 +53,6 @@ jest.mock('./hooks', () => ({
setSelection: jest.fn().mockName('hooks.selectedImage.setSelection'),
clearSelection: jest.fn().mockName('hooks.selectedImage.clearSelection'),
})),
- filterAssets: jest.fn(() => [{ staTICUrl: staticUrl }]),
useImages: jest.fn(() => ({ imagesRef: { current: [{ externalUrl: staticUrl }] } })),
}));
@@ -70,12 +70,13 @@ describe('TinyMceWidget', () => {
editorType: 'text',
editorRef: { current: { value: 'something' } },
isLibrary: false,
- assets: { sOmEaSsET: { staTICUrl: staticUrl } },
+ images: { sOmEaSsET: { staTICUrl: staticUrl } },
lmsEndpointUrl: 'sOmEvaLue.cOm',
studioEndpointUrl: 'sOmEoThERvaLue.cOm',
disabled: false,
id: 'sOMeiD',
updateContent: () => ({}),
+ learningContextId: 'course+org+run',
};
describe('snapshots', () => {
imgModalToggle.mockReturnValue({
@@ -114,15 +115,20 @@ describe('TinyMceWidget', () => {
mapStateToProps(testState).studioEndpointUrl,
).toEqual(selectors.app.studioEndpointUrl(testState));
});
- test('assets from app.assets', () => {
+ test('images from app.images', () => {
expect(
- mapStateToProps(testState).assets,
- ).toEqual(selectors.app.assets(testState));
+ 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));
+ });
});
});
diff --git a/src/editors/sharedComponents/TinyMceWidget/utils.js b/src/editors/sharedComponents/TinyMceWidget/utils.js
new file mode 100644
index 000000000..b6e56c655
--- /dev/null
+++ b/src/editors/sharedComponents/TinyMceWidget/utils.js
@@ -0,0 +1,15 @@
+const getLocatorSafeName = ({ displayName }) => {
+ const locatorSafeName = displayName.replace(/[^\w.%-]/gm, '');
+ return locatorSafeName;
+};
+
+export const getStaticUrl = ({ displayName }) => (`/static/${getLocatorSafeName({ displayName })}`);
+
+export const getRelativeUrl = ({ courseId, displayName }) => {
+ if (displayName) {
+ const assetCourseId = courseId.replace('course', 'asset');
+ const assetPathShell = `/${assetCourseId}+type@asset+block@`;
+ return `${assetPathShell}${displayName}`;
+ }
+ return '';
+};