feat: improve asset loading (#484)
* fix: update initialize to only call required functions * feat: update asset urls without asset object * feat: add pagination to select image modal * fix: lint errors * chore: update tests * fix: asset pattern regex match * feat: update pagination to be button to prevent page skipping * fix: e.target.error for feedback fields * fix: failing snapshots
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -23,7 +23,7 @@ exports[`SolutionWidget render snapshot: renders correct default 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<[object Object]
|
||||
editorContentHtml="This is my question"
|
||||
editorContentHtml="This is my solution"
|
||||
editorType="solution"
|
||||
id="solution"
|
||||
minHeight={150}
|
||||
|
||||
@@ -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 (
|
||||
<div className="tinyMceWidget mt-4 text-primary-500">
|
||||
@@ -28,7 +33,7 @@ export const ExplanationWidget = ({
|
||||
id="solution"
|
||||
editorType="solution"
|
||||
editorRef={editorRef}
|
||||
editorContentHtml={settings?.solutionExplanation}
|
||||
editorContentHtml={solutionContent}
|
||||
setEditorRef={setEditorRef}
|
||||
minHeight={150}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
@@ -41,11 +46,13 @@ 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));
|
||||
|
||||
@@ -12,6 +12,9 @@ jest.mock('../../../../../data/redux', () => ({
|
||||
problem: {
|
||||
settings: jest.fn(state => ({ question: state })),
|
||||
},
|
||||
app: {
|
||||
learningContextId: jest.fn(state => ({ learningContextId: state })),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
video: {
|
||||
@@ -25,11 +28,13 @@ 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 question' },
|
||||
settings: { solutionExplanation: 'This is my solution' },
|
||||
learningContextId: 'course+org+run',
|
||||
// injected
|
||||
intl: { formatMessage },
|
||||
};
|
||||
@@ -40,8 +45,11 @@ describe('SolutionWidget', () => {
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('question from problem.question', () => {
|
||||
test('settings from problem.settings', () => {
|
||||
expect(mapStateToProps(testState).settings).toEqual(selectors.problem.settings(testState));
|
||||
});
|
||||
test('learningContextId from app.learningContextId', () => {
|
||||
expect(mapStateToProps(testState).learningContextId).toEqual(selectors.app.learningContextId(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 QuestionWidget = ({
|
||||
// redux
|
||||
question,
|
||||
learningContextId,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
|
||||
const questionContent = replaceStaticWithAsset({
|
||||
initialContent: question,
|
||||
learningContextId,
|
||||
});
|
||||
if (!refReady) { return null; }
|
||||
return (
|
||||
<div className="tinyMceWidget">
|
||||
@@ -25,7 +30,7 @@ export const QuestionWidget = ({
|
||||
id="question"
|
||||
editorType="question"
|
||||
editorRef={editorRef}
|
||||
editorContentHtml={question}
|
||||
editorContentHtml={questionContent}
|
||||
setEditorRef={setEditorRef}
|
||||
minHeight={150}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
@@ -37,11 +42,13 @@ 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));
|
||||
|
||||
@@ -15,9 +15,7 @@ jest.mock('../../../../../data/redux', () => ({
|
||||
},
|
||||
selectors: {
|
||||
app: {
|
||||
isLibrary: jest.fn(state => ({ isLibrary: state })),
|
||||
lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })),
|
||||
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
|
||||
learningContextId: jest.fn(state => ({ learningContextId: state })),
|
||||
},
|
||||
problem: {
|
||||
question: jest.fn(state => ({ question: state })),
|
||||
@@ -35,13 +33,14 @@ jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({
|
||||
refReady: true,
|
||||
setEditorRef: jest.fn().mockName('prepareEditorRef.setEditorRef'),
|
||||
})),
|
||||
// problemEditorConfig: jest.fn(args => ({ problemEditorConfig: args })),
|
||||
replaceStaticWithAsset: jest.fn(() => 'This is my question'),
|
||||
}));
|
||||
|
||||
describe('QuestionWidget', () => {
|
||||
const props = {
|
||||
question: 'This is my question',
|
||||
updateQuestion: jest.fn(),
|
||||
learningContextId: 'course+org+run',
|
||||
// injected
|
||||
intl: { formatMessage },
|
||||
};
|
||||
@@ -55,5 +54,8 @@ 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,14 +56,13 @@ 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(), assets, lmsEndpointUrl });
|
||||
const reactBuiltOlx = setAssetToStaticUrl({ editorValue: reactOLXParser.buildOLX(), lmsEndpointUrl });
|
||||
return {
|
||||
settings: isAdvanced ? reactSettingsParser.parseRawOlxSettings() : reactSettingsParser.getSettings(),
|
||||
olx: isAdvanced ? rawOLX : reactBuiltOlx,
|
||||
@@ -143,7 +142,6 @@ export const getContent = ({
|
||||
openSaveWarningModal,
|
||||
isAdvancedProblemType,
|
||||
editorRef,
|
||||
assets,
|
||||
lmsEndpointUrl,
|
||||
}) => {
|
||||
const problem = problemState;
|
||||
@@ -161,7 +159,6 @@ export const getContent = ({
|
||||
isAdvanced: isAdvancedProblemType,
|
||||
ref: editorRef,
|
||||
problem,
|
||||
assets,
|
||||
lmsEndpointUrl,
|
||||
})();
|
||||
return data;
|
||||
|
||||
@@ -29,7 +29,6 @@ export const EditProblemView = ({
|
||||
// redux
|
||||
problemType,
|
||||
problemState,
|
||||
assets,
|
||||
lmsEndpointUrl,
|
||||
returnUrl,
|
||||
analytics,
|
||||
@@ -48,7 +47,6 @@ export const EditProblemView = ({
|
||||
openSaveWarningModal,
|
||||
isAdvancedProblemType,
|
||||
editorRef,
|
||||
assets,
|
||||
lmsEndpointUrl,
|
||||
})}
|
||||
returnFunction={returnFunction}
|
||||
@@ -70,7 +68,6 @@ export const EditProblemView = ({
|
||||
problem: problemState,
|
||||
isAdvanced: isAdvancedProblemType,
|
||||
ref: editorRef,
|
||||
assets,
|
||||
lmsEndpointUrl,
|
||||
})(),
|
||||
returnFunction,
|
||||
@@ -118,7 +115,6 @@ export const EditProblemView = ({
|
||||
};
|
||||
|
||||
EditProblemView.defaultProps = {
|
||||
assets: null,
|
||||
lmsEndpointUrl: null,
|
||||
returnFunction: null,
|
||||
};
|
||||
@@ -128,7 +124,6 @@ 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,
|
||||
@@ -137,7 +132,6 @@ 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),
|
||||
|
||||
@@ -16,19 +16,17 @@ export const ProblemEditor = ({
|
||||
problemType,
|
||||
blockFinished,
|
||||
blockFailed,
|
||||
studioViewFinished,
|
||||
blockValue,
|
||||
initializeProblemEditor,
|
||||
assetsFinished,
|
||||
advancedSettingsFinished,
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
if (blockFinished && studioViewFinished && assetsFinished && !blockFailed) {
|
||||
if (blockFinished && !blockFailed) {
|
||||
initializeProblemEditor(blockValue);
|
||||
}
|
||||
}, [blockFinished, studioViewFinished, assetsFinished, blockFailed]);
|
||||
}, [blockFinished, blockFailed]);
|
||||
|
||||
if (!blockFinished || !studioViewFinished || !assetsFinished || !advancedSettingsFinished) {
|
||||
if (!blockFinished || !advancedSettingsFinished) {
|
||||
return (
|
||||
<div className="text-center p-6">
|
||||
<Spinner
|
||||
@@ -55,18 +53,15 @@ export const ProblemEditor = ({
|
||||
};
|
||||
|
||||
ProblemEditor.defaultProps = {
|
||||
assetsFinished: null,
|
||||
returnFunction: null,
|
||||
};
|
||||
ProblemEditor.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
returnFunction: PropTypes.func,
|
||||
// redux
|
||||
assetsFinished: PropTypes.bool,
|
||||
advancedSettingsFinished: PropTypes.bool.isRequired,
|
||||
blockFinished: PropTypes.bool.isRequired,
|
||||
blockFailed: PropTypes.bool.isRequired,
|
||||
studioViewFinished: PropTypes.bool.isRequired,
|
||||
problemType: PropTypes.string.isRequired,
|
||||
initializeProblemEditor: PropTypes.func.isRequired,
|
||||
blockValue: PropTypes.objectOf(PropTypes.shape({})).isRequired,
|
||||
@@ -75,10 +70,8 @@ ProblemEditor.propTypes = {
|
||||
export const mapStateToProps = (state) => ({
|
||||
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 }),
|
||||
});
|
||||
|
||||
|
||||
@@ -45,9 +45,7 @@ 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', () => {
|
||||
@@ -58,14 +56,6 @@ describe('ProblemEditor', () => {
|
||||
const wrapper = shallow(<ProblemEditor {...props} blockFinished />);
|
||||
expect(wrapper.instance.findByType(Spinner)).toBeTruthy();
|
||||
});
|
||||
test('studio view loaded, block and assets not yet loaded, Spinner appears', () => {
|
||||
const wrapper = shallow(<ProblemEditor {...props} studioViewFinished />);
|
||||
expect(wrapper.instance.findByType(Spinner)).toBeTruthy();
|
||||
});
|
||||
test('assets loaded, block and studio view not yet loaded, Spinner appears', () => {
|
||||
const wrapper = shallow(<ProblemEditor {...props} assetsFinished />);
|
||||
expect(wrapper.instance.findByType(Spinner)).toBeTruthy();
|
||||
});
|
||||
test('advanceSettings loaded, block and studio view not yet loaded, Spinner appears', () => {
|
||||
const wrapper = shallow(<ProblemEditor {...props} advancedSettingsFinished />);
|
||||
expect(wrapper.instance.findByType(Spinner)).toBeTruthy();
|
||||
@@ -75,7 +65,6 @@ describe('ProblemEditor', () => {
|
||||
{...props}
|
||||
blockFinished
|
||||
studioViewFinished
|
||||
assetsFinished
|
||||
advancedSettingsFinished
|
||||
blockFailed
|
||||
/>);
|
||||
@@ -86,7 +75,6 @@ describe('ProblemEditor', () => {
|
||||
{...props}
|
||||
blockFinished
|
||||
studioViewFinished
|
||||
assetsFinished
|
||||
advancedSettingsFinished
|
||||
/>);
|
||||
expect(wrapper.instance.findByType('SelectTypeModal')).toHaveLength(1);
|
||||
@@ -97,7 +85,6 @@ describe('ProblemEditor', () => {
|
||||
problemType="multiplechoiceresponse"
|
||||
blockFinished
|
||||
studioViewFinished
|
||||
assetsFinished
|
||||
advancedSettingsFinished
|
||||
/>);
|
||||
expect(wrapper.instance.findByType('EditProblemView')).toHaveLength(1);
|
||||
@@ -121,16 +108,6 @@ 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,
|
||||
|
||||
@@ -5,17 +5,12 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
|
||||
getContent={
|
||||
{
|
||||
"getContent": {
|
||||
"assets": {
|
||||
"sOmEaSsET": {
|
||||
"staTICUrl": "/assets/sOmEaSsET",
|
||||
},
|
||||
},
|
||||
"editorRef": {
|
||||
"current": {
|
||||
"value": "something",
|
||||
},
|
||||
},
|
||||
"isRaw": false,
|
||||
"showRawEditor": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -59,17 +54,12 @@ exports[`TextEditor snapshots loaded, raw editor 1`] = `
|
||||
getContent={
|
||||
{
|
||||
"getContent": {
|
||||
"assets": {
|
||||
"sOmEaSsET": {
|
||||
"staTICUrl": "/assets/sOmEaSsET",
|
||||
},
|
||||
},
|
||||
"editorRef": {
|
||||
"current": {
|
||||
"value": "something",
|
||||
},
|
||||
},
|
||||
"isRaw": true,
|
||||
"showRawEditor": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -115,17 +105,12 @@ exports[`TextEditor snapshots not yet loaded, Spinner appears 1`] = `
|
||||
getContent={
|
||||
{
|
||||
"getContent": {
|
||||
"assets": {
|
||||
"sOmEaSsET": {
|
||||
"staTICUrl": "/assets/sOmEaSsET",
|
||||
},
|
||||
},
|
||||
"editorRef": {
|
||||
"current": {
|
||||
"value": "something",
|
||||
},
|
||||
},
|
||||
"isRaw": false,
|
||||
"showRawEditor": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -163,17 +148,12 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
|
||||
getContent={
|
||||
{
|
||||
"getContent": {
|
||||
"assets": {
|
||||
"sOmEaSsET": {
|
||||
"staTICUrl": "/assets/sOmEaSsET",
|
||||
},
|
||||
},
|
||||
"editorRef": {
|
||||
"current": {
|
||||
"value": "something",
|
||||
},
|
||||
},
|
||||
"isRaw": false,
|
||||
"showRawEditor": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { setAssetToStaticUrl } from '../../sharedComponents/TinyMceWidget/hooks'
|
||||
|
||||
export const { nullMethod, navigateCallback, navigateTo } = appHooks;
|
||||
|
||||
export const getContent = ({ editorRef, isRaw, assets }) => () => {
|
||||
const content = (isRaw && editorRef && editorRef.current
|
||||
export const getContent = ({ editorRef, showRawEditor }) => () => {
|
||||
const content = (showRawEditor && editorRef && editorRef.current
|
||||
? editorRef.current.state.doc.toString()
|
||||
: editorRef.current?.getContent());
|
||||
return setAssetToStaticUrl({ editorValue: content, assets });
|
||||
return setAssetToStaticUrl({ editorValue: content });
|
||||
};
|
||||
|
||||
@@ -43,18 +43,17 @@ describe('TextEditor hooks', () => {
|
||||
tinyMceHooks,
|
||||
tinyMceHookKeys.setAssetToStaticUrl,
|
||||
).mockReturnValueOnce(rawContent);
|
||||
const assets = [];
|
||||
test('returns correct content based on isRaw equals false', () => {
|
||||
const getContent = module.getContent({ editorRef, isRaw: false, assets })();
|
||||
test('returns correct content based on showRawEditor equals false', () => {
|
||||
const getContent = module.getContent({ editorRef, showRawEditor: false })();
|
||||
expect(spies.visualHtml.mock.calls.length).toEqual(1);
|
||||
expect(spies.visualHtml).toHaveBeenCalledWith({ editorValue: visualContent, assets });
|
||||
expect(spies.visualHtml).toHaveBeenCalledWith({ editorValue: visualContent });
|
||||
expect(getContent).toEqual(visualContent);
|
||||
});
|
||||
test('returns correct content based on isRaw equals true', () => {
|
||||
test('returns correct content based on showRawEditor equals true', () => {
|
||||
jest.clearAllMocks();
|
||||
const getContent = module.getContent({ editorRef, isRaw: true, assets })();
|
||||
const getContent = module.getContent({ editorRef, showRawEditor: true })();
|
||||
expect(spies.rawHtml.mock.calls.length).toEqual(1);
|
||||
expect(spies.rawHtml).toHaveBeenCalledWith({ editorValue: rawContent, assets });
|
||||
expect(spies.rawHtml).toHaveBeenCalledWith({ editorValue: rawContent });
|
||||
expect(getContent).toEqual(rawContent);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,27 +16,31 @@ import RawEditor from '../../sharedComponents/RawEditor';
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
import TinyMceWidget from '../../sharedComponents/TinyMceWidget';
|
||||
import { prepareEditorRef } from '../../sharedComponents/TinyMceWidget/hooks';
|
||||
import { prepareEditorRef, replaceStaticWithAsset } from '../../sharedComponents/TinyMceWidget/hooks';
|
||||
|
||||
export const TextEditor = ({
|
||||
onClose,
|
||||
returnFunction,
|
||||
// redux
|
||||
isRaw,
|
||||
showRawEditor,
|
||||
blockValue,
|
||||
blockFailed,
|
||||
initializeEditor,
|
||||
assetsFinished,
|
||||
assets,
|
||||
blockFinished,
|
||||
learningContextId,
|
||||
// inject
|
||||
intl,
|
||||
}) => {
|
||||
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
|
||||
const editorContent = blockValue ? replaceStaticWithAsset({
|
||||
initialContent: blockValue.data.data,
|
||||
learningContextId,
|
||||
}) : '';
|
||||
|
||||
if (!refReady) { return null; }
|
||||
|
||||
const selectEditor = () => {
|
||||
if (isRaw) {
|
||||
if (showRawEditor) {
|
||||
return (
|
||||
<RawEditor
|
||||
editorRef={editorRef}
|
||||
@@ -48,7 +52,7 @@ export const TextEditor = ({
|
||||
<TinyMceWidget
|
||||
editorType="text"
|
||||
editorRef={editorRef}
|
||||
editorContentHtml={blockValue ? blockValue.data.data : ''}
|
||||
editorContentHtml={editorContent}
|
||||
setEditorRef={setEditorRef}
|
||||
minHeight={500}
|
||||
height="100%"
|
||||
@@ -59,7 +63,7 @@ export const TextEditor = ({
|
||||
|
||||
return (
|
||||
<EditorContainer
|
||||
getContent={hooks.getContent({ editorRef, isRaw, assets })}
|
||||
getContent={hooks.getContent({ editorRef, showRawEditor })}
|
||||
onClose={onClose}
|
||||
returnFunction={returnFunction}
|
||||
>
|
||||
@@ -68,7 +72,7 @@ export const TextEditor = ({
|
||||
<FormattedMessage {...messages.couldNotLoadTextContext} />
|
||||
</Toast>
|
||||
|
||||
{(!assetsFinished)
|
||||
{(!blockFinished)
|
||||
? (
|
||||
<div className="text-center p-6">
|
||||
<Spinner
|
||||
@@ -84,9 +88,7 @@ export const TextEditor = ({
|
||||
};
|
||||
TextEditor.defaultProps = {
|
||||
blockValue: null,
|
||||
isRaw: null,
|
||||
assetsFinished: null,
|
||||
assets: null,
|
||||
blockFinished: null,
|
||||
returnFunction: null,
|
||||
};
|
||||
TextEditor.propTypes = {
|
||||
@@ -98,9 +100,9 @@ TextEditor.propTypes = {
|
||||
}),
|
||||
blockFailed: PropTypes.bool.isRequired,
|
||||
initializeEditor: PropTypes.func.isRequired,
|
||||
isRaw: PropTypes.bool,
|
||||
assetsFinished: PropTypes.bool,
|
||||
assets: PropTypes.shape({}),
|
||||
showRawEditor: PropTypes.bool.isRequired,
|
||||
blockFinished: PropTypes.bool,
|
||||
learningContextId: PropTypes.string.isRequired,
|
||||
// inject
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
@@ -108,9 +110,9 @@ TextEditor.propTypes = {
|
||||
export const mapStateToProps = (state) => ({
|
||||
blockValue: selectors.app.blockValue(state),
|
||||
blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }),
|
||||
isRaw: selectors.app.isRaw(state),
|
||||
assetsFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchAssets }),
|
||||
assets: selectors.app.assets(state),
|
||||
showRawEditor: selectors.app.showRawEditor(state),
|
||||
blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
|
||||
learningContextId: selectors.app.learningContextId(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
|
||||
@@ -30,6 +30,7 @@ jest.mock('../../sharedComponents/TinyMceWidget/hooks', () => ({
|
||||
refReady: true,
|
||||
setEditorRef: jest.fn().mockName('hooks.prepareEditorRef.setEditorRef'),
|
||||
})),
|
||||
replaceStaticWithAsset: jest.fn(() => 'eDiTablE Text'),
|
||||
}));
|
||||
|
||||
jest.mock('react', () => {
|
||||
@@ -54,9 +55,9 @@ jest.mock('../../data/redux', () => ({
|
||||
blockValue: jest.fn(state => ({ blockValue: state })),
|
||||
lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })),
|
||||
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
|
||||
isRaw: jest.fn(state => ({ isRaw: state })),
|
||||
showRawEditor: jest.fn(state => ({ showRawEditor: state })),
|
||||
isLibrary: jest.fn(state => ({ isLibrary: state })),
|
||||
assets: jest.fn(state => ({ assets: state })),
|
||||
learningContextId: jest.fn(state => ({ learningContextId: state })),
|
||||
},
|
||||
requests: {
|
||||
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
|
||||
@@ -77,9 +78,9 @@ describe('TextEditor', () => {
|
||||
blockValue: { data: { data: 'eDiTablE Text' } },
|
||||
blockFailed: false,
|
||||
initializeEditor: jest.fn().mockName('args.intializeEditor'),
|
||||
isRaw: false,
|
||||
assetsFinished: true,
|
||||
assets: { sOmEaSsET: { staTICUrl: '/assets/sOmEaSsET' } },
|
||||
showRawEditor: false,
|
||||
blockFinished: true,
|
||||
learningContextId: 'course+org+run',
|
||||
// inject
|
||||
intl: { formatMessage },
|
||||
};
|
||||
@@ -88,10 +89,10 @@ describe('TextEditor', () => {
|
||||
expect(shallow(<TextEditor {...props} />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('not yet loaded, Spinner appears', () => {
|
||||
expect(shallow(<TextEditor {...props} assetsFinished={false} />).snapshot).toMatchSnapshot();
|
||||
expect(shallow(<TextEditor {...props} blockFinished={false} />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('loaded, raw editor', () => {
|
||||
expect(shallow(<TextEditor {...props} isRaw />).snapshot).toMatchSnapshot();
|
||||
expect(shallow(<TextEditor {...props} showRawEditor />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('block failed to load, Toast is shown', () => {
|
||||
expect(shallow(<TextEditor {...props} blockFailed />).snapshot).toMatchSnapshot();
|
||||
@@ -105,20 +106,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('assetssFinished from requests.isFinished', () => {
|
||||
test('blockFinished from requests.isFinished', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).assetsFinished,
|
||||
).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchAssets }));
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,14 +8,12 @@ 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',
|
||||
@@ -26,7 +24,7 @@ export const RequestKeys = StrictDict({
|
||||
getTranscriptFile: 'getTranscriptFile',
|
||||
checkTranscriptsForImport: 'checkTranscriptsForImport',
|
||||
importTranscript: 'importTranscript',
|
||||
uploadImage: 'uploadImage',
|
||||
uploadAsset: 'uploadAsset',
|
||||
fetchAdvancedSettings: 'fetchAdvancedSettings',
|
||||
fetchVideoFeatures: 'fetchVideoFeatures',
|
||||
});
|
||||
|
||||
@@ -15,9 +15,11 @@ const initialState = {
|
||||
editorInitialized: false,
|
||||
studioEndpointUrl: null,
|
||||
lmsEndpointUrl: null,
|
||||
assets: {},
|
||||
images: {},
|
||||
imageCount: 0,
|
||||
videos: {},
|
||||
courseDetails: {},
|
||||
showRawEditor: false,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -40,15 +42,22 @@ 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 }),
|
||||
setAssets: (state, { payload }) => ({ ...state, assets: payload }),
|
||||
setImages: (state, { payload }) => ({
|
||||
...state,
|
||||
images: { ...state.images, ...payload.images },
|
||||
imageCount: payload.imageCount,
|
||||
}),
|
||||
setVideos: (state, { payload }) => ({ ...state, videos: payload }),
|
||||
setCourseDetails: (state, { payload }) => ({ ...state, courseDetails: payload }),
|
||||
setShowRawEditor: (state, { payload }) => ({
|
||||
...state,
|
||||
showRawEditor: payload.data?.metadata?.editor === 'raw',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -47,10 +47,18 @@ 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' };
|
||||
@@ -61,6 +69,16 @@ 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({
|
||||
|
||||
@@ -21,8 +21,9 @@ export const simpleSelectors = {
|
||||
studioEndpointUrl: mkSimpleSelector(app => app.studioEndpointUrl),
|
||||
unitUrl: mkSimpleSelector(app => app.unitUrl),
|
||||
blockTitle: mkSimpleSelector(app => app.blockTitle),
|
||||
assets: mkSimpleSelector(app => app.assets),
|
||||
images: mkSimpleSelector(app => app.images),
|
||||
videos: mkSimpleSelector(app => app.videos),
|
||||
showRawEditor: mkSimpleSelector(app => app.showRawEditor),
|
||||
};
|
||||
|
||||
export const returnUrl = createSelector(
|
||||
@@ -72,23 +73,6 @@ 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,
|
||||
@@ -111,6 +95,5 @@ export default {
|
||||
returnUrl,
|
||||
displayTitle,
|
||||
analytics,
|
||||
isRaw,
|
||||
isLibrary,
|
||||
};
|
||||
|
||||
@@ -47,8 +47,9 @@ describe('app selectors unit tests', () => {
|
||||
simpleKeys.unitUrl,
|
||||
simpleKeys.blockTitle,
|
||||
simpleKeys.studioView,
|
||||
simpleKeys.assets,
|
||||
simpleKeys.images,
|
||||
simpleKeys.videos,
|
||||
simpleKeys.showRawEditor,
|
||||
].map(testSimpleSelector);
|
||||
});
|
||||
});
|
||||
@@ -120,41 +121,6 @@ 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';
|
||||
|
||||
@@ -15,7 +15,7 @@ const initialState = {
|
||||
[RequestKeys.uploadTranscript]: { status: RequestStates.inactive },
|
||||
[RequestKeys.deleteTranscript]: { status: RequestStates.inactive },
|
||||
[RequestKeys.fetchCourseDetails]: { status: RequestStates.inactive },
|
||||
[RequestKeys.fetchAssets]: { status: RequestStates.inactive },
|
||||
[RequestKeys.fetchImages]: { status: RequestStates.inactive },
|
||||
[RequestKeys.fetchVideos]: { status: RequestStates.inactive },
|
||||
[RequestKeys.uploadVideo]: { status: RequestStates.inactive },
|
||||
[RequestKeys.checkTranscriptsForImport]: { status: RequestStates.inactive },
|
||||
|
||||
@@ -7,7 +7,10 @@ import { RequestKeys } from '../../constants/requests';
|
||||
|
||||
export const fetchBlock = () => (dispatch) => {
|
||||
dispatch(requests.fetchBlock({
|
||||
onSuccess: (response) => dispatch(actions.app.setBlockValue(response)),
|
||||
onSuccess: (response) => {
|
||||
dispatch(actions.app.setBlockValue(response));
|
||||
dispatch(actions.app.setShowRawEditor(response));
|
||||
},
|
||||
onFailure: (error) => dispatch(actions.requests.failRequest({
|
||||
requestKey: RequestKeys.fetchBlock,
|
||||
error,
|
||||
@@ -35,11 +38,12 @@ export const fetchUnit = () => (dispatch) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const fetchAssets = () => (dispatch) => {
|
||||
dispatch(requests.fetchAssets({
|
||||
onSuccess: (response) => dispatch(actions.app.setAssets(response)),
|
||||
export const fetchImages = ({ pageNumber }) => (dispatch) => {
|
||||
dispatch(requests.fetchImages({
|
||||
pageNumber,
|
||||
onSuccess: ({ images, imageCount }) => dispatch(actions.app.setImages({ images, imageCount })),
|
||||
onFailure: (error) => dispatch(actions.requests.failRequest({
|
||||
requestKey: RequestKeys.fetchAssets,
|
||||
requestKey: RequestKeys.fetchImages,
|
||||
error,
|
||||
})),
|
||||
}));
|
||||
@@ -72,13 +76,25 @@ 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());
|
||||
dispatch(module.fetchStudioView());
|
||||
dispatch(module.fetchAssets());
|
||||
dispatch(module.fetchVideos());
|
||||
dispatch(module.fetchCourseDetails());
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -95,7 +111,7 @@ export const saveBlock = (content, returnToUnit) => (dispatch) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const uploadImage = ({ file, setSelection }) => (dispatch) => {
|
||||
export const uploadAsset = ({ file, setSelection }) => (dispatch) => {
|
||||
dispatch(requests.uploadAsset({
|
||||
asset: file,
|
||||
onSuccess: (response) => setSelection(camelizeKeys(response.data.asset)),
|
||||
@@ -110,6 +126,6 @@ export default StrictDict({
|
||||
fetchVideos,
|
||||
initialize,
|
||||
saveBlock,
|
||||
fetchAssets,
|
||||
uploadImage,
|
||||
fetchImages,
|
||||
uploadAsset,
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ jest.mock('./requests', () => ({
|
||||
saveBlock: (args) => ({ saveBlock: args }),
|
||||
uploadAsset: (args) => ({ uploadAsset: args }),
|
||||
fetchStudioView: (args) => ({ fetchStudioView: args }),
|
||||
fetchAssets: (args) => ({ fetchAssets: args }),
|
||||
fetchImages: (args) => ({ fetchImages: args }),
|
||||
fetchVideos: (args) => ({ fetchVideos: args }),
|
||||
fetchCourseDetails: (args) => ({ fetchCourseDetails: args }),
|
||||
}));
|
||||
@@ -101,24 +101,24 @@ describe('app thunkActions', () => {
|
||||
}));
|
||||
});
|
||||
});
|
||||
describe('fetchAssets', () => {
|
||||
describe('fetchImages', () => {
|
||||
beforeEach(() => {
|
||||
thunkActions.fetchAssets()(dispatch);
|
||||
thunkActions.fetchImages({ pageNumber: 0 })(dispatch);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
it('dispatches fetchAssets action', () => {
|
||||
expect(dispatchedAction.fetchAssets).not.toEqual(undefined);
|
||||
it('dispatches fetchImages action', () => {
|
||||
expect(dispatchedAction.fetchImages).not.toEqual(undefined);
|
||||
});
|
||||
it('dispatches actions.app.setAssets on success', () => {
|
||||
it('dispatches actions.app.setImages on success', () => {
|
||||
dispatch.mockClear();
|
||||
dispatchedAction.fetchAssets.onSuccess(testValue);
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.app.setAssets(testValue));
|
||||
dispatchedAction.fetchImages.onSuccess({ images: {}, imageCount: 0 });
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.app.setImages({ images: {}, imageCount: 0 }));
|
||||
});
|
||||
it('dispatches failRequest with fetchAssets requestKey on failure', () => {
|
||||
it('dispatches failRequest with fetchImages requestKey on failure', () => {
|
||||
dispatch.mockClear();
|
||||
dispatchedAction.fetchAssets.onFailure(testValue);
|
||||
dispatchedAction.fetchImages.onFailure(testValue);
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.requests.failRequest({
|
||||
requestKey: RequestKeys.fetchAssets,
|
||||
requestKey: RequestKeys.fetchImages,
|
||||
error: testValue,
|
||||
}));
|
||||
});
|
||||
@@ -128,7 +128,7 @@ describe('app thunkActions', () => {
|
||||
thunkActions.fetchVideos()(dispatch);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
it('dispatches fetchAssets action', () => {
|
||||
it('dispatches fetchImages action', () => {
|
||||
expect(dispatchedAction.fetchVideos).not.toEqual(undefined);
|
||||
});
|
||||
it('dispatches actions.app.setVideos on success', () => {
|
||||
@@ -167,20 +167,20 @@ describe('app thunkActions', () => {
|
||||
}));
|
||||
});
|
||||
});
|
||||
describe('initialize', () => {
|
||||
describe('initialize without block type defined', () => {
|
||||
it('dispatches actions.app.initialize, and then fetches both block and unit', () => {
|
||||
const {
|
||||
fetchBlock,
|
||||
fetchUnit,
|
||||
fetchStudioView,
|
||||
fetchAssets,
|
||||
fetchImages,
|
||||
fetchVideos,
|
||||
fetchCourseDetails,
|
||||
} = thunkActions;
|
||||
thunkActions.fetchBlock = () => 'fetchBlock';
|
||||
thunkActions.fetchUnit = () => 'fetchUnit';
|
||||
thunkActions.fetchStudioView = () => 'fetchStudioView';
|
||||
thunkActions.fetchAssets = () => 'fetchAssets';
|
||||
thunkActions.fetchImages = () => 'fetchImages';
|
||||
thunkActions.fetchVideos = () => 'fetchVideos';
|
||||
thunkActions.fetchCourseDetails = () => 'fetchCourseDetails';
|
||||
thunkActions.initialize(testValue)(dispatch);
|
||||
@@ -188,15 +188,118 @@ describe('app thunkActions', () => {
|
||||
[actions.app.initialize(testValue)],
|
||||
[thunkActions.fetchBlock()],
|
||||
[thunkActions.fetchUnit()],
|
||||
[thunkActions.fetchStudioView()],
|
||||
[thunkActions.fetchAssets()],
|
||||
]);
|
||||
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.fetchCourseDetails()],
|
||||
]);
|
||||
thunkActions.fetchBlock = fetchBlock;
|
||||
thunkActions.fetchUnit = fetchUnit;
|
||||
thunkActions.fetchStudioView = fetchStudioView;
|
||||
thunkActions.fetchAssets = fetchAssets;
|
||||
thunkActions.fetchImages = fetchImages;
|
||||
thunkActions.fetchVideos = fetchVideos;
|
||||
thunkActions.fetchCourseDetails = fetchCourseDetails;
|
||||
});
|
||||
@@ -225,10 +328,10 @@ describe('app thunkActions', () => {
|
||||
expect(returnToUnit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('uploadImage', () => {
|
||||
describe('uploadAsset', () => {
|
||||
const setSelection = jest.fn();
|
||||
beforeEach(() => {
|
||||
thunkActions.uploadImage({ file: testValue, setSelection })(dispatch);
|
||||
thunkActions.uploadAsset({ file: testValue, setSelection })(dispatch);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
it('dispatches uploadAsset action', () => {
|
||||
|
||||
@@ -124,15 +124,16 @@ export const uploadAsset = ({ asset, ...rest }) => (dispatch, getState) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const fetchAssets = ({ ...rest }) => (dispatch, getState) => {
|
||||
export const fetchImages = ({ pageNumber, ...rest }) => (dispatch, getState) => {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.fetchAssets,
|
||||
requestKey: RequestKeys.fetchImages,
|
||||
promise: api
|
||||
.fetchAssets({
|
||||
.fetchImages({
|
||||
pageNumber,
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
|
||||
learningContextId: selectors.app.learningContextId(getState()),
|
||||
})
|
||||
.then((response) => loadImages(response.data.assets)),
|
||||
.then(({ data }) => ({ images: loadImages(data.assets), imageCount: data.totalCount })),
|
||||
...rest,
|
||||
}));
|
||||
};
|
||||
@@ -312,7 +313,7 @@ export default StrictDict({
|
||||
fetchStudioView,
|
||||
fetchUnit,
|
||||
saveBlock,
|
||||
fetchAssets,
|
||||
fetchImages,
|
||||
fetchVideos,
|
||||
uploadAsset,
|
||||
allowThumbnailUpload,
|
||||
|
||||
@@ -26,7 +26,7 @@ jest.mock('../../services/cms/api', () => ({
|
||||
fetchByUnitId: ({ id, url }) => ({ id, url }),
|
||||
fetchCourseDetails: (args) => args,
|
||||
saveBlock: (args) => args,
|
||||
fetchAssets: ({ id, url }) => ({ id, url }),
|
||||
fetchImages: ({ id, url }) => ({ id, url }),
|
||||
fetchVideos: ({ id, url }) => ({ id, url }),
|
||||
uploadAsset: (args) => args,
|
||||
loadImages: jest.fn(),
|
||||
@@ -237,8 +237,8 @@ describe('requests thunkActions module', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('fetchAssets', () => {
|
||||
let fetchAssets;
|
||||
describe('fetchImages', () => {
|
||||
let fetchImages;
|
||||
let loadImages;
|
||||
let dispatchedAction;
|
||||
const expectedArgs = {
|
||||
@@ -246,12 +246,12 @@ describe('requests thunkActions module', () => {
|
||||
learningContextId: selectors.app.learningContextId(testState),
|
||||
};
|
||||
beforeEach(() => {
|
||||
fetchAssets = jest.fn((args) => new Promise((resolve) => {
|
||||
resolve({ data: { assets: { fetchAssets: args } } });
|
||||
fetchImages = jest.fn((args) => new Promise((resolve) => {
|
||||
resolve({ data: { assets: { fetchImages: args } } });
|
||||
}));
|
||||
jest.spyOn(api, apiKeys.fetchAssets).mockImplementationOnce(fetchAssets);
|
||||
jest.spyOn(api, apiKeys.fetchImages).mockImplementationOnce(fetchImages);
|
||||
loadImages = jest.spyOn(api, apiKeys.loadImages).mockImplementationOnce(() => ({}));
|
||||
requests.fetchAssets({ ...fetchParams, onSuccess, onFailure })(dispatch, () => testState);
|
||||
requests.fetchImages({ ...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.fetchAssets promise called with studioEndpointUrl and learningContextId', () => {
|
||||
expect(fetchAssets).toHaveBeenCalledWith(expectedArgs);
|
||||
test('api.fetchImages promise called with studioEndpointUrl and learningContextId', () => {
|
||||
expect(fetchImages).toHaveBeenCalledWith(expectedArgs);
|
||||
});
|
||||
test('promise is chained with api.loadImages', () => {
|
||||
expect(loadImages).toHaveBeenCalledWith({ fetchAssets: expectedArgs });
|
||||
expect(loadImages).toHaveBeenCalledWith({ fetchImages: expectedArgs });
|
||||
});
|
||||
});
|
||||
describe('fetchVideos', () => {
|
||||
|
||||
@@ -26,9 +26,16 @@ export const apiMethods = {
|
||||
fetchStudioView: ({ blockId, studioEndpointUrl }) => get(
|
||||
urls.blockStudioView({ studioEndpointUrl, blockId }),
|
||||
),
|
||||
fetchAssets: ({ learningContextId, studioEndpointUrl }) => get(
|
||||
urls.courseAssets({ studioEndpointUrl, learningContextId }),
|
||||
),
|
||||
fetchImages: ({ learningContextId, studioEndpointUrl, pageNumber }) => {
|
||||
const params = {
|
||||
asset_type: 'Images',
|
||||
page: pageNumber,
|
||||
};
|
||||
return get(
|
||||
`${urls.courseAssets({ studioEndpointUrl, learningContextId })}`,
|
||||
{ params },
|
||||
);
|
||||
},
|
||||
fetchVideos: ({ studioEndpointUrl, learningContextId }) => get(
|
||||
urls.courseVideos({ studioEndpointUrl, learningContextId }),
|
||||
),
|
||||
|
||||
@@ -126,10 +126,17 @@ describe('cms api', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAssets', () => {
|
||||
describe('fetchImages', () => {
|
||||
it('should call get with url.courseAssets', () => {
|
||||
apiMethods.fetchAssets({ learningContextId, studioEndpointUrl });
|
||||
expect(get).toHaveBeenCalledWith(urls.courseAssets({ studioEndpointUrl, learningContextId }));
|
||||
apiMethods.fetchImages({ learningContextId, studioEndpointUrl, pageNumber: 0 });
|
||||
const params = {
|
||||
asset_type: 'Images',
|
||||
page: 0,
|
||||
};
|
||||
expect(get).toHaveBeenCalledWith(
|
||||
urls.courseAssets({ studioEndpointUrl, learningContextId }),
|
||||
{ params },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export const fetchByUnitId = ({ blockId, studioEndpointUrl }) => mockPromise({
|
||||
data: { ancestors: [{ id: 'unitUrl' }] },
|
||||
});
|
||||
// eslint-disable-next-line
|
||||
export const fetchAssets = ({ learningContextId, studioEndpointUrl }) => mockPromise({
|
||||
export const fetchImages = ({ learningContextId, studioEndpointUrl }) => mockPromise({
|
||||
data: {
|
||||
assets: [
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ export const blockStudioView = ({ studioEndpointUrl, blockId }) => (
|
||||
);
|
||||
|
||||
export const courseAssets = ({ studioEndpointUrl, learningContextId }) => (
|
||||
`${studioEndpointUrl}/assets/${learningContextId}/?page_size=500`
|
||||
`${studioEndpointUrl}/assets/${learningContextId}/`
|
||||
);
|
||||
|
||||
export const thumbnailUpload = ({ studioEndpointUrl, learningContextId, videoId }) => (
|
||||
|
||||
@@ -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}/?page_size=500`);
|
||||
.toEqual(`${studioEndpointUrl}/assets/${learningContextId}/`);
|
||||
});
|
||||
});
|
||||
describe('thumbnailUpload', () => {
|
||||
|
||||
@@ -43,7 +43,14 @@ export const displayList = ({ sortBy, searchString, images }) => (
|
||||
imageList: images,
|
||||
}).sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest]));
|
||||
|
||||
export const imgListHooks = ({ searchSortProps, setSelection, images }) => {
|
||||
export const imgListHooks = ({
|
||||
searchSortProps,
|
||||
setSelection,
|
||||
images,
|
||||
imageCount,
|
||||
}) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const dispatch = useDispatch();
|
||||
const [highlighted, setHighlighted] = module.state.highlighted(null);
|
||||
const [
|
||||
showSelectImageError,
|
||||
@@ -73,6 +80,9 @@ export const imgListHooks = ({ searchSortProps, setSelection, images }) => {
|
||||
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: {
|
||||
@@ -118,7 +128,7 @@ export const fileInputHooks = ({ setSelection, clearSelection, imgList }) => {
|
||||
},
|
||||
})) {
|
||||
dispatch(
|
||||
thunkActions.app.uploadImage({
|
||||
thunkActions.app.uploadAsset({
|
||||
file: selectedFile,
|
||||
setSelection,
|
||||
}),
|
||||
@@ -133,9 +143,19 @@ export const fileInputHooks = ({ setSelection, clearSelection, imgList }) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const imgHooks = ({ setSelection, clearSelection, images }) => {
|
||||
export const imgHooks = ({
|
||||
setSelection,
|
||||
clearSelection,
|
||||
images,
|
||||
imageCount,
|
||||
}) => {
|
||||
const searchSortProps = module.searchAndSortHooks();
|
||||
const imgList = module.imgListHooks({ setSelection, searchSortProps, images });
|
||||
const imgList = module.imgListHooks({
|
||||
setSelection,
|
||||
searchSortProps,
|
||||
images,
|
||||
imageCount,
|
||||
});
|
||||
const fileInput = module.fileInputHooks({
|
||||
setSelection,
|
||||
clearSelection,
|
||||
|
||||
@@ -27,7 +27,7 @@ jest.mock('react-redux', () => {
|
||||
jest.mock('../../../data/redux', () => ({
|
||||
thunkActions: {
|
||||
app: {
|
||||
uploadImage: jest.fn(),
|
||||
uploadAsset: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -248,7 +248,7 @@ describe('SelectImageModal hooks', () => {
|
||||
hook.click();
|
||||
expect(click).toHaveBeenCalled();
|
||||
});
|
||||
describe('addFile (uploadImage args)', () => {
|
||||
describe('addFile (uploadAsset 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 uploadImage thunkAction with the first target file and setSelection', () => {
|
||||
it('dispatches uploadAsset 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.uploadImage({
|
||||
expect(dispatch).toHaveBeenCalledWith(thunkActions.app.uploadAsset({
|
||||
file: testValue,
|
||||
setSelection,
|
||||
}));
|
||||
@@ -281,6 +281,7 @@ 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();
|
||||
@@ -292,9 +293,11 @@ describe('SelectImageModal hooks', () => {
|
||||
.mockReturnValueOnce(searchAndSortHooks);
|
||||
spies.file = jest.spyOn(hooks, hookKeys.fileInputHooks)
|
||||
.mockReturnValueOnce(fileInputHooks);
|
||||
hook = hooks.imgHooks({ setSelection, clearSelection, images });
|
||||
hook = hooks.imgHooks({
|
||||
setSelection, clearSelection, images, imageCount,
|
||||
});
|
||||
});
|
||||
it('forwards fileInputHooks as fileInput, called with uploadImage prop', () => {
|
||||
it('forwards fileInputHooks as fileInput, called with uploadAsset prop', () => {
|
||||
expect(hook.fileInput).toEqual(fileInputHooks);
|
||||
expect(spies.file.mock.calls.length).toEqual(1);
|
||||
expect(spies.file).toHaveBeenCalledWith({
|
||||
@@ -307,6 +310,7 @@ describe('SelectImageModal hooks', () => {
|
||||
setSelection,
|
||||
searchSortProps: searchAndSortHooks,
|
||||
images,
|
||||
imageCount,
|
||||
});
|
||||
});
|
||||
it('forwards searchAndSortHooks as searchSortProps', () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ export const SelectImageModal = ({
|
||||
isLoaded,
|
||||
isFetchError,
|
||||
isUploadError,
|
||||
imageCount,
|
||||
}) => {
|
||||
const {
|
||||
galleryError,
|
||||
@@ -25,7 +26,12 @@ export const SelectImageModal = ({
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
} = hooks.imgHooks({ setSelection, clearSelection, images: images.current });
|
||||
} = hooks.imgHooks({
|
||||
setSelection,
|
||||
clearSelection,
|
||||
images: images.current,
|
||||
imageCount,
|
||||
});
|
||||
|
||||
const modalMessages = {
|
||||
confirmMsg: messages.nextButtonLabel,
|
||||
@@ -66,12 +72,14 @@ 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.fetchAssets }),
|
||||
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchAssets }),
|
||||
isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchImages }),
|
||||
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchImages }),
|
||||
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }),
|
||||
imageCount: state.app.imageCount,
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import SelectableBox from '../SelectableBox';
|
||||
import messages from './messages';
|
||||
import GalleryCard from './GalleryCard';
|
||||
import GalleryLoadMoreButton from './GalleryLoadMoreButton';
|
||||
|
||||
export const Gallery = ({
|
||||
galleryIsEmpty,
|
||||
@@ -23,9 +24,13 @@ export const Gallery = ({
|
||||
height,
|
||||
isLoaded,
|
||||
thumbnailFallback,
|
||||
allowLazyLoad,
|
||||
fetchNextPage,
|
||||
assetCount,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
if (!isLoaded) {
|
||||
|
||||
if (!isLoaded && !allowLazyLoad) {
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -65,7 +70,7 @@ export const Gallery = ({
|
||||
type="radio"
|
||||
value={highlighted}
|
||||
>
|
||||
{ displayList.map(asset => (
|
||||
{displayList.map(asset => (
|
||||
<GalleryCard
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
@@ -74,6 +79,16 @@ export const Gallery = ({
|
||||
/>
|
||||
)) }
|
||||
</SelectableBox.Set>
|
||||
{allowLazyLoad && (
|
||||
<GalleryLoadMoreButton
|
||||
{...{
|
||||
fetchNextPage,
|
||||
assetCount,
|
||||
displayListLength: displayList.length,
|
||||
isLoaded,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -84,6 +99,9 @@ Gallery.defaultProps = {
|
||||
height: '375px',
|
||||
show: true,
|
||||
thumbnailFallback: undefined,
|
||||
allowLazyLoad: false,
|
||||
fetchNextPage: null,
|
||||
assetCount: 0,
|
||||
};
|
||||
Gallery.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
@@ -97,6 +115,9 @@ Gallery.propTypes = {
|
||||
showIdsOnCards: PropTypes.bool,
|
||||
height: PropTypes.string,
|
||||
thumbnailFallback: PropTypes.element,
|
||||
allowLazyLoad: PropTypes.bool,
|
||||
fetchNextPage: PropTypes.func,
|
||||
assetCount: PropTypes.number,
|
||||
};
|
||||
|
||||
export default Gallery;
|
||||
|
||||
@@ -28,6 +28,9 @@ 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(<IntlProvider locale="en">{component}</IntlProvider>);
|
||||
test('snapshot: not loaded, show spinner', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Badge,
|
||||
Image,
|
||||
Truncate,
|
||||
} from '@openedx/paragon';
|
||||
import { FormattedMessage, FormattedDate, FormattedTime } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -64,7 +65,9 @@ export const GalleryCard = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="card-text px-3 py-2" style={{ marginTop: '10px' }}>
|
||||
<h3 className="text-primary-500">{asset.displayName}</h3>
|
||||
<h3 className="text-primary-500">
|
||||
<Truncate>{asset.displayName}</Truncate>
|
||||
</h3>
|
||||
{ asset.transcripts && (
|
||||
<div style={{ margin: '0 0 5px 0' }}>
|
||||
<LanguageNamesWidget
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon, StatefulButton } from '@openedx/paragon';
|
||||
import { ExpandMore, SpinnerSimple } from '@openedx/paragon/icons';
|
||||
|
||||
const GalleryLoadMoreButton = ({
|
||||
assetCount,
|
||||
displayListLength,
|
||||
fetchNextPage,
|
||||
isLoaded,
|
||||
}) => {
|
||||
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: <Icon src={ExpandMore} />,
|
||||
pending: <Icon src={SpinnerSimple} className="icon-spin" />,
|
||||
},
|
||||
disabledStates: ['pending'],
|
||||
variant: 'primary',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row justify-content-center py-3">
|
||||
{displayListLength !== assetCount && (
|
||||
<StatefulButton
|
||||
{...buttonProps}
|
||||
onClick={handlePageChange}
|
||||
state={buttonState}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -29,6 +29,7 @@ export const SearchSort = ({
|
||||
onSwitchClick,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<ActionRow>
|
||||
<Form.Group style={{ margin: 0 }}>
|
||||
|
||||
@@ -43,6 +43,8 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no im
|
||||
}
|
||||
>
|
||||
<Gallery
|
||||
allowLazyLoad={false}
|
||||
assetCount={0}
|
||||
displayList={
|
||||
[
|
||||
{
|
||||
@@ -62,6 +64,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no im
|
||||
"id": "emptyGalleryMsg",
|
||||
}
|
||||
}
|
||||
fetchNextPage={null}
|
||||
galleryIsEmpty={true}
|
||||
height="375px"
|
||||
highlighted="props.highlighted"
|
||||
@@ -117,6 +120,8 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but searc
|
||||
}
|
||||
>
|
||||
<Gallery
|
||||
allowLazyLoad={false}
|
||||
assetCount={0}
|
||||
displayList={
|
||||
[
|
||||
{
|
||||
@@ -136,6 +141,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but searc
|
||||
"id": "emptyGalleryMsg",
|
||||
}
|
||||
}
|
||||
fetchNextPage={null}
|
||||
galleryIsEmpty={false}
|
||||
height="375px"
|
||||
highlighted="props.highlighted"
|
||||
@@ -191,6 +197,8 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal
|
||||
}
|
||||
>
|
||||
<Gallery
|
||||
allowLazyLoad={false}
|
||||
assetCount={0}
|
||||
displayList={
|
||||
[
|
||||
{
|
||||
@@ -210,6 +218,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal
|
||||
"id": "emptyGalleryMsg",
|
||||
}
|
||||
}
|
||||
fetchNextPage={null}
|
||||
galleryIsEmpty={false}
|
||||
height="375px"
|
||||
highlighted="props.highlighted"
|
||||
@@ -265,6 +274,8 @@ exports[`TextEditor Image Gallery component component snapshot: not loaded, show
|
||||
}
|
||||
>
|
||||
<Gallery
|
||||
allowLazyLoad={false}
|
||||
assetCount={0}
|
||||
displayList={
|
||||
[
|
||||
{
|
||||
@@ -284,6 +295,7 @@ exports[`TextEditor Image Gallery component component snapshot: not loaded, show
|
||||
"id": "emptyGalleryMsg",
|
||||
}
|
||||
}
|
||||
fetchNextPage={null}
|
||||
galleryIsEmpty={false}
|
||||
height="375px"
|
||||
highlighted="props.highlighted"
|
||||
|
||||
@@ -59,7 +59,14 @@ exports[`GalleryCard component snapshot with duration badge 1`] = `
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
<Truncate
|
||||
elementType="div"
|
||||
ellipsis="..."
|
||||
lines={1}
|
||||
whiteSpace={false}
|
||||
>
|
||||
props.img.displayName
|
||||
</Truncate>
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
@@ -136,7 +143,14 @@ exports[`GalleryCard component snapshot with duration transcripts 1`] = `
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
<Truncate
|
||||
elementType="div"
|
||||
ellipsis="..."
|
||||
lines={1}
|
||||
whiteSpace={false}
|
||||
>
|
||||
props.img.displayName
|
||||
</Truncate>
|
||||
</h3>
|
||||
<div
|
||||
style={
|
||||
@@ -228,7 +242,14 @@ exports[`GalleryCard component snapshot with status badge 1`] = `
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
<Truncate
|
||||
elementType="div"
|
||||
ellipsis="..."
|
||||
lines={1}
|
||||
whiteSpace={false}
|
||||
>
|
||||
props.img.displayName
|
||||
</Truncate>
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
@@ -306,7 +327,14 @@ exports[`GalleryCard component snapshot with thumbnail fallback and load error 1
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
<Truncate
|
||||
elementType="div"
|
||||
ellipsis="..."
|
||||
lines={1}
|
||||
whiteSpace={false}
|
||||
>
|
||||
props.img.displayName
|
||||
</Truncate>
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
@@ -384,7 +412,14 @@ exports[`GalleryCard component snapshot with thumbnail fallback and no error 1`]
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
<Truncate
|
||||
elementType="div"
|
||||
ellipsis="..."
|
||||
lines={1}
|
||||
whiteSpace={false}
|
||||
>
|
||||
props.img.displayName
|
||||
</Truncate>
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
@@ -461,7 +496,14 @@ exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
<Truncate
|
||||
elementType="div"
|
||||
ellipsis="..."
|
||||
lines={1}
|
||||
whiteSpace={false}
|
||||
>
|
||||
props.img.displayName
|
||||
</Truncate>
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
|
||||
@@ -56,6 +56,7 @@ export const SelectionModal = ({
|
||||
isLoaded,
|
||||
...galleryProps,
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
close={close}
|
||||
|
||||
@@ -45,6 +45,7 @@ exports[`TinyMceWidget snapshots ImageUploadModal is not rendered 1`] = `
|
||||
},
|
||||
"initializeEditor": undefined,
|
||||
"isLibrary": true,
|
||||
"learningContextId": "course+org+run",
|
||||
"lmsEndpointUrl": "sOmEvaLue.cOm",
|
||||
"minHeight": undefined,
|
||||
"openImgModal": [MockFunction modal.openModal],
|
||||
@@ -122,6 +123,7 @@ exports[`TinyMceWidget snapshots SourcecodeModal is not rendered 1`] = `
|
||||
},
|
||||
"initializeEditor": undefined,
|
||||
"isLibrary": false,
|
||||
"learningContextId": "course+org+run",
|
||||
"lmsEndpointUrl": "sOmEvaLue.cOm",
|
||||
"minHeight": undefined,
|
||||
"openImgModal": [MockFunction modal.openModal],
|
||||
@@ -210,6 +212,7 @@ exports[`TinyMceWidget snapshots renders as expected with default behavior 1`] =
|
||||
},
|
||||
"initializeEditor": undefined,
|
||||
"isLibrary": false,
|
||||
"learningContextId": "course+org+run",
|
||||
"lmsEndpointUrl": "sOmEvaLue.cOm",
|
||||
"minHeight": undefined,
|
||||
"openImgModal": [MockFunction modal.openModal],
|
||||
|
||||
@@ -5,11 +5,13 @@ import {
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { a11ycheckerCss } from 'frontend-components-tinymce-advanced-plugins';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import tinyMCEStyles from '../../data/constants/tinyMCEStyles';
|
||||
import { StrictDict } from '../../utils';
|
||||
import pluginConfig from './pluginConfig';
|
||||
import * as module from './hooks';
|
||||
import tinyMCE from '../../data/constants/tinyMCE';
|
||||
import { getRelativeUrl, getStaticUrl } from './utils';
|
||||
|
||||
export const state = StrictDict({
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
@@ -22,21 +24,20 @@ export const state = StrictDict({
|
||||
refReady: (val) => 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/<asset hash>/filename gets converted to /static/filename. This is
|
||||
* important for rerunning courses and importing/exporting course as the /static/ part of the url
|
||||
@@ -401,42 +400,20 @@ export const setAssetToStaticUrl = ({ editorValue, assets, 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') && assetUrls.length > 0) {
|
||||
if (src.startsWith('/asset')) {
|
||||
const assetBlockName = src.substring(src.indexOf('@') + 1, src.search(/("|")/));
|
||||
const nameFromEditorSrc = assetBlockName.substring(assetBlockName.indexOf('@') + 1);
|
||||
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;
|
||||
}
|
||||
const portableUrl = getStaticUrl({ displayName: nameFromEditorSrc });
|
||||
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 {
|
||||
|
||||
@@ -49,7 +49,7 @@ const mockImage = {
|
||||
height: initialContentHeight,
|
||||
};
|
||||
|
||||
const mockAssets = {
|
||||
const mockImages = {
|
||||
[mockImage.id]: mockImage,
|
||||
};
|
||||
|
||||
@@ -181,41 +181,32 @@ describe('TinyMceEditor hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceStaticwithAsset', () => {
|
||||
test('it calls getContent and setContent for text editor', () => {
|
||||
const editor = { getContent: jest.fn(() => '<img src="/static/soMEImagEURl1.jpeg"/>'), 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 = '<img src="/static/soMEImagEURl1.jpeg"/><a href="/assets/v1/some-key/test.pdf">test</a>';
|
||||
const learningContextId = 'course+test+run';
|
||||
const lmsEndpointUrl = 'sOmEvaLue.cOm';
|
||||
it('it returns updated src for text editor to update content', () => {
|
||||
const expected = '<img src="/asset+test+run+type@asset+block@soMEImagEURl1.jpeg"/><a href="/asset+test+run+type@asset+block@test.pdf">test</a>';
|
||||
const actual = module.replaceStaticWithAsset({ initialContent, learningContextId });
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
test('it calls getContent and updateContent for expandable editor', () => {
|
||||
const editor = { getContent: jest.fn(() => '<img src="/static/soMEImagEURl1.jpeg"/>') };
|
||||
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 = `<img src="${lmsEndpointUrl}/asset+test+run+type@asset+block@soMEImagEURl1.jpeg"/><a href="${lmsEndpointUrl}/asset+test+run+type@asset+block@test.pdf">test</a>`;
|
||||
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 = '<img src="/asset@asset-block/soME_ImagE_URl1"/> <a href="/asset@soMEImagEURl">testing link</a>';
|
||||
const assets = [
|
||||
{ portableUrl: '/static/soMEImagEURl', displayName: 'soMEImagEURl' },
|
||||
{ portableUrl: '/static/soME_ImagE_URl1', displayName: 'soME ImagE URl1' },
|
||||
];
|
||||
const editorValue = '<img src="/asset@/soME_ImagE_URl1"/> <a href="/asset@soMEImagEURl">testing link</a>';
|
||||
const lmsEndpointUrl = 'sOmEvaLue.cOm';
|
||||
const content = module.setAssetToStaticUrl({ editorValue, assets, lmsEndpointUrl });
|
||||
const content = module.setAssetToStaticUrl({ editorValue, lmsEndpointUrl });
|
||||
expect(content).toEqual('<img src="/static/soME_ImagE_URl1"/> <a href="/static/soMEImagEURl">testing link</a>');
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
15
src/editors/sharedComponents/TinyMceWidget/utils.js
Normal file
15
src/editors/sharedComponents/TinyMceWidget/utils.js
Normal file
@@ -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 '';
|
||||
};
|
||||
Reference in New Issue
Block a user