From bf9c8cc7cd64764b6def81016f322d1e14d443e8 Mon Sep 17 00:00:00 2001 From: Diana Olarte Date: Fri, 17 Jan 2025 18:10:21 +1100 Subject: [PATCH] feat: allow to update images in the creation workflow --- src/editors/data/constants/requests.ts | 1 + src/editors/data/redux/thunkActions/app.js | 40 +++++++++---------- .../data/redux/thunkActions/app.test.js | 26 +++++++----- .../data/redux/thunkActions/requests.js | 29 +++++++++++++- .../data/redux/thunkActions/requests.test.js | 18 +++++++++ 5 files changed, 82 insertions(+), 32 deletions(-) diff --git a/src/editors/data/constants/requests.ts b/src/editors/data/constants/requests.ts index be2efcb23..b9d29ce85 100644 --- a/src/editors/data/constants/requests.ts +++ b/src/editors/data/constants/requests.ts @@ -26,6 +26,7 @@ export const RequestKeys = StrictDict({ checkTranscriptsForImport: 'checkTranscriptsForImport', importTranscript: 'importTranscript', uploadAsset: 'uploadAsset', + batchUploadAssets: 'batchUploadAssets', fetchAdvancedSettings: 'fetchAdvancedSettings', fetchVideoFeatures: 'fetchVideoFeatures', } as const); diff --git a/src/editors/data/redux/thunkActions/app.js b/src/editors/data/redux/thunkActions/app.js index e995b8706..ec5b2ffdb 100644 --- a/src/editors/data/redux/thunkActions/app.js +++ b/src/editors/data/redux/thunkActions/app.js @@ -1,5 +1,4 @@ import { StrictDict, camelizeKeys } from '../../../utils'; -import { isLibraryKey } from '../../../../generic/key-utils'; import * as requests from './requests'; // This 'module' self-import hack enables mocking during tests. // See src/editors/decisions/0005-internal-editor-testability-decisions.md. The whole approach to how hooks are tested @@ -131,30 +130,28 @@ export const saveBlock = (content, returnToUnit) => (dispatch) => { })); }; -const imagesNewBlock = []; - /** * @param {func} onSuccess */ -export const createBlock = (content, returnToUnit) => (dispatch) => { +export const createBlock = (content, returnToUnit) => (dispatch, getState) => { dispatch(requests.createBlock({ onSuccess: (response) => { dispatch(actions.app.setBlockId(response.id)); - imagesNewBlock.forEach(({ file, url }) => { - dispatch(requests.uploadAsset({ - asset: file, - onSuccess: async (resp) => { - const imagePath = `/${resp.data.asset.portableUrl}`; - const reader = new FileReader(); - reader.addEventListener('load', () => { - const imageBS64 = reader.result.toString(); - content = content.replace(imageBS64, imagePath); - dispatch(saveBlock(content, returnToUnit)); - }); - reader.readAsDataURL(file); - }, - })); - }); + const newImgages = Object.values(selectors.images(getState())).map((image) => image.file); + + if (newImgages.length === 0) { + dispatch(saveBlock(content, returnToUnit)); + return; + } + dispatch(requests.batchUploadAssets({ + assets: newImgages, + content, + onSuccess: (updatedContent) => dispatch(saveBlock(updatedContent, returnToUnit)), + onFailure: (error) => dispatch(actions.requests.failRequest({ + requestKey: RequestKeys.batchUploadAssets, + error, + })), + })); }, onFailure: (error) => dispatch(actions.requests.failRequest({ requestKey: RequestKeys.createBlock, @@ -165,7 +162,6 @@ export const createBlock = (content, returnToUnit) => (dispatch) => { export const uploadAsset = ({ file, setSelection }) => (dispatch, getState) => { if (selectors.isCreateBlock(getState())) { - console.debug(file); const tempFileURL = URL.createObjectURL(file); const tempImage = { displayName: file.name, @@ -175,10 +171,10 @@ export const uploadAsset = ({ file, setSelection }) => (dispatch, getState) => { thumbnail: tempFileURL, id: file.name, locked: false, + file, }; - - imagesNewBlock.push({ url: tempFileURL, file }); setSelection(tempImage); + dispatch(appActions.setImages({ images: { [file.name]: tempImage }, imageCount: 1 })); return; } diff --git a/src/editors/data/redux/thunkActions/app.test.js b/src/editors/data/redux/thunkActions/app.test.js index c1c179987..099a7419b 100644 --- a/src/editors/data/redux/thunkActions/app.test.js +++ b/src/editors/data/redux/thunkActions/app.test.js @@ -1,6 +1,7 @@ /* eslint-disable no-import-assign */ import { actions } from '..'; import { camelizeKeys } from '../../../utils'; +import { mockImageData } from '../../constants/mockData'; import { RequestKeys } from '../../constants/requests'; import * as thunkActions from './app'; @@ -10,6 +11,7 @@ jest.mock('./requests', () => ({ createBlock: (args) => ({ createBlock: args }), saveBlock: (args) => ({ saveBlock: args }), uploadAsset: (args) => ({ uploadAsset: args }), + batchUploadAssets: (args) => ({ batchUploadAssets: args }), fetchStudioView: (args) => ({ fetchStudioView: args }), fetchImages: (args) => ({ fetchImages: args }), fetchVideos: (args) => ({ fetchVideos: args }), @@ -31,8 +33,10 @@ const testValue = { describe('app thunkActions', () => { let dispatch; let dispatchedAction; + let getState; beforeEach(() => { dispatch = jest.fn((action) => ({ dispatch: action })); + getState = jest.fn(() => ({ app: { blockId: 'blockId', images: {} } })); }); describe('fetchBlock', () => { beforeEach(() => { @@ -358,26 +362,30 @@ describe('app thunkActions', () => { let returnToUnit; beforeEach(() => { returnToUnit = jest.fn(); - thunkActions.createBlock(testValue, returnToUnit)(dispatch); + thunkActions.createBlock(testValue, returnToUnit)(dispatch, getState); [[dispatchedAction]] = dispatch.mock.calls; }); it('dispatches createBlock', () => { expect(dispatchedAction.createBlock).not.toBe(undefined); }); test('onSuccess: calls setBlockId and dispatches saveBlock', () => { - const { - saveBlock, - } = thunkActions; - thunkActions.saveBlock = saveBlock; - - dispatchedAction.createBlock.onSuccess({ id: 'library' }); - expect(dispatch).toHaveBeenCalledWith(actions.app.setBlockId('library')); + const data = { id: 'block-id' }; + dispatchedAction.createBlock.onSuccess(data); + expect(dispatch).toHaveBeenCalledWith(actions.app.setBlockId(data.id)); + expect(dispatch.mock.calls.length).toBe(3); + }); + test('should call batchUploadAssets if the block has images', () => { + getState.mockReturnValueOnce({ app: { blockId: '', images: mockImageData } }); + const data = { id: 'block-id' }; + dispatchedAction.createBlock.onSuccess(data); + expect(dispatch).toHaveBeenCalledWith(actions.app.setBlockId(data.id)); + expect(Object.keys(dispatch.mock.calls[2][0])[0]).toBe(RequestKeys.batchUploadAssets); }); }); describe('uploadAsset', () => { const setSelection = jest.fn(); beforeEach(() => { - thunkActions.uploadAsset({ file: testValue, setSelection })(dispatch); + thunkActions.uploadAsset({ file: testValue, setSelection })(dispatch, getState); [[dispatchedAction]] = dispatch.mock.calls; }); it('dispatches uploadAsset action', () => { diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index 1e68bc101..802207c4f 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -146,9 +146,36 @@ export const createBlock = ({ ...rest }) => (dispatch, getState) => { })); }; +export const batchUploadAssets = ({ assets, content, ...rest }) => (dispatch) => { + let newContent = content; + const promises = assets.reduce((promiseChain, asset) => promiseChain + .then(() => new Promise((resolve) => { + dispatch(module.uploadAsset({ + asset, + onSuccess: (response) => { + const imagePath = `/${response.data.asset.portableUrl}`; + const reader = new FileReader(); + reader.addEventListener('load', () => { + const imageBS64 = reader.result.toString(); + newContent = newContent.replace(imageBS64, imagePath); + URL.revokeObjectURL(asset); + resolve(newContent); + }); + reader.readAsDataURL(asset); + }, + })); + })), Promise.resolve()); + + dispatch(module.networkRequest({ + requestKey: RequestKeys.batchUploadAssets, + promise: promises, + ...rest, + })); +}; + export const uploadAsset = ({ asset, ...rest }) => (dispatch, getState) => { const learningContextId = selectors.app.learningContextId(getState()); - dispatch(module.networkRequest({ + return dispatch(module.networkRequest({ requestKey: RequestKeys.uploadAsset, promise: api.uploadAsset({ learningContextId, diff --git a/src/editors/data/redux/thunkActions/requests.test.js b/src/editors/data/redux/thunkActions/requests.test.js index ebe78a6a6..88b030535 100644 --- a/src/editors/data/redux/thunkActions/requests.test.js +++ b/src/editors/data/redux/thunkActions/requests.test.js @@ -427,6 +427,24 @@ describe('requests thunkActions module', () => { }); }); + describe('batchUploadAssets', () => { + const assets = [new Blob(['file1']), new Blob(['file2'])]; + testNetworkRequestAction({ + action: requests.batchUploadAssets, + args: { ...fetchParams, assets }, + expectedString: 'with upload batch assets promise', + expectedData: { + ...fetchParams, + requestKey: RequestKeys.batchUploadAssets, + promise: assets.reduce((acc, asset) => acc.then(() => api.uploadAsset({ + asset, + learningContextId: selectors.app.learningContextId(testState), + studioEndpointUrl: selectors.app.studioEndpointUrl(testState), + })), Promise.resolve()), + }, + }); + }); + describe('uploadThumbnail', () => { const thumbnail = 'SoME tHumbNAil CoNtent As String'; const videoId = 'SoME VidEOid CoNtent As String';