From 5eb6a48c16e89ca9a148afc5e4f8709e7207ec06 Mon Sep 17 00:00:00 2001 From: "Kyle D. McCormick" Date: Thu, 24 Oct 2024 00:05:58 -0400 Subject: [PATCH] feat: Support TinyMCE image upload for library components For: https://github.com/openedx/frontend-app-authoring/issues/1398 --- .../data/redux/thunkActions/requests.js | 51 ++++++++++++++++--- src/editors/data/services/cms/api.ts | 8 +++ src/editors/data/services/cms/urls.ts | 4 ++ .../TinyMceWidget/pluginConfig.js | 8 +-- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index 46f9d1a03a..ed760d1d9c 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -4,6 +4,7 @@ import { RequestKeys } from '../../constants/requests'; import api, { loadImages } from '../../services/cms/api'; import { actions as requestsActions } from '../requests'; import { selectors as appSelectors } from '../app'; +import { isLibraryKey } from '../../../../generic/key-utils'; // 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 @@ -133,15 +134,51 @@ export const uploadAsset = ({ asset, ...rest }) => (dispatch, getState) => { }; export const fetchImages = ({ pageNumber, ...rest }) => (dispatch, getState) => { + const learningContextId = selectors.app.learningContextId(getState()); + const studioEndpointUrl = selectors.app.studioEndpointUrl(getState()); dispatch(module.networkRequest({ requestKey: RequestKeys.fetchImages, - promise: api - .fetchImages({ - pageNumber, - studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), - learningContextId: selectors.app.learningContextId(getState()), - }) - .then(({ data }) => ({ images: loadImages(data.assets), imageCount: data.totalCount })), + promise: isLibraryKey(learingContextId) ? ( + api + .fetchImages({ pageNumber, studioEndpointUrl, learningContextId }) + .then(({ data }) => ({ images: loadImages(data.assets), imageCount: data.totalCount })) + ) : ( + api + .fetchLibraryBlockAssets({ + pageNumber, + studioEndpointUrl, + blockId: selectors.app.blockId(getState()), + }) + .then(({ data }) => { + const imageTypes = { + png: 'image/png', + jpeg: 'image/jpeg', + jpg: 'image/jpeg', + }; + const images = data.files.reduce( + (obj, file) => { + const fileName = file.path.replace(/^.*[\\/]/, ''); + const fileExt = file.path.split('.').pop().toLowerCase(); + return imageTypes.hasOwnProperty(fileExt) ? { + ...obj, + [fileName]: { + displayName: fileName, + contentType: imageTypesByExtension[fileExt], + dateAdded: '', + url: file.url, + externalUrl: file.url, + portableUrl: file.path, + thumbnail: file.url, + id: file.path, + locked: false, + }, + } : obj; + }, + {}, + ); + return { images, imageCount: images.keys().length }; + }) + ), ...rest, })); }; diff --git a/src/editors/data/services/cms/api.ts b/src/editors/data/services/cms/api.ts index d40c9d5f36..b9b79f127d 100644 --- a/src/editors/data/services/cms/api.ts +++ b/src/editors/data/services/cms/api.ts @@ -115,6 +115,14 @@ export const apiMethods = { fetchStudioView: ({ blockId, studioEndpointUrl }) => get( urls.blockStudioView({ studioEndpointUrl, blockId }), ), + fetchLibraryBlockAssets: ({ + blockId, + studioEndpointUrl, + pageNumber, + }): Promise<{ data: AssetResponse & Pagination }> => get( + urls.libraryBlockAssets({ studioEndpointUrl, blockId }), + { page: pageNumber }, + ), fetchImages: ({ learningContextId, studioEndpointUrl, diff --git a/src/editors/data/services/cms/urls.ts b/src/editors/data/services/cms/urls.ts index 3618499915..3b2738c253 100644 --- a/src/editors/data/services/cms/urls.ts +++ b/src/editors/data/services/cms/urls.ts @@ -57,6 +57,10 @@ export const blockStudioView = (({ studioEndpointUrl, blockId }) => ( : `${studioEndpointUrl}/api/xblock/v2/xblocks/${blockId}/view/studio_view/` )) satisfies UrlFunction; +export const libraryBlockAssets = (({ studioEndpointUrl, blockId }) => ( + `${studioEndpointUrl}/libraries/v2/blocks/${blockId}/assets` +)) satisfies UrlFunction; + export const courseAssets = (({ studioEndpointUrl, learningContextId }) => ( `${studioEndpointUrl}/assets/${learningContextId}/` )) satisfies UrlFunction; diff --git a/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js b/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js index 7d7120dc25..b688efeb0b 100644 --- a/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js +++ b/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js @@ -4,10 +4,10 @@ import { buttons, plugins } from '../../data/constants/tinyMCE'; const mapToolbars = toolbars => toolbars.map(toolbar => toolbar.join(' ')).join(' | '); const pluginConfig = ({ isLibrary, placeholder, editorType }) => { - const image = isLibrary ? '' : plugins.image; - const imageTools = isLibrary ? '' : plugins.imagetools; - const imageUploadButton = isLibrary ? '' : buttons.imageUploadButton; - const editImageSettings = isLibrary ? '' : buttons.editImageSettings; + const { image } = plugins; + const imageTools = plugins.imagetools; + const { imageUploadButton } = buttons; + const { editImageSettings } = buttons; const codePlugin = editorType === 'text' ? plugins.code : ''; const codeButton = editorType === 'text' ? buttons.code : ''; const labelButton = editorType === 'question' ? buttons.customLabelButton : '';