Skip to content

Commit

Permalink
feat: enable to upload images from libreries in TinyMCE
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoa committed Nov 5, 2024
1 parent ecfe27b commit 7a98722
Show file tree
Hide file tree
Showing 12 changed files with 459 additions and 57 deletions.
1 change: 1 addition & 0 deletions src/editors/data/redux/app/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const app = createSlice({
images: { ...state.images, ...payload.images },
imageCount: payload.imageCount,
}),
resetImages: (state) => ({ ...state, images: {}, imageCount: 0 }),
setVideos: (state, { payload }) => ({ ...state, videos: payload }),
setCourseDetails: (state, { payload }) => ({ ...state, courseDetails: payload }),
setShowRawEditor: (state, { payload }) => ({
Expand Down
8 changes: 2 additions & 6 deletions src/editors/data/redux/thunkActions/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,8 @@ export const initialize = (data) => (dispatch) => {
dispatch(module.fetchCourseDetails());
break;
case 'html':
if (isLibraryKey(data.learningContextId)) {
// eslint-disable-next-line no-console
console.log('Not fetching image assets - not implemented yet for content libraries.');
} else {
dispatch(module.fetchImages({ pageNumber: 0 }));
}
if (isLibraryKey(data.learningContextId)) { dispatch(actions.app.resetImages()); }
dispatch(module.fetchImages({ pageNumber: 0 }));
break;
default:
break;
Expand Down
27 changes: 23 additions & 4 deletions src/editors/data/redux/thunkActions/requests.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StrictDict } from '../../../utils';
import { StrictDict, parseLibraryImageData, getLibraryImageAssets } from '../../../utils';

import { RequestKeys } from '../../constants/requests';
import api, { loadImages } from '../../services/cms/api';
Expand All @@ -10,6 +10,7 @@ import { selectors as appSelectors } from '../app';
// should be re-thought and cleaned up to avoid this pattern.
// eslint-disable-next-line import/no-self-import
import * as module from './requests';
import { isLibraryKey } from '../../../../generic/key-utils';

// Similar to `import { actions, selectors } from '..';` but avoid circular imports:
const actions = { requests: requestsActions };
Expand Down Expand Up @@ -121,27 +122,45 @@ export const saveBlock = ({ content, ...rest }) => (dispatch, getState) => {
}));
};
export const uploadAsset = ({ asset, ...rest }) => (dispatch, getState) => {
const learningContextId = selectors.app.learningContextId(getState());
dispatch(module.networkRequest({
requestKey: RequestKeys.uploadAsset,
promise: api.uploadAsset({
learningContextId: selectors.app.learningContextId(getState()),
learningContextId,
asset,
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
blockId: selectors.app.blockId(getState()),
}).then((resp) => {
if (isLibraryKey(learningContextId)) {
return ({
...resp,
data: { asset: parseLibraryImageData(resp.data) },
});
}
return resp;
}),
...rest,
}));
};

export const fetchImages = ({ pageNumber, ...rest }) => (dispatch, getState) => {
const learningContextId = selectors.app.learningContextId(getState());
dispatch(module.networkRequest({
requestKey: RequestKeys.fetchImages,
promise: api
.fetchImages({
pageNumber,
blockId: selectors.app.blockId(getState()),
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
learningContextId: selectors.app.learningContextId(getState()),
learningContextId,
})
.then(({ data }) => ({ images: loadImages(data.assets), imageCount: data.totalCount })),
.then(({ data }) => {
if (isLibraryKey(learningContextId)) {
const images = getLibraryImageAssets(data.files);
return { images, imageCount: Object.keys(images).length };
}
return { images: loadImages(data.assets), imageCount: data.totalCount };
}),
...rest,
}));
};
Expand Down
133 changes: 104 additions & 29 deletions src/editors/data/redux/thunkActions/requests.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { keyStore } from '../../../utils';
import { keyStore, parseLibraryImageData, getLibraryImageAssets } from '../../../utils';
import { RequestKeys } from '../../constants/requests';
import api from '../../services/cms/api';
import * as requests from './requests';
Expand Down Expand Up @@ -40,6 +40,12 @@ jest.mock('../../services/cms/api', () => ({
uploadVideo: (args) => args,
}));

jest.mock('../../../utils', () => ({
...jest.requireActual('../../../utils'),
parseLibraryImageData: jest.fn(),
getLibraryImageAssets: jest.fn(() => ({})),
}));

const apiKeys = keyStore(api);

let dispatch;
Expand Down Expand Up @@ -241,10 +247,6 @@ describe('requests thunkActions module', () => {
let fetchImages;
let loadImages;
let dispatchedAction;
const expectedArgs = {
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
learningContextId: selectors.app.learningContextId(testState),
};
beforeEach(() => {
fetchImages = jest.fn((args) => new Promise((resolve) => {
resolve({ data: { assets: { fetchImages: args } } });
Expand All @@ -254,18 +256,50 @@ describe('requests thunkActions module', () => {
requests.fetchImages({ ...fetchParams, onSuccess, onFailure })(dispatch, () => testState);
[[dispatchedAction]] = dispatch.mock.calls;
});
it('dispatches networkRequest', () => {
expect(dispatchedAction.networkRequest).not.toEqual(undefined);
});
test('forwards onSuccess and onFailure', () => {
expect(dispatchedAction.networkRequest.onSuccess).toEqual(onSuccess);
expect(dispatchedAction.networkRequest.onFailure).toEqual(onFailure);
});
test('api.fetchImages promise called with studioEndpointUrl and learningContextId', () => {
expect(fetchImages).toHaveBeenCalledWith(expectedArgs);
describe('courses', () => {
const expectedArgs = {
blockId: selectors.app.blockId(testState),
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
learningContextId: selectors.app.learningContextId(testState),
};
it('dispatches networkRequest', () => {
expect(dispatchedAction.networkRequest).not.toEqual(undefined);
});
test('forwards onSuccess and onFailure', () => {
expect(dispatchedAction.networkRequest.onSuccess).toEqual(onSuccess);
expect(dispatchedAction.networkRequest.onFailure).toEqual(onFailure);
});
test('api.fetchImages promise called with studioEndpointUrl and learningContextId', () => {
expect(fetchImages).toHaveBeenCalledWith(expectedArgs);
});
test('promise is chained with api.loadImages', () => {
expect(loadImages).toHaveBeenCalledWith({ fetchImages: expectedArgs });
});
test('promise is chained with api.loadImages', () => {
expect(loadImages).toHaveBeenCalledWith({ fetchImages: expectedArgs });
});
});
test('promise is chained with api.loadImages', () => {
expect(loadImages).toHaveBeenCalledWith({ fetchImages: expectedArgs });
describe('libraries', () => {
const expectedArgs = {
learningContextId: 'lib:demo',
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
blockId: selectors.app.blockId(testState),
};
beforeEach(() => {
jest.spyOn(selectors.app, 'learningContextId').mockImplementationOnce(() => ('lib:demo'));
fetchImages = jest.fn((args) => new Promise((resolve) => {
resolve({ data: { assets: { fetchImages: args } } });
}));
jest.spyOn(api, apiKeys.fetchImages).mockImplementationOnce(fetchImages);
requests.fetchImages({
...fetchParams, onSuccess, onFailure,
})(dispatch, () => testState);
[[dispatchedAction]] = dispatch.mock.calls;
});
test('api.fetchImages promise called with studioEndpointUrl and blockId', () => {
expect(fetchImages).toHaveBeenCalledWith(expectedArgs);
expect(getLibraryImageAssets).toHaveBeenCalled();
});
});
});
describe('fetchVideos', () => {
Expand Down Expand Up @@ -316,21 +350,62 @@ describe('requests thunkActions module', () => {
});
describe('uploadAsset', () => {
const asset = 'SoME iMage CoNtent As String';
testNetworkRequestAction({
action: requests.uploadAsset,
args: { asset, ...fetchParams },
expectedString: 'with uploadAsset promise',
expectedData: {
...fetchParams,
requestKey: RequestKeys.uploadAsset,
promise: api.uploadAsset({
learningContextId: selectors.app.learningContextId(testState),
asset,
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
}),
},
let uploadAsset;
let dispatchedAction;

describe('courses', () => {
const expectedArgs = {
learningContextId: selectors.app.learningContextId(testState),
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
blockId: selectors.app.blockId(testState),
asset,
};
beforeEach(() => {
uploadAsset = jest.fn((args) => new Promise((resolve) => {
resolve({ data: { asset: args } });
}));
jest.spyOn(api, apiKeys.uploadAsset).mockImplementationOnce(uploadAsset);
requests.uploadAsset({
asset, ...fetchParams, onSuccess, onFailure,
})(dispatch, () => testState);
[[dispatchedAction]] = dispatch.mock.calls;
});
it('dispatches networkRequest', () => {
expect(dispatchedAction.networkRequest).not.toEqual(undefined);
});
test('forwards onSuccess and onFailure', () => {
expect(dispatchedAction.networkRequest.onSuccess).toEqual(onSuccess);
expect(dispatchedAction.networkRequest.onFailure).toEqual(onFailure);
});
test('api.uploadAsset promise called with studioEndpointUrl, blockId and learningContextId', () => {
expect(uploadAsset).toHaveBeenCalledWith(expectedArgs);
});
});
describe('libraries', () => {
const expectedArgs = {
learningContextId: 'lib:demo',
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
blockId: selectors.app.blockId(testState),
asset,
};
beforeEach(() => {
jest.spyOn(selectors.app, 'learningContextId').mockImplementationOnce(() => ('lib:demo'));
uploadAsset = jest.fn((args) => new Promise((resolve) => {
resolve({ data: { asset: args } });
}));
jest.spyOn(api, apiKeys.uploadAsset).mockImplementationOnce(uploadAsset);
requests.uploadAsset({
asset, ...fetchParams, onSuccess, onFailure,
})(dispatch, () => testState);
[[dispatchedAction]] = dispatch.mock.calls;
});
test('api.uploadAsset promise called with studioEndpointUrl and blockId', () => {
expect(uploadAsset).toHaveBeenCalledWith(expectedArgs);
expect(parseLibraryImageData).toHaveBeenCalled();
});
});
});

describe('uploadThumbnail', () => {
const thumbnail = 'SoME tHumbNAil CoNtent As String';
const videoId = 'SoME VidEOid CoNtent As String';
Expand Down
45 changes: 39 additions & 6 deletions src/editors/data/services/cms/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import * as api from './api';
import * as urls from './urls';
import { get, post, deleteObject } from './utils';
import {
get, post, put, deleteObject,
} from './utils';

jest.mock('./urls', () => ({
block: jest.fn().mockReturnValue('urls.block'),
blockAncestor: jest.fn().mockReturnValue('urls.blockAncestor'),
blockStudioView: jest.fn().mockReturnValue('urls.StudioView'),
courseAssets: jest.fn().mockReturnValue('urls.courseAssets'),
libraryAssets: jest.fn().mockReturnValue('urls.libraryAssets'),
videoTranscripts: jest.fn().mockReturnValue('urls.videoTranscripts'),
allowThumbnailUpload: jest.fn().mockReturnValue('urls.allowThumbnailUpload'),
thumbnailUpload: jest.fn().mockReturnValue('urls.thumbnailUpload'),
Expand All @@ -25,19 +28,21 @@ jest.mock('./urls', () => ({
jest.mock('./utils', () => ({
get: jest.fn().mockName('get'),
post: jest.fn().mockName('post'),
put: jest.fn().mockName('put'),
deleteObject: jest.fn().mockName('deleteObject'),
}));

const { apiMethods } = api;

const blockId = 'block-v1-coursev1:2uX@4345432';
const learningContextId = 'demo2uX';
let learningContextId;
const studioEndpointUrl = 'hortus.coa';
const title = 'remember this needs to go into metadata to save';

describe('cms api', () => {
beforeEach(() => {
jest.clearAllMocks();
learningContextId = 'demo2uX';
});
describe('apiMethods', () => {
describe('fetchBlockId', () => {
Expand Down Expand Up @@ -102,7 +107,9 @@ describe('cms api', () => {

describe('fetchImages', () => {
it('should call get with url.courseAssets', () => {
apiMethods.fetchImages({ learningContextId, studioEndpointUrl, pageNumber: 0 });
apiMethods.fetchImages({
blockId, learningContextId, studioEndpointUrl, pageNumber: 0,
});
const params = {
asset_type: 'Images',
page: 0,
Expand All @@ -112,6 +119,15 @@ describe('cms api', () => {
{ params },
);
});
it('should call get with urls.libraryAssets for library V2', () => {
learningContextId = 'lib:demo2uX';
apiMethods.fetchImages({
blockId, learningContextId, studioEndpointUrl, pageNumber: 0,
});
expect(get).toHaveBeenCalledWith(
urls.libraryAssets({ studioEndpointUrl, blockId }),
);
});
});

describe('fetchCourseDetails', () => {
Expand Down Expand Up @@ -246,11 +262,14 @@ describe('cms api', () => {
});

describe('uploadAsset', () => {
const asset = new Blob(['data'], { type: 'image/jpeg' });
const img = new Blob(['data'], { type: 'image/jpeg' });
const filename = 'image.jpg';
const asset = new File([img], filename, { type: 'image/jpeg' });
const mockFormdata = new FormData();
mockFormdata.append('file', asset);
it('should call post with urls.courseAssets and imgdata', () => {
const mockFormdata = new FormData();
mockFormdata.append('file', asset);
apiMethods.uploadAsset({
blockId,
learningContextId,
studioEndpointUrl,
asset,
Expand All @@ -260,6 +279,20 @@ describe('cms api', () => {
mockFormdata,
);
});
it('should call post with urls.libraryAssets and imgdata', () => {
learningContextId = 'lib:demo2uX';
mockFormdata.append('content', asset);
apiMethods.uploadAsset({
blockId,
learningContextId,
studioEndpointUrl,
asset,
});
expect(put).toHaveBeenCalledWith(
`${urls.libraryAssets({ blockId, studioEndpointUrl })}static/${encodeURI(filename)}`,
mockFormdata,
);
});
});

describe('uploadVideo', () => {
Expand Down
Loading

0 comments on commit 7a98722

Please sign in to comment.