Skip to content

Commit

Permalink
feat: allow to update images in the creation workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoa committed Jan 17, 2025
1 parent e677ed0 commit bf9c8cc
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 32 deletions.
1 change: 1 addition & 0 deletions src/editors/data/constants/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const RequestKeys = StrictDict({
checkTranscriptsForImport: 'checkTranscriptsForImport',
importTranscript: 'importTranscript',
uploadAsset: 'uploadAsset',
batchUploadAssets: 'batchUploadAssets',
fetchAdvancedSettings: 'fetchAdvancedSettings',
fetchVideoFeatures: 'fetchVideoFeatures',
} as const);
40 changes: 18 additions & 22 deletions src/editors/data/redux/thunkActions/app.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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;
}

Expand Down
26 changes: 17 additions & 9 deletions src/editors/data/redux/thunkActions/app.test.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 }),
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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', () => {
Expand Down
29 changes: 28 additions & 1 deletion src/editors/data/redux/thunkActions/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions src/editors/data/redux/thunkActions/requests.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit bf9c8cc

Please sign in to comment.