From ba8141ea6aa60256b5ac6ca6d29047f08c3eb4cf Mon Sep 17 00:00:00 2001 From: bszabo Date: Wed, 26 Jun 2024 10:02:59 -0400 Subject: [PATCH] Revert "feat: improve asset loading (#484)" This reverts commit f3ae225d641ffde833aec2da42aba178e1c349db. --- .../EditProblemView/AnswerWidget/hooks.js | 24 ++- .../__snapshots__/index.test.jsx.snap | 2 +- .../ExplanationWidget/index.jsx | 11 +- .../ExplanationWidget/index.test.jsx | 12 +- .../EditProblemView/QuestionWidget/index.jsx | 11 +- .../QuestionWidget/index.test.jsx | 10 +- .../components/EditProblemView/hooks.js | 5 +- .../components/EditProblemView/index.jsx | 6 + .../containers/ProblemEditor/index.jsx | 13 +- .../containers/ProblemEditor/index.test.jsx | 23 +++ .../__snapshots__/index.test.jsx.snap | 28 +++- src/editors/containers/TextEditor/hooks.js | 6 +- .../containers/TextEditor/hooks.test.jsx | 13 +- src/editors/containers/TextEditor/index.jsx | 36 +++-- .../containers/TextEditor/index.test.jsx | 31 ++-- src/editors/data/constants/requests.js | 4 +- src/editors/data/redux/app/reducer.js | 15 +- src/editors/data/redux/app/reducer.test.js | 20 +-- src/editors/data/redux/app/selectors.js | 21 ++- src/editors/data/redux/app/selectors.test.js | 38 ++++- src/editors/data/redux/requests/reducer.js | 2 +- src/editors/data/redux/thunkActions/app.js | 40 ++--- .../data/redux/thunkActions/app.test.js | 143 +++--------------- .../data/redux/thunkActions/requests.js | 11 +- .../data/redux/thunkActions/requests.test.js | 20 +-- src/editors/data/services/cms/api.js | 13 +- src/editors/data/services/cms/api.test.js | 13 +- src/editors/data/services/cms/mockApi.js | 2 +- src/editors/data/services/cms/urls.js | 2 +- src/editors/data/services/cms/urls.test.js | 2 +- .../SelectImageModal/hooks.js | 28 +--- .../SelectImageModal/hooks.test.js | 16 +- .../SelectImageModal/index.jsx | 14 +- .../SelectionModal/Gallery.jsx | 25 +-- .../SelectionModal/Gallery.test.jsx | 3 - .../SelectionModal/GalleryCard.jsx | 5 +- .../SelectionModal/GalleryLoadMoreButton.jsx | 54 ------- .../SelectionModal/SearchSort.jsx | 1 - .../__snapshots__/Gallery.test.jsx.snap | 12 -- .../__snapshots__/GalleryCard.test.jsx.snap | 54 +------ .../sharedComponents/SelectionModal/index.jsx | 1 - .../__snapshots__/index.test.jsx.snap | 3 - .../sharedComponents/TinyMceWidget/hooks.js | 137 ++++++++++------- .../TinyMceWidget/hooks.test.js | 62 +++++--- .../sharedComponents/TinyMceWidget/index.jsx | 14 +- .../TinyMceWidget/index.test.jsx | 18 +-- .../sharedComponents/TinyMceWidget/utils.js | 15 -- 47 files changed, 404 insertions(+), 635 deletions(-) delete mode 100644 src/editors/sharedComponents/SelectionModal/GalleryLoadMoreButton.jsx delete mode 100644 src/editors/sharedComponents/TinyMceWidget/utils.js diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js index 0d7b423f6..7f040f6fd 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js @@ -39,23 +39,19 @@ export const setAnswerTitle = ({ }; export const setSelectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (e) => { - if (e.target) { - dispatch(actions.problem.updateAnswer({ - id: answer.id, - hasSingleAnswer, - selectedFeedback: e.target.value, - })); - } + dispatch(actions.problem.updateAnswer({ + id: answer.id, + hasSingleAnswer, + selectedFeedback: e.target.value, + })); }; export const setUnselectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (e) => { - if (e.target) { - dispatch(actions.problem.updateAnswer({ - id: answer.id, - hasSingleAnswer, - unselectedFeedback: e.target.value, - })); - } + 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 8a9deb930..468b4b388 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 solution" + editorContentHtml="This is my question" 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 308b1165c..0dccc5a95 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.jsx @@ -6,20 +6,15 @@ import { selectors } from '../../../../../data/redux'; import messages from './messages'; import TinyMceWidget from '../../../../../sharedComponents/TinyMceWidget'; -import { prepareEditorRef, replaceStaticWithAsset } from '../../../../../sharedComponents/TinyMceWidget/hooks'; +import { prepareEditorRef } 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 (
@@ -33,7 +28,7 @@ export const ExplanationWidget = ({ id="solution" editorType="solution" editorRef={editorRef} - editorContentHtml={solutionContent} + editorContentHtml={settings?.solutionExplanation} setEditorRef={setEditorRef} minHeight={150} placeholder={intl.formatMessage(messages.placeholder)} @@ -46,13 +41,11 @@ ExplanationWidget.propTypes = { // redux // eslint-disable-next-line settings: PropTypes.any.isRequired, - learningContextId: PropTypes.string.isRequired, // injected intl: intlShape.isRequired, }; export const mapStateToProps = (state) => ({ settings: selectors.problem.settings(state), - learningContextId: selectors.app.learningContextId(state), }); export default injectIntl(connect(mapStateToProps)(ExplanationWidget)); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.test.jsx index 48f784b6a..7eedd86d5 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.test.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.test.jsx @@ -12,9 +12,6 @@ jest.mock('../../../../../data/redux', () => ({ problem: { settings: jest.fn(state => ({ question: state })), }, - app: { - learningContextId: jest.fn(state => ({ learningContextId: state })), - }, }, thunkActions: { video: { @@ -28,13 +25,11 @@ jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({ refReady: true, setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'), })), - replaceStaticWithAsset: jest.fn(() => 'This is my solution'), })); describe('SolutionWidget', () => { const props = { - settings: { solutionExplanation: 'This is my solution' }, - learningContextId: 'course+org+run', + settings: { solutionExplanation: 'This is my question' }, // injected intl: { formatMessage }, }; @@ -45,11 +40,8 @@ describe('SolutionWidget', () => { }); describe('mapStateToProps', () => { const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; - test('settings from problem.settings', () => { + test('question from problem.question', () => { expect(mapStateToProps(testState).settings).toEqual(selectors.problem.settings(testState)); }); - test('learningContextId from app.learningContextId', () => { - expect(mapStateToProps(testState).learningContextId).toEqual(selectors.app.learningContextId(testState)); - }); }); }); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx index 6be330f5a..df21809db 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx @@ -6,20 +6,15 @@ import { selectors } from '../../../../../data/redux'; import messages from './messages'; import TinyMceWidget from '../../../../../sharedComponents/TinyMceWidget'; -import { prepareEditorRef, replaceStaticWithAsset } from '../../../../../sharedComponents/TinyMceWidget/hooks'; +import { prepareEditorRef } from '../../../../../sharedComponents/TinyMceWidget/hooks'; export const QuestionWidget = ({ // redux question, - learningContextId, // injected intl, }) => { const { editorRef, refReady, setEditorRef } = prepareEditorRef(); - const questionContent = replaceStaticWithAsset({ - initialContent: question, - learningContextId, - }); if (!refReady) { return null; } return (
@@ -30,7 +25,7 @@ export const QuestionWidget = ({ id="question" editorType="question" editorRef={editorRef} - editorContentHtml={questionContent} + editorContentHtml={question} setEditorRef={setEditorRef} minHeight={150} placeholder={intl.formatMessage(messages.placeholder)} @@ -42,13 +37,11 @@ export const QuestionWidget = ({ QuestionWidget.propTypes = { // redux question: PropTypes.string.isRequired, - learningContextId: PropTypes.string.isRequired, // injected intl: intlShape.isRequired, }; export const mapStateToProps = (state) => ({ question: selectors.problem.question(state), - learningContextId: selectors.app.learningContextId(state), }); export default injectIntl(connect(mapStateToProps)(QuestionWidget)); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.test.jsx index d18a6bc38..5e01ae0b6 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.test.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.test.jsx @@ -15,7 +15,9 @@ jest.mock('../../../../../data/redux', () => ({ }, selectors: { app: { - learningContextId: jest.fn(state => ({ learningContextId: state })), + isLibrary: jest.fn(state => ({ isLibrary: state })), + lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })), + studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })), }, problem: { question: jest.fn(state => ({ question: state })), @@ -33,14 +35,13 @@ jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({ refReady: true, setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'), })), - replaceStaticWithAsset: jest.fn(() => 'This is my question'), + // problemEditorConfig: jest.fn(args => ({ problemEditorConfig: args })), })); describe('QuestionWidget', () => { const props = { question: 'This is my question', updateQuestion: jest.fn(), - learningContextId: 'course+org+run', // injected intl: { formatMessage }, }; @@ -54,8 +55,5 @@ describe('QuestionWidget', () => { test('question from problem.question', () => { expect(mapStateToProps(testState).question).toEqual(selectors.problem.question(testState)); }); - test('learningContextId from app.learningContextId', () => { - expect(mapStateToProps(testState).learningContextId).toEqual(selectors.app.learningContextId(testState)); - }); }); }); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/hooks.js index 6a11b3f71..5b338f127 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/hooks.js @@ -56,13 +56,14 @@ export const parseState = ({ problem, isAdvanced, ref, + assets, lmsEndpointUrl, }) => () => { const rawOLX = ref?.current?.state.doc.toString(); const editorObject = fetchEditorContent({ format: '' }); const reactOLXParser = new ReactStateOLXParser({ problem, editorObject }); const reactSettingsParser = new ReactStateSettingsParser({ problem, rawOLX }); - const reactBuiltOlx = setAssetToStaticUrl({ editorValue: reactOLXParser.buildOLX(), lmsEndpointUrl }); + const reactBuiltOlx = setAssetToStaticUrl({ editorValue: reactOLXParser.buildOLX(), assets, lmsEndpointUrl }); return { settings: isAdvanced ? reactSettingsParser.parseRawOlxSettings() : reactSettingsParser.getSettings(), olx: isAdvanced ? rawOLX : reactBuiltOlx, @@ -142,6 +143,7 @@ export const getContent = ({ openSaveWarningModal, isAdvancedProblemType, editorRef, + assets, lmsEndpointUrl, }) => { const problem = problemState; @@ -159,6 +161,7 @@ export const getContent = ({ isAdvanced: isAdvancedProblemType, ref: editorRef, problem, + assets, lmsEndpointUrl, })(); return data; diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx index e05a4ae4b..3b08bd806 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx @@ -29,6 +29,7 @@ export const EditProblemView = ({ // redux problemType, problemState, + assets, lmsEndpointUrl, returnUrl, analytics, @@ -47,6 +48,7 @@ export const EditProblemView = ({ openSaveWarningModal, isAdvancedProblemType, editorRef, + assets, lmsEndpointUrl, })} returnFunction={returnFunction} @@ -68,6 +70,7 @@ export const EditProblemView = ({ problem: problemState, isAdvanced: isAdvancedProblemType, ref: editorRef, + assets, lmsEndpointUrl, })(), returnFunction, @@ -115,6 +118,7 @@ export const EditProblemView = ({ }; EditProblemView.defaultProps = { + assets: null, lmsEndpointUrl: null, returnFunction: null, }; @@ -124,6 +128,7 @@ EditProblemView.propTypes = { returnFunction: PropTypes.func, // eslint-disable-next-line problemState: PropTypes.any.isRequired, + assets: PropTypes.shape({}), analytics: PropTypes.shape({}).isRequired, lmsEndpointUrl: PropTypes.string, returnUrl: PropTypes.string.isRequired, @@ -132,6 +137,7 @@ EditProblemView.propTypes = { }; export const mapStateToProps = (state) => ({ + assets: selectors.app.assets(state), analytics: selectors.app.analytics(state), lmsEndpointUrl: selectors.app.lmsEndpointUrl(state), returnUrl: selectors.app.returnUrl(state), diff --git a/src/editors/containers/ProblemEditor/index.jsx b/src/editors/containers/ProblemEditor/index.jsx index fcb05d194..cbc06adb8 100644 --- a/src/editors/containers/ProblemEditor/index.jsx +++ b/src/editors/containers/ProblemEditor/index.jsx @@ -16,17 +16,19 @@ export const ProblemEditor = ({ problemType, blockFinished, blockFailed, + studioViewFinished, blockValue, initializeProblemEditor, + assetsFinished, advancedSettingsFinished, }) => { React.useEffect(() => { - if (blockFinished && !blockFailed) { + if (blockFinished && studioViewFinished && assetsFinished && !blockFailed) { initializeProblemEditor(blockValue); } - }, [blockFinished, blockFailed]); + }, [blockFinished, studioViewFinished, assetsFinished, blockFailed]); - if (!blockFinished || !advancedSettingsFinished) { + if (!blockFinished || !studioViewFinished || !assetsFinished || !advancedSettingsFinished) { return (
({ blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }), blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }), + studioViewFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchStudioView }), problemType: selectors.problem.problemType(state), blockValue: selectors.app.blockValue(state), + assetsFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchAssets }), advancedSettingsFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchAdvancedSettings }), }); diff --git a/src/editors/containers/ProblemEditor/index.test.jsx b/src/editors/containers/ProblemEditor/index.test.jsx index 6c171b22a..954378fec 100644 --- a/src/editors/containers/ProblemEditor/index.test.jsx +++ b/src/editors/containers/ProblemEditor/index.test.jsx @@ -45,7 +45,9 @@ describe('ProblemEditor', () => { blockValue: { data: { data: 'eDiTablE Text' } }, blockFinished: false, blockFailed: false, + studioViewFinished: false, initializeProblemEditor: jest.fn().mockName('args.intializeProblemEditor'), + assetsFinished: false, advancedSettingsFinished: false, }; describe('snapshots', () => { @@ -56,6 +58,14 @@ describe('ProblemEditor', () => { const wrapper = shallow(); expect(wrapper.instance.findByType(Spinner)).toBeTruthy(); }); + test('studio view loaded, block and assets not yet loaded, Spinner appears', () => { + const wrapper = shallow(); + expect(wrapper.instance.findByType(Spinner)).toBeTruthy(); + }); + test('assets loaded, block and studio view not yet loaded, Spinner appears', () => { + const wrapper = shallow(); + expect(wrapper.instance.findByType(Spinner)).toBeTruthy(); + }); test('advanceSettings loaded, block and studio view not yet loaded, Spinner appears', () => { const wrapper = shallow(); expect(wrapper.instance.findByType(Spinner)).toBeTruthy(); @@ -65,6 +75,7 @@ describe('ProblemEditor', () => { {...props} blockFinished studioViewFinished + assetsFinished advancedSettingsFinished blockFailed />); @@ -75,6 +86,7 @@ describe('ProblemEditor', () => { {...props} blockFinished studioViewFinished + assetsFinished advancedSettingsFinished />); expect(wrapper.instance.findByType('SelectTypeModal')).toHaveLength(1); @@ -85,6 +97,7 @@ describe('ProblemEditor', () => { problemType="multiplechoiceresponse" blockFinished studioViewFinished + assetsFinished advancedSettingsFinished />); expect(wrapper.instance.findByType('EditProblemView')).toHaveLength(1); @@ -108,6 +121,16 @@ describe('ProblemEditor', () => { mapStateToProps(testState).blockFinished, ).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchBlock })); }); + test('studioViewFinished from requests.isFinished', () => { + expect( + mapStateToProps(testState).studioViewFinished, + ).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchStudioView })); + }); + test('assetsFinished from requests.isFinished', () => { + expect( + mapStateToProps(testState).assetsFinished, + ).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchAssets })); + }); test('advancedSettingsFinished from requests.isFinished', () => { expect( mapStateToProps(testState).advancedSettingsFinished, diff --git a/src/editors/containers/TextEditor/__snapshots__/index.test.jsx.snap b/src/editors/containers/TextEditor/__snapshots__/index.test.jsx.snap index 3273658c8..2e95bf873 100644 --- a/src/editors/containers/TextEditor/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/TextEditor/__snapshots__/index.test.jsx.snap @@ -5,12 +5,17 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = ` getContent={ { "getContent": { + "assets": { + "sOmEaSsET": { + "staTICUrl": "/assets/sOmEaSsET", + }, + }, "editorRef": { "current": { "value": "something", }, }, - "showRawEditor": false, + "isRaw": false, }, } } @@ -54,12 +59,17 @@ exports[`TextEditor snapshots loaded, raw editor 1`] = ` getContent={ { "getContent": { + "assets": { + "sOmEaSsET": { + "staTICUrl": "/assets/sOmEaSsET", + }, + }, "editorRef": { "current": { "value": "something", }, }, - "showRawEditor": true, + "isRaw": true, }, } } @@ -105,12 +115,17 @@ exports[`TextEditor snapshots not yet loaded, Spinner appears 1`] = ` getContent={ { "getContent": { + "assets": { + "sOmEaSsET": { + "staTICUrl": "/assets/sOmEaSsET", + }, + }, "editorRef": { "current": { "value": "something", }, }, - "showRawEditor": false, + "isRaw": false, }, } } @@ -148,12 +163,17 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = ` getContent={ { "getContent": { + "assets": { + "sOmEaSsET": { + "staTICUrl": "/assets/sOmEaSsET", + }, + }, "editorRef": { "current": { "value": "something", }, }, - "showRawEditor": false, + "isRaw": false, }, } } diff --git a/src/editors/containers/TextEditor/hooks.js b/src/editors/containers/TextEditor/hooks.js index 3d725f114..2f4d74b89 100644 --- a/src/editors/containers/TextEditor/hooks.js +++ b/src/editors/containers/TextEditor/hooks.js @@ -3,9 +3,9 @@ import { setAssetToStaticUrl } from '../../sharedComponents/TinyMceWidget/hooks' export const { nullMethod, navigateCallback, navigateTo } = appHooks; -export const getContent = ({ editorRef, showRawEditor }) => () => { - const content = (showRawEditor && editorRef && editorRef.current +export const getContent = ({ editorRef, isRaw, assets }) => () => { + const content = (isRaw && editorRef && editorRef.current ? editorRef.current.state.doc.toString() : editorRef.current?.getContent()); - return setAssetToStaticUrl({ editorValue: content }); + return setAssetToStaticUrl({ editorValue: content, assets }); }; diff --git a/src/editors/containers/TextEditor/hooks.test.jsx b/src/editors/containers/TextEditor/hooks.test.jsx index 0580139fa..e2fa7722f 100644 --- a/src/editors/containers/TextEditor/hooks.test.jsx +++ b/src/editors/containers/TextEditor/hooks.test.jsx @@ -43,17 +43,18 @@ describe('TextEditor hooks', () => { tinyMceHooks, tinyMceHookKeys.setAssetToStaticUrl, ).mockReturnValueOnce(rawContent); - test('returns correct content based on showRawEditor equals false', () => { - const getContent = module.getContent({ editorRef, showRawEditor: false })(); + const assets = []; + test('returns correct content based on isRaw equals false', () => { + const getContent = module.getContent({ editorRef, isRaw: false, assets })(); expect(spies.visualHtml.mock.calls.length).toEqual(1); - expect(spies.visualHtml).toHaveBeenCalledWith({ editorValue: visualContent }); + expect(spies.visualHtml).toHaveBeenCalledWith({ editorValue: visualContent, assets }); expect(getContent).toEqual(visualContent); }); - test('returns correct content based on showRawEditor equals true', () => { + test('returns correct content based on isRaw equals true', () => { jest.clearAllMocks(); - const getContent = module.getContent({ editorRef, showRawEditor: true })(); + const getContent = module.getContent({ editorRef, isRaw: true, assets })(); expect(spies.rawHtml.mock.calls.length).toEqual(1); - expect(spies.rawHtml).toHaveBeenCalledWith({ editorValue: rawContent }); + expect(spies.rawHtml).toHaveBeenCalledWith({ editorValue: rawContent, assets }); expect(getContent).toEqual(rawContent); }); }); diff --git a/src/editors/containers/TextEditor/index.jsx b/src/editors/containers/TextEditor/index.jsx index a8e0a9286..521abbf8e 100644 --- a/src/editors/containers/TextEditor/index.jsx +++ b/src/editors/containers/TextEditor/index.jsx @@ -16,31 +16,27 @@ import RawEditor from '../../sharedComponents/RawEditor'; import * as hooks from './hooks'; import messages from './messages'; import TinyMceWidget from '../../sharedComponents/TinyMceWidget'; -import { prepareEditorRef, replaceStaticWithAsset } from '../../sharedComponents/TinyMceWidget/hooks'; +import { prepareEditorRef } from '../../sharedComponents/TinyMceWidget/hooks'; export const TextEditor = ({ onClose, returnFunction, // redux - showRawEditor, + isRaw, blockValue, blockFailed, initializeEditor, - blockFinished, - learningContextId, + assetsFinished, + assets, // inject intl, }) => { const { editorRef, refReady, setEditorRef } = prepareEditorRef(); - const editorContent = blockValue ? replaceStaticWithAsset({ - initialContent: blockValue.data.data, - learningContextId, - }) : ''; if (!refReady) { return null; } const selectEditor = () => { - if (showRawEditor) { + if (isRaw) { return ( @@ -72,7 +68,7 @@ export const TextEditor = ({ - {(!blockFinished) + {(!assetsFinished) ? (
({ blockValue: selectors.app.blockValue(state), blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }), - showRawEditor: selectors.app.showRawEditor(state), - blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }), - learningContextId: selectors.app.learningContextId(state), + isRaw: selectors.app.isRaw(state), + assetsFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchAssets }), + assets: selectors.app.assets(state), }); export const mapDispatchToProps = { diff --git a/src/editors/containers/TextEditor/index.test.jsx b/src/editors/containers/TextEditor/index.test.jsx index 9268d533e..16465c8ed 100644 --- a/src/editors/containers/TextEditor/index.test.jsx +++ b/src/editors/containers/TextEditor/index.test.jsx @@ -30,7 +30,6 @@ jest.mock('../../sharedComponents/TinyMceWidget/hooks', () => ({ refReady: true, setEditorRef: jest.fn().mockName('hooks.prepareEditorRef.setEditorRef'), })), - replaceStaticWithAsset: jest.fn(() => 'eDiTablE Text'), })); jest.mock('react', () => { @@ -55,9 +54,9 @@ jest.mock('../../data/redux', () => ({ blockValue: jest.fn(state => ({ blockValue: state })), lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })), studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })), - showRawEditor: jest.fn(state => ({ showRawEditor: state })), + isRaw: jest.fn(state => ({ isRaw: state })), isLibrary: jest.fn(state => ({ isLibrary: state })), - learningContextId: jest.fn(state => ({ learningContextId: state })), + assets: jest.fn(state => ({ assets: state })), }, requests: { isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })), @@ -78,9 +77,9 @@ describe('TextEditor', () => { blockValue: { data: { data: 'eDiTablE Text' } }, blockFailed: false, initializeEditor: jest.fn().mockName('args.intializeEditor'), - showRawEditor: false, - blockFinished: true, - learningContextId: 'course+org+run', + isRaw: false, + assetsFinished: true, + assets: { sOmEaSsET: { staTICUrl: '/assets/sOmEaSsET' } }, // inject intl: { formatMessage }, }; @@ -89,10 +88,10 @@ describe('TextEditor', () => { expect(shallow().snapshot).toMatchSnapshot(); }); test('not yet loaded, Spinner appears', () => { - expect(shallow().snapshot).toMatchSnapshot(); + expect(shallow().snapshot).toMatchSnapshot(); }); test('loaded, raw editor', () => { - expect(shallow().snapshot).toMatchSnapshot(); + expect(shallow().snapshot).toMatchSnapshot(); }); test('block failed to load, Toast is shown', () => { expect(shallow().snapshot).toMatchSnapshot(); @@ -106,20 +105,20 @@ describe('TextEditor', () => { mapStateToProps(testState).blockValue, ).toEqual(selectors.app.blockValue(testState)); }); + test('assets from app.assets', () => { + expect( + mapStateToProps(testState).assets, + ).toEqual(selectors.app.assets(testState)); + }); test('blockFailed from requests.isFailed', () => { expect( mapStateToProps(testState).blockFailed, ).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.fetchBlock })); }); - test('blockFinished from requests.isFinished', () => { + test('assetssFinished from requests.isFinished', () => { expect( - mapStateToProps(testState).blockFinished, - ).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchBlock })); - }); - test('learningContextId from app.learningContextId', () => { - expect( - mapStateToProps(testState).learningContextId, - ).toEqual(selectors.app.learningContextId(testState)); + mapStateToProps(testState).assetsFinished, + ).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchAssets })); }); }); diff --git a/src/editors/data/constants/requests.js b/src/editors/data/constants/requests.js index ec736f27e..989661420 100644 --- a/src/editors/data/constants/requests.js +++ b/src/editors/data/constants/requests.js @@ -8,12 +8,14 @@ export const RequestStates = StrictDict({ }); export const RequestKeys = StrictDict({ + fetchAssets: 'fetchAssets', fetchVideos: 'fetchVideos', fetchBlock: 'fetchBlock', fetchImages: 'fetchImages', fetchUnit: 'fetchUnit', fetchStudioView: 'fetchStudioView', saveBlock: 'saveBlock', + uploadAsset: 'uploadAsset', uploadVideo: 'uploadVideo', allowThumbnailUpload: 'allowThumbnailUpload', uploadThumbnail: 'uploadThumbnail', @@ -24,7 +26,7 @@ export const RequestKeys = StrictDict({ getTranscriptFile: 'getTranscriptFile', checkTranscriptsForImport: 'checkTranscriptsForImport', importTranscript: 'importTranscript', - uploadAsset: 'uploadAsset', + uploadImage: 'uploadImage', fetchAdvancedSettings: 'fetchAdvancedSettings', fetchVideoFeatures: 'fetchVideoFeatures', }); diff --git a/src/editors/data/redux/app/reducer.js b/src/editors/data/redux/app/reducer.js index 1951983f4..405c9b860 100644 --- a/src/editors/data/redux/app/reducer.js +++ b/src/editors/data/redux/app/reducer.js @@ -15,11 +15,9 @@ const initialState = { editorInitialized: false, studioEndpointUrl: null, lmsEndpointUrl: null, - images: {}, - imageCount: 0, + assets: {}, videos: {}, courseDetails: {}, - showRawEditor: false, }; // eslint-disable-next-line no-unused-vars @@ -42,22 +40,15 @@ const app = createSlice({ blockValue: payload, blockTitle: payload.data.display_name, }), + setStudioView: (state, { payload }) => ({ ...state, studioView: payload }), setBlockContent: (state, { payload }) => ({ ...state, blockContent: payload }), setBlockTitle: (state, { payload }) => ({ ...state, blockTitle: payload }), setSaveResponse: (state, { payload }) => ({ ...state, saveResponse: payload }), initializeEditor: (state) => ({ ...state, editorInitialized: true }), - setImages: (state, { payload }) => ({ - ...state, - images: { ...state.images, ...payload.images }, - imageCount: payload.imageCount, - }), + setAssets: (state, { payload }) => ({ ...state, assets: payload }), setVideos: (state, { payload }) => ({ ...state, videos: payload }), setCourseDetails: (state, { payload }) => ({ ...state, courseDetails: payload }), - setShowRawEditor: (state, { payload }) => ({ - ...state, - showRawEditor: payload.data?.metadata?.editor === 'raw', - }), }, }); diff --git a/src/editors/data/redux/app/reducer.test.js b/src/editors/data/redux/app/reducer.test.js index c125185d8..f23e8c14c 100644 --- a/src/editors/data/redux/app/reducer.test.js +++ b/src/editors/data/redux/app/reducer.test.js @@ -47,18 +47,10 @@ describe('app reducer', () => { ['setBlockContent', 'blockContent'], ['setBlockTitle', 'blockTitle'], ['setSaveResponse', 'saveResponse'], + ['setAssets', 'assets'], ['setVideos', 'videos'], ['setCourseDetails', 'courseDetails'], ].map(args => setterTest(...args)); - describe('setShowRawEditor', () => { - it('sets showRawEditor', () => { - const blockValue = { data: { metadata: { editor: 'raw' } } }; - expect(reducer(testingState, actions.setShowRawEditor(blockValue))).toEqual({ - ...testingState, - showRawEditor: true, - }); - }); - }); describe('setBlockValue', () => { it('sets blockValue, as well as setting the blockTitle from data.display_name', () => { const blockValue = { data: { display_name: 'my test name' }, other: 'data' }; @@ -69,16 +61,6 @@ describe('app reducer', () => { }); }); }); - describe('setImages', () => { - it('sets images, as well as setting imageCount', () => { - const imageData = { images: { id1: { id: 'id1' } }, imageCount: 1 }; - expect(reducer(testingState, actions.setImages(imageData))).toEqual({ - ...testingState, - images: imageData.images, - imageCount: imageData.imageCount, - }); - }); - }); describe('initializeEditor', () => { it('sets editorInitialized to true', () => { expect(reducer(testingState, actions.initializeEditor())).toEqual({ diff --git a/src/editors/data/redux/app/selectors.js b/src/editors/data/redux/app/selectors.js index c9d23c2e0..870739781 100644 --- a/src/editors/data/redux/app/selectors.js +++ b/src/editors/data/redux/app/selectors.js @@ -21,9 +21,8 @@ export const simpleSelectors = { studioEndpointUrl: mkSimpleSelector(app => app.studioEndpointUrl), unitUrl: mkSimpleSelector(app => app.unitUrl), blockTitle: mkSimpleSelector(app => app.blockTitle), - images: mkSimpleSelector(app => app.images), + assets: mkSimpleSelector(app => app.assets), videos: mkSimpleSelector(app => app.videos), - showRawEditor: mkSimpleSelector(app => app.showRawEditor), }; export const returnUrl = createSelector( @@ -73,6 +72,23 @@ export const analytics = createSelector( ), ); +export const isRaw = createSelector( + [module.simpleSelectors.studioView], + (studioView) => { + if (!studioView?.data) { + return null; + } + const { html, content } = studioView.data; + if (html && html.includes('data-editor="raw"')) { + return true; + } + if (content && content.includes('data-editor="raw"')) { + return true; + } + return false; + }, +); + export const isLibrary = createSelector( [ module.simpleSelectors.learningContextId, @@ -95,5 +111,6 @@ export default { returnUrl, displayTitle, analytics, + isRaw, isLibrary, }; diff --git a/src/editors/data/redux/app/selectors.test.js b/src/editors/data/redux/app/selectors.test.js index 33a022e0b..58bff4d18 100644 --- a/src/editors/data/redux/app/selectors.test.js +++ b/src/editors/data/redux/app/selectors.test.js @@ -47,9 +47,8 @@ describe('app selectors unit tests', () => { simpleKeys.unitUrl, simpleKeys.blockTitle, simpleKeys.studioView, - simpleKeys.images, + simpleKeys.assets, simpleKeys.videos, - simpleKeys.showRawEditor, ].map(testSimpleSelector); }); }); @@ -121,6 +120,41 @@ describe('app selectors unit tests', () => { }); }); + describe('isRaw', () => { + const studioViewCourseRaw = { + data: { + html: 'data-editor="raw"', + }, + }; + const studioViewV2LibraryRaw = { + data: { + content: 'data-editor="raw"', + }, + }; + const studioViewVisual = { + data: { + html: 'sOmEthIngElse', + }, + }; + it('is memoized based on studioView', () => { + expect(selectors.isRaw.preSelectors).toEqual([ + simpleSelectors.studioView, + ]); + }); + it('returns null if studioView is null', () => { + expect(selectors.isRaw.cb(null)).toEqual(null); + }); + it('returns true if course studioView is raw', () => { + expect(selectors.isRaw.cb(studioViewCourseRaw)).toEqual(true); + }); + it('returns true if v2 library studioView is raw', () => { + expect(selectors.isRaw.cb(studioViewV2LibraryRaw)).toEqual(true); + }); + it('returns false if the studioView is not Raw', () => { + expect(selectors.isRaw.cb(studioViewVisual)).toEqual(false); + }); + }); + describe('isLibrary', () => { const learningContextIdLibrary = 'library-v1:name'; const learningContextIdCourse = 'course-v1:name'; diff --git a/src/editors/data/redux/requests/reducer.js b/src/editors/data/redux/requests/reducer.js index 583c39064..15274560f 100644 --- a/src/editors/data/redux/requests/reducer.js +++ b/src/editors/data/redux/requests/reducer.js @@ -15,7 +15,7 @@ const initialState = { [RequestKeys.uploadTranscript]: { status: RequestStates.inactive }, [RequestKeys.deleteTranscript]: { status: RequestStates.inactive }, [RequestKeys.fetchCourseDetails]: { status: RequestStates.inactive }, - [RequestKeys.fetchImages]: { status: RequestStates.inactive }, + [RequestKeys.fetchAssets]: { status: RequestStates.inactive }, [RequestKeys.fetchVideos]: { status: RequestStates.inactive }, [RequestKeys.uploadVideo]: { status: RequestStates.inactive }, [RequestKeys.checkTranscriptsForImport]: { status: RequestStates.inactive }, diff --git a/src/editors/data/redux/thunkActions/app.js b/src/editors/data/redux/thunkActions/app.js index a248279d2..9c4e4aea6 100644 --- a/src/editors/data/redux/thunkActions/app.js +++ b/src/editors/data/redux/thunkActions/app.js @@ -7,10 +7,7 @@ import { RequestKeys } from '../../constants/requests'; export const fetchBlock = () => (dispatch) => { dispatch(requests.fetchBlock({ - onSuccess: (response) => { - dispatch(actions.app.setBlockValue(response)); - dispatch(actions.app.setShowRawEditor(response)); - }, + onSuccess: (response) => dispatch(actions.app.setBlockValue(response)), onFailure: (error) => dispatch(actions.requests.failRequest({ requestKey: RequestKeys.fetchBlock, error, @@ -38,12 +35,11 @@ export const fetchUnit = () => (dispatch) => { })); }; -export const fetchImages = ({ pageNumber }) => (dispatch) => { - dispatch(requests.fetchImages({ - pageNumber, - onSuccess: ({ images, imageCount }) => dispatch(actions.app.setImages({ images, imageCount })), +export const fetchAssets = () => (dispatch) => { + dispatch(requests.fetchAssets({ + onSuccess: (response) => dispatch(actions.app.setAssets(response)), onFailure: (error) => dispatch(actions.requests.failRequest({ - requestKey: RequestKeys.fetchImages, + requestKey: RequestKeys.fetchAssets, error, })), })); @@ -76,25 +72,13 @@ export const fetchCourseDetails = () => (dispatch) => { * @param {string} blockType */ export const initialize = (data) => (dispatch) => { - const editorType = data.blockType; dispatch(actions.app.initialize(data)); dispatch(module.fetchBlock()); dispatch(module.fetchUnit()); - switch (editorType) { - case 'problem': - dispatch(module.fetchImages({ pageNumber: 0 })); - break; - case 'video': - dispatch(module.fetchVideos()); - dispatch(module.fetchStudioView()); - dispatch(module.fetchCourseDetails()); - break; - case 'html': - dispatch(module.fetchImages({ pageNumber: 0 })); - break; - default: - break; - } + dispatch(module.fetchStudioView()); + dispatch(module.fetchAssets()); + dispatch(module.fetchVideos()); + dispatch(module.fetchCourseDetails()); }; /** @@ -111,7 +95,7 @@ export const saveBlock = (content, returnToUnit) => (dispatch) => { })); }; -export const uploadAsset = ({ file, setSelection }) => (dispatch) => { +export const uploadImage = ({ file, setSelection }) => (dispatch) => { dispatch(requests.uploadAsset({ asset: file, onSuccess: (response) => setSelection(camelizeKeys(response.data.asset)), @@ -126,6 +110,6 @@ export default StrictDict({ fetchVideos, initialize, saveBlock, - fetchImages, - uploadAsset, + fetchAssets, + uploadImage, }); diff --git a/src/editors/data/redux/thunkActions/app.test.js b/src/editors/data/redux/thunkActions/app.test.js index 2c962b285..dce0a212d 100644 --- a/src/editors/data/redux/thunkActions/app.test.js +++ b/src/editors/data/redux/thunkActions/app.test.js @@ -10,7 +10,7 @@ jest.mock('./requests', () => ({ saveBlock: (args) => ({ saveBlock: args }), uploadAsset: (args) => ({ uploadAsset: args }), fetchStudioView: (args) => ({ fetchStudioView: args }), - fetchImages: (args) => ({ fetchImages: args }), + fetchAssets: (args) => ({ fetchAssets: args }), fetchVideos: (args) => ({ fetchVideos: args }), fetchCourseDetails: (args) => ({ fetchCourseDetails: args }), })); @@ -101,24 +101,24 @@ describe('app thunkActions', () => { })); }); }); - describe('fetchImages', () => { + describe('fetchAssets', () => { beforeEach(() => { - thunkActions.fetchImages({ pageNumber: 0 })(dispatch); + thunkActions.fetchAssets()(dispatch); [[dispatchedAction]] = dispatch.mock.calls; }); - it('dispatches fetchImages action', () => { - expect(dispatchedAction.fetchImages).not.toEqual(undefined); + it('dispatches fetchAssets action', () => { + expect(dispatchedAction.fetchAssets).not.toEqual(undefined); }); - it('dispatches actions.app.setImages on success', () => { + it('dispatches actions.app.setAssets on success', () => { dispatch.mockClear(); - dispatchedAction.fetchImages.onSuccess({ images: {}, imageCount: 0 }); - expect(dispatch).toHaveBeenCalledWith(actions.app.setImages({ images: {}, imageCount: 0 })); + dispatchedAction.fetchAssets.onSuccess(testValue); + expect(dispatch).toHaveBeenCalledWith(actions.app.setAssets(testValue)); }); - it('dispatches failRequest with fetchImages requestKey on failure', () => { + it('dispatches failRequest with fetchAssets requestKey on failure', () => { dispatch.mockClear(); - dispatchedAction.fetchImages.onFailure(testValue); + dispatchedAction.fetchAssets.onFailure(testValue); expect(dispatch).toHaveBeenCalledWith(actions.requests.failRequest({ - requestKey: RequestKeys.fetchImages, + requestKey: RequestKeys.fetchAssets, error: testValue, })); }); @@ -128,7 +128,7 @@ describe('app thunkActions', () => { thunkActions.fetchVideos()(dispatch); [[dispatchedAction]] = dispatch.mock.calls; }); - it('dispatches fetchImages action', () => { + it('dispatches fetchAssets action', () => { expect(dispatchedAction.fetchVideos).not.toEqual(undefined); }); it('dispatches actions.app.setVideos on success', () => { @@ -167,20 +167,20 @@ describe('app thunkActions', () => { })); }); }); - describe('initialize without block type defined', () => { + describe('initialize', () => { it('dispatches actions.app.initialize, and then fetches both block and unit', () => { const { fetchBlock, fetchUnit, fetchStudioView, - fetchImages, + fetchAssets, fetchVideos, fetchCourseDetails, } = thunkActions; thunkActions.fetchBlock = () => 'fetchBlock'; thunkActions.fetchUnit = () => 'fetchUnit'; thunkActions.fetchStudioView = () => 'fetchStudioView'; - thunkActions.fetchImages = () => 'fetchImages'; + thunkActions.fetchAssets = () => 'fetchAssets'; thunkActions.fetchVideos = () => 'fetchVideos'; thunkActions.fetchCourseDetails = () => 'fetchCourseDetails'; thunkActions.initialize(testValue)(dispatch); @@ -188,118 +188,15 @@ describe('app thunkActions', () => { [actions.app.initialize(testValue)], [thunkActions.fetchBlock()], [thunkActions.fetchUnit()], - ]); - thunkActions.fetchBlock = fetchBlock; - thunkActions.fetchUnit = fetchUnit; - thunkActions.fetchStudioView = fetchStudioView; - thunkActions.fetchImages = fetchImages; - thunkActions.fetchVideos = fetchVideos; - thunkActions.fetchCourseDetails = fetchCourseDetails; - }); - }); - describe('initialize with block type html', () => { - it('dispatches actions.app.initialize, and then fetches both block and unit', () => { - const { - fetchBlock, - fetchUnit, - fetchStudioView, - fetchImages, - fetchVideos, - fetchCourseDetails, - } = thunkActions; - thunkActions.fetchBlock = () => 'fetchBlock'; - thunkActions.fetchUnit = () => 'fetchUnit'; - thunkActions.fetchStudioView = () => 'fetchStudioView'; - thunkActions.fetchImages = () => 'fetchImages'; - thunkActions.fetchVideos = () => 'fetchVideos'; - thunkActions.fetchCourseDetails = () => 'fetchCourseDetails'; - const data = { - ...testValue, - blockType: 'html', - }; - thunkActions.initialize(data)(dispatch); - expect(dispatch.mock.calls).toEqual([ - [actions.app.initialize(data)], - [thunkActions.fetchBlock()], - [thunkActions.fetchUnit()], - [thunkActions.fetchImages()], - ]); - thunkActions.fetchBlock = fetchBlock; - thunkActions.fetchUnit = fetchUnit; - thunkActions.fetchStudioView = fetchStudioView; - thunkActions.fetchImages = fetchImages; - thunkActions.fetchVideos = fetchVideos; - thunkActions.fetchCourseDetails = fetchCourseDetails; - }); - }); - describe('initialize with block type problem', () => { - it('dispatches actions.app.initialize, and then fetches both block and unit', () => { - const { - fetchBlock, - fetchUnit, - fetchStudioView, - fetchImages, - fetchVideos, - fetchCourseDetails, - } = thunkActions; - thunkActions.fetchBlock = () => 'fetchBlock'; - thunkActions.fetchUnit = () => 'fetchUnit'; - thunkActions.fetchStudioView = () => 'fetchStudioView'; - thunkActions.fetchImages = () => 'fetchImages'; - thunkActions.fetchVideos = () => 'fetchVideos'; - thunkActions.fetchCourseDetails = () => 'fetchCourseDetails'; - const data = { - ...testValue, - blockType: 'problem', - }; - thunkActions.initialize(data)(dispatch); - expect(dispatch.mock.calls).toEqual([ - [actions.app.initialize(data)], - [thunkActions.fetchBlock()], - [thunkActions.fetchUnit()], - [thunkActions.fetchImages()], - ]); - thunkActions.fetchBlock = fetchBlock; - thunkActions.fetchUnit = fetchUnit; - thunkActions.fetchStudioView = fetchStudioView; - thunkActions.fetchImages = fetchImages; - thunkActions.fetchVideos = fetchVideos; - thunkActions.fetchCourseDetails = fetchCourseDetails; - }); - }); - describe('initialize with block type video', () => { - it('dispatches actions.app.initialize, and then fetches both block and unit', () => { - const { - fetchBlock, - fetchUnit, - fetchStudioView, - fetchImages, - fetchVideos, - fetchCourseDetails, - } = thunkActions; - thunkActions.fetchBlock = () => 'fetchBlock'; - thunkActions.fetchUnit = () => 'fetchUnit'; - thunkActions.fetchStudioView = () => 'fetchStudioView'; - thunkActions.fetchImages = () => 'fetchImages'; - thunkActions.fetchVideos = () => 'fetchVideos'; - thunkActions.fetchCourseDetails = () => 'fetchCourseDetails'; - const data = { - ...testValue, - blockType: 'video', - }; - thunkActions.initialize(data)(dispatch); - expect(dispatch.mock.calls).toEqual([ - [actions.app.initialize(data)], - [thunkActions.fetchBlock()], - [thunkActions.fetchUnit()], - [thunkActions.fetchVideos()], [thunkActions.fetchStudioView()], + [thunkActions.fetchAssets()], + [thunkActions.fetchVideos()], [thunkActions.fetchCourseDetails()], ]); thunkActions.fetchBlock = fetchBlock; thunkActions.fetchUnit = fetchUnit; thunkActions.fetchStudioView = fetchStudioView; - thunkActions.fetchImages = fetchImages; + thunkActions.fetchAssets = fetchAssets; thunkActions.fetchVideos = fetchVideos; thunkActions.fetchCourseDetails = fetchCourseDetails; }); @@ -328,10 +225,10 @@ describe('app thunkActions', () => { expect(returnToUnit).toHaveBeenCalled(); }); }); - describe('uploadAsset', () => { + describe('uploadImage', () => { const setSelection = jest.fn(); beforeEach(() => { - thunkActions.uploadAsset({ file: testValue, setSelection })(dispatch); + thunkActions.uploadImage({ file: testValue, setSelection })(dispatch); [[dispatchedAction]] = dispatch.mock.calls; }); it('dispatches uploadAsset action', () => { diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index f419c9a4b..b0cb34eb2 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -124,16 +124,15 @@ export const uploadAsset = ({ asset, ...rest }) => (dispatch, getState) => { })); }; -export const fetchImages = ({ pageNumber, ...rest }) => (dispatch, getState) => { +export const fetchAssets = ({ ...rest }) => (dispatch, getState) => { dispatch(module.networkRequest({ - requestKey: RequestKeys.fetchImages, + requestKey: RequestKeys.fetchAssets, promise: api - .fetchImages({ - pageNumber, + .fetchAssets({ studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), learningContextId: selectors.app.learningContextId(getState()), }) - .then(({ data }) => ({ images: loadImages(data.assets), imageCount: data.totalCount })), + .then((response) => loadImages(response.data.assets)), ...rest, })); }; @@ -313,7 +312,7 @@ export default StrictDict({ fetchStudioView, fetchUnit, saveBlock, - fetchImages, + fetchAssets, fetchVideos, uploadAsset, allowThumbnailUpload, diff --git a/src/editors/data/redux/thunkActions/requests.test.js b/src/editors/data/redux/thunkActions/requests.test.js index 4b5961b9e..7525498aa 100644 --- a/src/editors/data/redux/thunkActions/requests.test.js +++ b/src/editors/data/redux/thunkActions/requests.test.js @@ -26,7 +26,7 @@ jest.mock('../../services/cms/api', () => ({ fetchByUnitId: ({ id, url }) => ({ id, url }), fetchCourseDetails: (args) => args, saveBlock: (args) => args, - fetchImages: ({ id, url }) => ({ id, url }), + fetchAssets: ({ id, url }) => ({ id, url }), fetchVideos: ({ id, url }) => ({ id, url }), uploadAsset: (args) => args, loadImages: jest.fn(), @@ -237,8 +237,8 @@ describe('requests thunkActions module', () => { }, }); }); - describe('fetchImages', () => { - let fetchImages; + describe('fetchAssets', () => { + let fetchAssets; let loadImages; let dispatchedAction; const expectedArgs = { @@ -246,12 +246,12 @@ describe('requests thunkActions module', () => { learningContextId: selectors.app.learningContextId(testState), }; beforeEach(() => { - fetchImages = jest.fn((args) => new Promise((resolve) => { - resolve({ data: { assets: { fetchImages: args } } }); + fetchAssets = jest.fn((args) => new Promise((resolve) => { + resolve({ data: { assets: { fetchAssets: args } } }); })); - jest.spyOn(api, apiKeys.fetchImages).mockImplementationOnce(fetchImages); + jest.spyOn(api, apiKeys.fetchAssets).mockImplementationOnce(fetchAssets); loadImages = jest.spyOn(api, apiKeys.loadImages).mockImplementationOnce(() => ({})); - requests.fetchImages({ ...fetchParams, onSuccess, onFailure })(dispatch, () => testState); + requests.fetchAssets({ ...fetchParams, onSuccess, onFailure })(dispatch, () => testState); [[dispatchedAction]] = dispatch.mock.calls; }); it('dispatches networkRequest', () => { @@ -261,11 +261,11 @@ describe('requests thunkActions module', () => { expect(dispatchedAction.networkRequest.onSuccess).toEqual(onSuccess); expect(dispatchedAction.networkRequest.onFailure).toEqual(onFailure); }); - test('api.fetchImages promise called with studioEndpointUrl and learningContextId', () => { - expect(fetchImages).toHaveBeenCalledWith(expectedArgs); + test('api.fetchAssets promise called with studioEndpointUrl and learningContextId', () => { + expect(fetchAssets).toHaveBeenCalledWith(expectedArgs); }); test('promise is chained with api.loadImages', () => { - expect(loadImages).toHaveBeenCalledWith({ fetchImages: expectedArgs }); + expect(loadImages).toHaveBeenCalledWith({ fetchAssets: expectedArgs }); }); }); describe('fetchVideos', () => { diff --git a/src/editors/data/services/cms/api.js b/src/editors/data/services/cms/api.js index 38abad715..b97ea81c1 100644 --- a/src/editors/data/services/cms/api.js +++ b/src/editors/data/services/cms/api.js @@ -26,16 +26,9 @@ export const apiMethods = { fetchStudioView: ({ blockId, studioEndpointUrl }) => get( urls.blockStudioView({ studioEndpointUrl, blockId }), ), - fetchImages: ({ learningContextId, studioEndpointUrl, pageNumber }) => { - const params = { - asset_type: 'Images', - page: pageNumber, - }; - return get( - `${urls.courseAssets({ studioEndpointUrl, learningContextId })}`, - { params }, - ); - }, + fetchAssets: ({ learningContextId, studioEndpointUrl }) => get( + urls.courseAssets({ studioEndpointUrl, learningContextId }), + ), fetchVideos: ({ studioEndpointUrl, learningContextId }) => get( urls.courseVideos({ studioEndpointUrl, learningContextId }), ), diff --git a/src/editors/data/services/cms/api.test.js b/src/editors/data/services/cms/api.test.js index 116efbf62..2edb7ea7b 100644 --- a/src/editors/data/services/cms/api.test.js +++ b/src/editors/data/services/cms/api.test.js @@ -126,17 +126,10 @@ describe('cms api', () => { }); }); - describe('fetchImages', () => { + describe('fetchAssets', () => { it('should call get with url.courseAssets', () => { - apiMethods.fetchImages({ learningContextId, studioEndpointUrl, pageNumber: 0 }); - const params = { - asset_type: 'Images', - page: 0, - }; - expect(get).toHaveBeenCalledWith( - urls.courseAssets({ studioEndpointUrl, learningContextId }), - { params }, - ); + apiMethods.fetchAssets({ learningContextId, studioEndpointUrl }); + expect(get).toHaveBeenCalledWith(urls.courseAssets({ studioEndpointUrl, learningContextId })); }); }); diff --git a/src/editors/data/services/cms/mockApi.js b/src/editors/data/services/cms/mockApi.js index 5c4440b3e..2353bb748 100644 --- a/src/editors/data/services/cms/mockApi.js +++ b/src/editors/data/services/cms/mockApi.js @@ -69,7 +69,7 @@ export const fetchByUnitId = ({ blockId, studioEndpointUrl }) => mockPromise({ data: { ancestors: [{ id: 'unitUrl' }] }, }); // eslint-disable-next-line -export const fetchImages = ({ learningContextId, studioEndpointUrl }) => mockPromise({ +export const fetchAssets = ({ learningContextId, studioEndpointUrl }) => mockPromise({ data: { assets: [ { diff --git a/src/editors/data/services/cms/urls.js b/src/editors/data/services/cms/urls.js index de4e9f158..14b7ff338 100644 --- a/src/editors/data/services/cms/urls.js +++ b/src/editors/data/services/cms/urls.js @@ -52,7 +52,7 @@ export const blockStudioView = ({ studioEndpointUrl, blockId }) => ( ); export const courseAssets = ({ studioEndpointUrl, learningContextId }) => ( - `${studioEndpointUrl}/assets/${learningContextId}/` + `${studioEndpointUrl}/assets/${learningContextId}/?page_size=500` ); export const thumbnailUpload = ({ studioEndpointUrl, learningContextId, videoId }) => ( diff --git a/src/editors/data/services/cms/urls.test.js b/src/editors/data/services/cms/urls.test.js index bbaf1cbcf..e9bbed2f0 100644 --- a/src/editors/data/services/cms/urls.test.js +++ b/src/editors/data/services/cms/urls.test.js @@ -119,7 +119,7 @@ describe('cms url methods', () => { describe('courseAssets', () => { it('returns url with studioEndpointUrl and learningContextId', () => { expect(courseAssets({ studioEndpointUrl, learningContextId })) - .toEqual(`${studioEndpointUrl}/assets/${learningContextId}/`); + .toEqual(`${studioEndpointUrl}/assets/${learningContextId}/?page_size=500`); }); }); describe('thumbnailUpload', () => { diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.js b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.js index 801f9c0dc..efc396762 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.js +++ b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.js @@ -43,14 +43,7 @@ export const displayList = ({ sortBy, searchString, images }) => ( imageList: images, }).sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest])); -export const imgListHooks = ({ - searchSortProps, - setSelection, - images, - imageCount, -}) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const dispatch = useDispatch(); +export const imgListHooks = ({ searchSortProps, setSelection, images }) => { const [highlighted, setHighlighted] = module.state.highlighted(null); const [ showSelectImageError, @@ -80,9 +73,6 @@ export const imgListHooks = ({ highlighted, onHighlightChange: (e) => setHighlighted(e.target.value), emptyGalleryLabel: messages.emptyGalleryLabel, - allowLazyLoad: true, - fetchNextPage: ({ pageNumber }) => dispatch(thunkActions.app.fetchImages({ pageNumber })), - assetCount: imageCount, }, // highlight by id selectBtnProps: { @@ -128,7 +118,7 @@ export const fileInputHooks = ({ setSelection, clearSelection, imgList }) => { }, })) { dispatch( - thunkActions.app.uploadAsset({ + thunkActions.app.uploadImage({ file: selectedFile, setSelection, }), @@ -143,19 +133,9 @@ export const fileInputHooks = ({ setSelection, clearSelection, imgList }) => { }; }; -export const imgHooks = ({ - setSelection, - clearSelection, - images, - imageCount, -}) => { +export const imgHooks = ({ setSelection, clearSelection, images }) => { const searchSortProps = module.searchAndSortHooks(); - const imgList = module.imgListHooks({ - setSelection, - searchSortProps, - images, - imageCount, - }); + const imgList = module.imgListHooks({ setSelection, searchSortProps, images }); const fileInput = module.fileInputHooks({ setSelection, clearSelection, diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.test.js b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.test.js index 2f560e994..7128d857b 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.test.js +++ b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.test.js @@ -27,7 +27,7 @@ jest.mock('react-redux', () => { jest.mock('../../../data/redux', () => ({ thunkActions: { app: { - uploadAsset: jest.fn(), + uploadImage: jest.fn(), }, }, })); @@ -248,7 +248,7 @@ describe('SelectImageModal hooks', () => { hook.click(); expect(click).toHaveBeenCalled(); }); - describe('addFile (uploadAsset args)', () => { + describe('addFile (uploadImage args)', () => { const eventSuccess = { target: { files: [{ value: testValue, size: 2000 }] } }; const eventFailure = { target: { files: [testValueInvalidImage] } }; it('image fails to upload if file size is greater than 1000000', () => { @@ -259,14 +259,14 @@ describe('SelectImageModal hooks', () => { expect(spies.checkValidFileSize.mock.calls.length).toEqual(1); expect(spies.checkValidFileSize).toHaveReturnedWith(false); }); - it('dispatches uploadAsset thunkAction with the first target file and setSelection', () => { + it('dispatches uploadImage thunkAction with the first target file and setSelection', () => { const checkValidFileSize = true; spies.checkValidFileSize = jest.spyOn(hooks, hookKeys.checkValidFileSize) .mockReturnValueOnce(checkValidFileSize); hook.addFile(eventSuccess); expect(spies.checkValidFileSize.mock.calls.length).toEqual(1); expect(spies.checkValidFileSize).toHaveReturnedWith(true); - expect(dispatch).toHaveBeenCalledWith(thunkActions.app.uploadAsset({ + expect(dispatch).toHaveBeenCalledWith(thunkActions.app.uploadImage({ file: testValue, setSelection, })); @@ -281,7 +281,6 @@ describe('SelectImageModal hooks', () => { const searchAndSortHooks = { search: 'props' }; const fileInputHooks = { file: 'input hooks' }; const images = { sOmEuiMAge: { staTICUrl: '/assets/sOmEuiMAge' } }; - const imageCount = 1; const setSelection = jest.fn(); const clearSelection = jest.fn(); @@ -293,11 +292,9 @@ describe('SelectImageModal hooks', () => { .mockReturnValueOnce(searchAndSortHooks); spies.file = jest.spyOn(hooks, hookKeys.fileInputHooks) .mockReturnValueOnce(fileInputHooks); - hook = hooks.imgHooks({ - setSelection, clearSelection, images, imageCount, - }); + hook = hooks.imgHooks({ setSelection, clearSelection, images }); }); - it('forwards fileInputHooks as fileInput, called with uploadAsset prop', () => { + it('forwards fileInputHooks as fileInput, called with uploadImage prop', () => { expect(hook.fileInput).toEqual(fileInputHooks); expect(spies.file.mock.calls.length).toEqual(1); expect(spies.file).toHaveBeenCalledWith({ @@ -310,7 +307,6 @@ describe('SelectImageModal hooks', () => { setSelection, searchSortProps: searchAndSortHooks, images, - imageCount, }); }); it('forwards searchAndSortHooks as searchSortProps', () => { diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx index 262433100..54c75e793 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx +++ b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx @@ -17,7 +17,6 @@ export const SelectImageModal = ({ isLoaded, isFetchError, isUploadError, - imageCount, }) => { const { galleryError, @@ -26,12 +25,7 @@ export const SelectImageModal = ({ galleryProps, searchSortProps, selectBtnProps, - } = hooks.imgHooks({ - setSelection, - clearSelection, - images: images.current, - imageCount, - }); + } = hooks.imgHooks({ setSelection, clearSelection, images: images.current }); const modalMessages = { confirmMsg: messages.nextButtonLabel, @@ -72,14 +66,12 @@ SelectImageModal.propTypes = { isLoaded: PropTypes.bool.isRequired, isFetchError: PropTypes.bool.isRequired, isUploadError: PropTypes.bool.isRequired, - imageCount: PropTypes.number.isRequired, }; export const mapStateToProps = (state) => ({ - isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchImages }), - isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchImages }), + isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchAssets }), + isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchAssets }), isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }), - imageCount: state.app.imageCount, }); export const mapDispatchToProps = {}; diff --git a/src/editors/sharedComponents/SelectionModal/Gallery.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.jsx index 9124ce80d..5c54605e6 100644 --- a/src/editors/sharedComponents/SelectionModal/Gallery.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.jsx @@ -11,7 +11,6 @@ import { import SelectableBox from '../SelectableBox'; import messages from './messages'; import GalleryCard from './GalleryCard'; -import GalleryLoadMoreButton from './GalleryLoadMoreButton'; export const Gallery = ({ galleryIsEmpty, @@ -24,13 +23,9 @@ export const Gallery = ({ height, isLoaded, thumbnailFallback, - allowLazyLoad, - fetchNextPage, - assetCount, }) => { const intl = useIntl(); - - if (!isLoaded && !allowLazyLoad) { + if (!isLoaded) { return (
- {displayList.map(asset => ( + { displayList.map(asset => ( )) } - {allowLazyLoad && ( - - )}
); }; @@ -99,9 +84,6 @@ Gallery.defaultProps = { height: '375px', show: true, thumbnailFallback: undefined, - allowLazyLoad: false, - fetchNextPage: null, - assetCount: 0, }; Gallery.propTypes = { show: PropTypes.bool, @@ -115,9 +97,6 @@ Gallery.propTypes = { showIdsOnCards: PropTypes.bool, height: PropTypes.string, thumbnailFallback: PropTypes.element, - allowLazyLoad: PropTypes.bool, - fetchNextPage: PropTypes.func, - assetCount: PropTypes.number, }; export default Gallery; diff --git a/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx index bddbe3776..efd818be4 100644 --- a/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx @@ -28,9 +28,6 @@ describe('TextEditor Image Gallery component', () => { highlighted: 'props.highlighted', onHighlightChange: jest.fn().mockName('props.onHighlightChange'), isLoaded: true, - fetchNextPage: null, - assetCount: 0, - allowLazyLoad: false, }; const shallowWithIntl = (component) => shallow({component}); test('snapshot: not loaded, show spinner', () => { diff --git a/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx b/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx index 6e73c9fb2..062507eb4 100644 --- a/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx +++ b/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import { Badge, Image, - Truncate, } from '@openedx/paragon'; import { FormattedMessage, FormattedDate, FormattedTime } from '@edx/frontend-platform/i18n'; @@ -65,9 +64,7 @@ export const GalleryCard = ({ )}
-

- {asset.displayName} -

+

{asset.displayName}

{ asset.transcripts && (
{ - const [currentPage, setCurrentPage] = useState(1); - - const handlePageChange = () => { - fetchNextPage({ pageNumber: currentPage }); - setCurrentPage(currentPage + 1); - }; - const buttonState = isLoaded ? 'default' : 'pending'; - const buttonProps = { - labels: { - default: 'Load more', - pending: 'Loading', - }, - icons: { - default: , - pending: , - }, - disabledStates: ['pending'], - variant: 'primary', - }; - - return ( -
- {displayListLength !== assetCount && ( - - )} -
- ); -}; - -GalleryLoadMoreButton.propTypes = { - assetCount: PropTypes.number.isRequired, - displayListLength: PropTypes.number.isRequired, - fetchNextPage: PropTypes.func.isRequired, - currentPage: PropTypes.number.isRequired, - setCurrentPage: PropTypes.func.isRequired, - isLoaded: PropTypes.bool.isRequired, -}; - -export default GalleryLoadMoreButton; diff --git a/src/editors/sharedComponents/SelectionModal/SearchSort.jsx b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx index ac9576faa..57ef43db6 100644 --- a/src/editors/sharedComponents/SelectionModal/SearchSort.jsx +++ b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx @@ -29,7 +29,6 @@ export const SearchSort = ({ onSwitchClick, }) => { const intl = useIntl(); - return ( diff --git a/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap index a0651d470..ad2464da5 100644 --- a/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap @@ -43,8 +43,6 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no im } > - - props.img.displayName - + props.img.displayName

- - props.img.displayName - + props.img.displayName

- - props.img.displayName - + props.img.displayName

- - props.img.displayName - + props.img.displayName

- - props.img.displayName - + props.img.displayName

- - props.img.displayName - + props.img.displayName

useState(val), }); -export const addImagesAndDimensionsToRef = ({ imagesRef, images, editorContentHtml }) => { - const imagesWithDimensions = Object.values(images).map((image) => { +export const addImagesAndDimensionsToRef = ({ imagesRef, assets, editorContentHtml }) => { + const imagesWithDimensions = module.filterAssets({ assets }).map((image) => { const imageFragment = module.getImageFromHtmlString(editorContentHtml, image.url); return { ...image, width: imageFragment?.width, height: imageFragment?.height }; }); + imagesRef.current = imagesWithDimensions; }; -export const useImages = ({ images, editorContentHtml }) => { +export const useImages = ({ assets, editorContentHtml }) => { const imagesRef = useRef([]); useEffect(() => { - module.addImagesAndDimensionsToRef({ imagesRef, images, editorContentHtml }); - }, [images]); + module.addImagesAndDimensionsToRef({ imagesRef, assets, editorContentHtml }); + }, []); return { imagesRef }; }; @@ -70,45 +69,45 @@ export const parseContentForLabels = ({ editor, updateContent }) => { } }; -export const replaceStaticWithAsset = ({ - initialContent, - learningContextId, +export const replaceStaticwithAsset = ({ + editor, + imageUrls, editorType, lmsEndpointUrl, + updateContent, }) => { - 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 => { + let content = editor.getContent(); + const imageSrcs = content.split('src="'); + imageSrcs.forEach(src => { const currentContent = content; let staticFullUrl; const isStatic = src.startsWith('/static/'); - 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 })}`; + 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); + } } - } 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 }) => () => { @@ -133,10 +132,10 @@ export const setupCustomBehavior = ({ openImgModal, openSourceCodeModal, editorType, + imageUrls, images, setImage, lmsEndpointUrl, - learningContextId, }) => (editor) => { // image upload button editor.ui.registry.addButton(tinyMCE.buttons.imageUploadButton, { @@ -189,24 +188,18 @@ export const setupCustomBehavior = ({ }); if (editorType === 'expandable') { editor.on('init', () => { - const initialContent = editor.getContent(); - const newContent = module.replaceStaticWithAsset({ - initialContent, + module.replaceStaticwithAsset({ + editor, + imageUrls, editorType, lmsEndpointUrl, - learningContextId, + updateContent, }); - updateContent(newContent); }); } editor.on('ExecCommand', (e) => { if (editorType === 'text' && e.command === 'mceFocus') { - const initialContent = editor.getContent(); - const newContent = module.replaceStaticWithAsset({ - initialContent, - learningContextId, - }); - editor.setContent(newContent); + module.replaceStaticwithAsset({ editor, imageUrls }); } if (e.command === 'RemoveFormat') { editor.formatter.remove('blockquote'); @@ -236,7 +229,6 @@ export const editorConfig = ({ updateContent, content, minHeight, - learningContextId, }) => { const { toolbar, @@ -275,7 +267,7 @@ export const editorConfig = ({ setImage: setSelection, content, images, - learningContextId, + imageUrls: module.fetchImageUrls(images), }), quickbars_insert_toolbar: quickbarsInsertToolbar, quickbars_selection_toolbar: quickbarsSelectionToolbar, @@ -388,7 +380,16 @@ export const openModalWithSelectedImage = ({ openImgModal(); }; -export const setAssetToStaticUrl = ({ editorValue, lmsEndpointUrl }) => { +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 }) => { /* For assets to remain usable across course instances, we convert their url to be course-agnostic. * For example, /assets/course//filename gets converted to /static/filename. This is * important for rerunning courses and importing/exporting course as the /static/ part of the url @@ -400,20 +401,42 @@ export const setAssetToStaticUrl = ({ editorValue, lmsEndpointUrl }) => { const regExLmsEndpointUrl = RegExp(lmsEndpointUrl, 'g'); let content = editorValue.replace(regExLmsEndpointUrl, ''); + const assetUrls = []; + const assetsList = Object.values(assets); + assetsList.forEach(asset => { + assetUrls.push({ portableUrl: asset.portableUrl, displayName: asset.displayName }); + }); const assetSrcs = typeof content === 'string' ? content.split(/(src="|src="|href="|href=")/g) : []; assetSrcs.forEach(src => { - if (src.startsWith('/asset')) { + if (src.startsWith('/asset') && assetUrls.length > 0) { const assetBlockName = src.substring(src.indexOf('@') + 1, src.search(/("|")/)); const nameFromEditorSrc = assetBlockName.substring(assetBlockName.indexOf('@') + 1); - const portableUrl = getStaticUrl({ displayName: nameFromEditorSrc }); - const currentSrc = src.substring(0, src.search(/("|")/)); - const updatedContent = content.replace(currentSrc, portableUrl); - content = updatedContent; + const nameFromStudioSrc = assetBlockName.substring(assetBlockName.indexOf('/') + 1); + let portableUrl; + assetUrls.forEach((url) => { + const displayName = url.displayName.replace(/\s/g, '_'); + if (displayName === nameFromEditorSrc || displayName === nameFromStudioSrc) { + portableUrl = url.portableUrl; + } + }); + if (portableUrl) { + const currentSrc = src.substring(0, src.search(/("|")/)); + const updatedContent = content.replace(currentSrc, portableUrl); + content = updatedContent; + } } }); return content; }; +export const fetchImageUrls = (images) => { + const imageUrls = []; + images.current.forEach(image => { + imageUrls.push({ staticFullUrl: image.staticFullUrl, displayName: image.displayName }); + }); + return imageUrls; +}; + export const selectedImage = (val) => { const [selection, setSelection] = module.state.imageSelection(val); return { diff --git a/src/editors/sharedComponents/TinyMceWidget/hooks.test.js b/src/editors/sharedComponents/TinyMceWidget/hooks.test.js index 67096eb91..2eb52c412 100644 --- a/src/editors/sharedComponents/TinyMceWidget/hooks.test.js +++ b/src/editors/sharedComponents/TinyMceWidget/hooks.test.js @@ -49,7 +49,7 @@ const mockImage = { height: initialContentHeight, }; -const mockImages = { +const mockAssets = { [mockImage.id]: mockImage, }; @@ -181,32 +181,41 @@ describe('TinyMceEditor hooks', () => { }); }); - 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); + describe('replaceStaticwithAsset', () => { + test('it calls getContent and setContent for text editor', () => { + const editor = { getContent: jest.fn(() => ''), 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(); }); - it('it returs updated src with absolute url for expandable editor to update content', () => { + 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'; const editorType = 'expandable'; - const expected = `test`; - const actual = module.replaceStaticWithAsset({ - initialContent, + const updateContent = jest.fn(); + module.replaceStaticwithAsset({ + editor, + imageUrls, editorType, lmsEndpointUrl, - learningContextId, + updateContent, }); - expect(actual).toEqual(expected); + expect(editor.getContent).toHaveBeenCalled(); + expect(updateContent).toHaveBeenCalled(); }); }); describe('setAssetToStaticUrl', () => { it('returns content with updated img links', () => { - const editorValue = ' testing link'; + const editorValue = ' testing link'; + const assets = [ + { portableUrl: '/static/soMEImagEURl', displayName: 'soMEImagEURl' }, + { portableUrl: '/static/soME_ImagE_URl1', displayName: 'soME ImagE URl1' }, + ]; const lmsEndpointUrl = 'sOmEvaLue.cOm'; - const content = module.setAssetToStaticUrl({ editorValue, lmsEndpointUrl }); + const content = module.setAssetToStaticUrl({ editorValue, assets, lmsEndpointUrl }); expect(content).toEqual(' testing link'); }); }); @@ -219,7 +228,6 @@ describe('TinyMceEditor hooks', () => { studioEndpointUrl: 'sOmEoThEruRl.cOm', images: mockImagesRef, isLibrary: false, - learningContextId: 'course+org+run', }; const evt = 'fakeEvent'; const editor = 'myEditor'; @@ -336,14 +344,27 @@ 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(() => { @@ -501,10 +522,11 @@ 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, - images: mockImages, + assets, editorContentHtml: mockEditorContentHtml, }, ); diff --git a/src/editors/sharedComponents/TinyMceWidget/index.jsx b/src/editors/sharedComponents/TinyMceWidget/index.jsx index 151d08cba..d2c8a3ff1 100644 --- a/src/editors/sharedComponents/TinyMceWidget/index.jsx +++ b/src/editors/sharedComponents/TinyMceWidget/index.jsx @@ -41,8 +41,7 @@ export const TinyMceWidget = ({ id, editorContentHtml, // editorContent in html form // redux - learningContextId, - images, + assets, isLibrary, lmsEndpointUrl, studioEndpointUrl, @@ -51,7 +50,7 @@ export const TinyMceWidget = ({ }) => { const { isImgOpen, openImgModal, closeImgModal } = hooks.imgModalToggle(); const { isSourceCodeOpen, openSourceCodeModal, closeSourceCodeModal } = hooks.sourceCodeModalToggle(editorRef); - const { imagesRef } = hooks.useImages({ images, editorContentHtml }); + const { imagesRef } = hooks.useImages({ assets, editorContentHtml }); const imageSelection = hooks.selectedImage(null); @@ -86,7 +85,6 @@ export const TinyMceWidget = ({ editorType, editorRef, isLibrary, - learningContextId, lmsEndpointUrl, studioEndpointUrl, images: imagesRef, @@ -105,7 +103,7 @@ TinyMceWidget.defaultProps = { editorRef: null, lmsEndpointUrl: null, studioEndpointUrl: null, - images: null, + assets: null, id: null, disabled: false, editorContentHtml: undefined, @@ -114,10 +112,9 @@ TinyMceWidget.defaultProps = { ...editorConfigDefaultProps, }; TinyMceWidget.propTypes = { - learningContextId: PropTypes.string, editorType: PropTypes.string, isLibrary: PropTypes.bool, - images: PropTypes.shape({}), + assets: PropTypes.shape({}), editorRef: PropTypes.shape({}), lmsEndpointUrl: PropTypes.string, studioEndpointUrl: PropTypes.string, @@ -130,11 +127,10 @@ TinyMceWidget.propTypes = { }; export const mapStateToProps = (state) => ({ - images: selectors.app.images(state), + assets: selectors.app.assets(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 1b2a51b55..4bfb7a081 100644 --- a/src/editors/sharedComponents/TinyMceWidget/index.test.jsx +++ b/src/editors/sharedComponents/TinyMceWidget/index.test.jsx @@ -30,8 +30,7 @@ jest.mock('../../data/redux', () => ({ 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 })), + assets: jest.fn(state => ({ assets: state })), }, }, })); @@ -53,6 +52,7 @@ 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,13 +70,12 @@ describe('TinyMceWidget', () => { editorType: 'text', editorRef: { current: { value: 'something' } }, isLibrary: false, - images: { sOmEaSsET: { staTICUrl: staticUrl } }, + assets: { sOmEaSsET: { staTICUrl: staticUrl } }, lmsEndpointUrl: 'sOmEvaLue.cOm', studioEndpointUrl: 'sOmEoThERvaLue.cOm', disabled: false, id: 'sOMeiD', updateContent: () => ({}), - learningContextId: 'course+org+run', }; describe('snapshots', () => { imgModalToggle.mockReturnValue({ @@ -115,20 +114,15 @@ describe('TinyMceWidget', () => { mapStateToProps(testState).studioEndpointUrl, ).toEqual(selectors.app.studioEndpointUrl(testState)); }); - test('images from app.images', () => { + test('assets from app.assets', () => { expect( - mapStateToProps(testState).images, - ).toEqual(selectors.app.images(testState)); + mapStateToProps(testState).assets, + ).toEqual(selectors.app.assets(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 deleted file mode 100644 index b6e56c655..000000000 --- a/src/editors/sharedComponents/TinyMceWidget/utils.js +++ /dev/null @@ -1,15 +0,0 @@ -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 ''; -};