feat: image context toolbar (#55)

Note: this removes the crop and rotate functionality from the toolbar, as it requires an image_proxy server or tinymce cloud. https://www.tiny.cloud/docs-4x/plugins/imagetools/#imagetools_proxy. We also need to create a follow up ticket to handle right click behavior.
This is that follow up ticket
This commit is contained in:
connorhaugh
2022-04-08 13:23:42 -04:00
committed by GitHub
parent ef6ea6b617
commit 33b2c6a660
14 changed files with 36 additions and 4 deletions

View File

@@ -25,6 +25,7 @@ export const Editor = ({
courseId,
blockType,
blockId,
lmsEndpointUrl,
studioEndpointUrl,
// redux
initialize,
@@ -35,6 +36,7 @@ export const Editor = ({
blockId,
blockType,
courseId,
lmsEndpointUrl,
studioEndpointUrl,
},
});
@@ -67,6 +69,7 @@ export const Editor = ({
Editor.defaultProps = {
courseId: null,
blockId: null,
lmsEndpointUrl: null,
studioEndpointUrl: null,
};
@@ -74,6 +77,7 @@ Editor.propTypes = {
courseId: PropTypes.string,
blockType: PropTypes.string.isRequired,
blockId: PropTypes.string,
lmsEndpointUrl: PropTypes.string,
studioEndpointUrl: PropTypes.string,
// redux
initialize: PropTypes.func.isRequired,

View File

@@ -20,6 +20,7 @@ const props = {
courseId: 'course-v1:edX+DemoX+Demo_Course',
blockId: 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
studioEndpointUrl: 'fakeurl.com',
lmsEndpointUrl: 'evenfakerurl.com',
initialize: jest.fn(),
};

View File

@@ -9,6 +9,7 @@ export const EditorPage = ({
courseId,
blockType,
blockId,
lmsEndpointUrl,
studioEndpointUrl,
}) => (
<Provider store={store}>
@@ -17,6 +18,7 @@ export const EditorPage = ({
courseId,
blockType,
blockId,
lmsEndpointUrl,
studioEndpointUrl,
}}
/>
@@ -25,6 +27,7 @@ export const EditorPage = ({
EditorPage.defaultProps = {
courseId: null,
blockId: null,
lmsEndpointUrl: null,
studioEndpointUrl: null,
};
@@ -32,6 +35,7 @@ EditorPage.propTypes = {
courseId: PropTypes.string,
blockType: PropTypes.string.isRequired,
blockId: PropTypes.string,
lmsEndpointUrl: PropTypes.string,
studioEndpointUrl: PropTypes.string,
};

View File

@@ -6,6 +6,7 @@ const props = {
courseId: 'course-v1:edX+DemoX+Demo_Course',
blockType: 'html',
blockId: 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
lmsEndpointUrl: 'evenfakerurl.com',
studioEndpointUrl: 'fakeurl.com',
};
jest.mock('react-redux', () => ({

View File

@@ -16,6 +16,7 @@ exports[`Editor Page snapshots rendering correctly with expected Input 1`] = `
blockId="block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4"
blockType="html"
courseId="course-v1:edX+DemoX+Demo_Course"
lmsEndpointUrl="evenfakerurl.com"
studioEndpointUrl="fakeurl.com"
/>
</Provider>

View File

@@ -40,6 +40,7 @@ export const TextEditor = ({
editorRef,
// redux
blockValue,
lmsEndpointUrl,
blockFailed,
blockFinished,
initializeEditor,
@@ -78,6 +79,7 @@ export const TextEditor = ({
blockValue,
openModal,
initializeEditor,
lmsEndpointUrl,
setSelection: imageSelection.setSelection,
clearSelection: imageSelection.clearSelection,
})}
@@ -89,6 +91,7 @@ export const TextEditor = ({
TextEditor.defaultProps = {
blockValue: null,
editorRef: null,
lmsEndpointUrl: null,
};
TextEditor.propTypes = {
editorRef: PropTypes.oneOfType([
@@ -100,6 +103,7 @@ TextEditor.propTypes = {
blockValue: PropTypes.shape({
data: PropTypes.shape({ data: PropTypes.string }),
}),
lmsEndpointUrl: PropTypes.string,
blockFailed: PropTypes.bool.isRequired,
blockFinished: PropTypes.bool.isRequired,
initializeEditor: PropTypes.func.isRequired,
@@ -109,6 +113,7 @@ TextEditor.propTypes = {
export const mapStateToProps = (state) => ({
blockValue: selectors.app.blockValue(state),
lmsEndpointUrl: selectors.app.lmsEndpointUrl(state),
blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }),
blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
});

View File

@@ -53,6 +53,7 @@ jest.mock('../../data/redux', () => ({
selectors: {
app: {
blockValue: jest.fn(state => ({ blockValue: state })),
lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })),
},
requests: {
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
@@ -67,6 +68,7 @@ describe('TextEditor', () => {
editorRef: { current: { value: 'something' } },
// redux
blockValue: { data: { some: 'eDiTablE Text' } },
lmsEndpointUrl: 'sOmEvaLue.cOm',
blockFailed: false,
blockFinished: true,
initializeEditor: jest.fn().mockName('args.intializeEditor'),
@@ -96,6 +98,11 @@ describe('TextEditor', () => {
mapStateToProps(testState).blockValue,
).toEqual(selectors.app.blockValue(testState));
});
test('lmsEndpointUrl from app.lmsEndpointUrl', () => {
expect(
mapStateToProps(testState).lmsEndpointUrl,
).toEqual(selectors.app.lmsEndpointUrl(testState));
});
test('blockFailed from requests.isFailed', () => {
expect(
mapStateToProps(testState).blockFailed,

View File

@@ -38,6 +38,7 @@ exports[`TextEditor snapshots block failed to load, Toast is shown 1`] = `
},
"clearSelection": [MockFunction hooks.selectedImage.clearSelection],
"initializeEditor": [MockFunction args.intializeEditor],
"lmsEndpointUrl": "sOmEvaLue.cOm",
"openModal": [MockFunction modal.openModal],
"setEditorRef": [MockFunction args.setEditorRef],
"setSelection": [MockFunction hooks.selectedImage.setSelection],
@@ -125,6 +126,7 @@ exports[`TextEditor snapshots renders as expected with default behavior 1`] = `
},
"clearSelection": [MockFunction hooks.selectedImage.clearSelection],
"initializeEditor": [MockFunction args.intializeEditor],
"lmsEndpointUrl": "sOmEvaLue.cOm",
"openModal": [MockFunction modal.openModal],
"setEditorRef": [MockFunction args.setEditorRef],
"setSelection": [MockFunction hooks.selectedImage.setSelection],

View File

@@ -68,8 +68,6 @@ export const pluginConfig = {
code: 'code',
}),
imageToolbar: StrictDict({
rotate: 'rotateleft rotateright',
flip: 'flipv fliph',
editImageSettings: 'editimagesettings',
}),
};
@@ -86,6 +84,7 @@ export const editorConfig = ({
openModal,
initializeEditor,
setSelection,
lmsEndpointUrl,
}) => ({
onInit: (evt, editor) => module.initializeEditorRef(setEditorRef, initializeEditor)(editor),
initialValue: blockValue ? blockValue.data.data : '',
@@ -95,7 +94,7 @@ export const editorConfig = ({
menubar: false,
toolbar: module.getConfig('toolbar'),
imagetools_toolbar: module.getConfig('imageToolbar'),
imagetools_cors_hosts: ['courses.edx.org'],
imagetools_cors_hosts: [lmsEndpointUrl], // as image assets come from lms, we need to whitelist it.
height: '100%',
min_height: 500,
branding: false,

View File

@@ -88,11 +88,13 @@ describe('TextEditor hooks', () => {
});
describe('editorConfig', () => {
const blockvalue = null;
const lmsEndpointUrl = 'sOmEuRL.cOm';
const props = {
setEditorRef: jest.fn(),
blockValue: blockvalue,
openModal: jest.fn(),
initializeEditor: jest.fn(),
lmsEndpointUrl,
};
let output;
test('It creates an onInit which calls initializeEditor and setEditorRef', () => {
@@ -132,7 +134,7 @@ describe('TextEditor hooks', () => {
value => expect(output.init.imagetools_toolbar.includes(value)).toBe(true),
);
expect(output.init.menubar).toBe(false);
expect(output.init.imagetools_cors_hosts).toMatchObject(['courses.edx.org']);
expect(output.init.imagetools_cors_hosts).toMatchObject([lmsEndpointUrl]);
expect(output.init.height).toBe('100%');
expect(output.init.min_height).toBe(500);
expect(output.init.branding).toBe(false);

View File

@@ -14,6 +14,7 @@ const initialState = {
courseId: null,
editorInitialized: false,
studioEndpointUrl: null,
lmsEndpointUrl: null,
};
// eslint-disable-next-line no-unused-vars
@@ -24,6 +25,7 @@ const app = createSlice({
initialize: (state, { payload }) => ({
...state,
studioEndpointUrl: payload.studioEndpointUrl,
lmsEndpointUrl: payload.lmsEndpointUrl,
blockId: payload.blockId,
courseId: payload.courseId,
blockType: payload.blockType,

View File

@@ -17,6 +17,7 @@ describe('app reducer', () => {
it('loads initial input fields into the store', () => {
const data = {
studioEndpointUrl: 'testURL',
lmsEndpointUrl: 'sOmEOtherTestuRl',
blockId: 'anID',
courseId: 'OTHERid',
blockType: 'someTYPE',

View File

@@ -17,6 +17,7 @@ export const simpleSelectors = {
courseId: mkSimpleSelector(app => app.courseId),
editorInitialized: mkSimpleSelector(app => app.editorInitialized),
saveResponse: mkSimpleSelector(app => app.saveResponse),
lmsEndpointUrl: mkSimpleSelector(app => app.lmsEndpointUrl),
studioEndpointUrl: mkSimpleSelector(app => app.studioEndpointUrl),
unitUrl: mkSimpleSelector(app => app.unitUrl),
blockTitle: mkSimpleSelector(app => app.blockTitle),

View File

@@ -16,6 +16,7 @@ export const EditorGallery = () => {
}, {});
const courseId = 'fake-course-id';
const studioEndpointUrl = 'fake-studio-endpoint-url';
const lmsEndpointUrl = 'courses.edx.org'; // this is hardcoded because that is where the image data is from.
const handleChange = (e) => setBlockType(e.target.value);
return (
<div className="gallery">
@@ -36,6 +37,7 @@ export const EditorGallery = () => {
blockType,
courseId,
studioEndpointUrl,
lmsEndpointUrl,
}}
/>
</div>