diff --git a/src/editors/data/redux/app/reducer.js b/src/editors/data/redux/app/reducer.js index 043de8bf69..3e019c8a54 100644 --- a/src/editors/data/redux/app/reducer.js +++ b/src/editors/data/redux/app/reducer.js @@ -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 }) => ({ diff --git a/src/editors/data/redux/thunkActions/app.js b/src/editors/data/redux/thunkActions/app.js index 478ef4240c..09ab9eb61b 100644 --- a/src/editors/data/redux/thunkActions/app.js +++ b/src/editors/data/redux/thunkActions/app.js @@ -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; diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index 46f9d1a03a..e3b4379a1e 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -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'; @@ -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 }; @@ -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, })); }; diff --git a/src/editors/data/redux/thunkActions/requests.test.js b/src/editors/data/redux/thunkActions/requests.test.js index 4b5961b9eb..d389465de9 100644 --- a/src/editors/data/redux/thunkActions/requests.test.js +++ b/src/editors/data/redux/thunkActions/requests.test.js @@ -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'; @@ -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; @@ -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 } } }); @@ -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', () => { @@ -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'; diff --git a/src/editors/data/services/cms/api.test.ts b/src/editors/data/services/cms/api.test.ts index d7b553fb96..80563a812a 100644 --- a/src/editors/data/services/cms/api.test.ts +++ b/src/editors/data/services/cms/api.test.ts @@ -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'), @@ -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', () => { @@ -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, @@ -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', () => { @@ -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, @@ -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', () => { diff --git a/src/editors/data/services/cms/api.ts b/src/editors/data/services/cms/api.ts index d40c9d5f36..20aa84daa4 100644 --- a/src/editors/data/services/cms/api.ts +++ b/src/editors/data/services/cms/api.ts @@ -2,7 +2,9 @@ import type { AxiosRequestConfig } from 'axios'; import { camelizeKeys } from '../../../utils'; import { isLibraryKey } from '../../../../generic/key-utils'; import * as urls from './urls'; -import { get, post, deleteObject } from './utils'; +import { + get, post, put, deleteObject, +} from './utils'; import { durationStringFromValue } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks'; const fetchByUnitIdOptions: AxiosRequestConfig = {}; @@ -116,17 +118,15 @@ export const apiMethods = { urls.blockStudioView({ studioEndpointUrl, blockId }), ), fetchImages: ({ + blockId, learningContextId, studioEndpointUrl, pageNumber, }): Promise<{ data: AssetResponse & Pagination }> => { if (isLibraryKey(learningContextId)) { - // V2 content libraries don't support static assets yet: - return Promise.resolve({ - data: { - assets: [], start: 0, end: 0, page: 0, pageSize: 50, totalCount: 0, - }, - }); + return get( + `${urls.libraryAssets({ studioEndpointUrl, blockId })}`, + ); } const params = { asset_type: 'Images', @@ -147,12 +147,20 @@ export const apiMethods = { urls.courseAdvanceSettings({ studioEndpointUrl, learningContextId }), ), uploadAsset: ({ + blockId, learningContextId, studioEndpointUrl, asset, }) => { const data = new FormData(); data.append('file', asset); + if (isLibraryKey(learningContextId)) { + data.set('content', asset); + return put( + `${urls.libraryAssets({ blockId, studioEndpointUrl })}static/${encodeURI(asset.name)}`, + data, + ); + } return post( urls.courseAssets({ studioEndpointUrl, learningContextId }), data, diff --git a/src/editors/data/services/cms/urls.ts b/src/editors/data/services/cms/urls.ts index 3618499915..d6dd3e362b 100644 --- a/src/editors/data/services/cms/urls.ts +++ b/src/editors/data/services/cms/urls.ts @@ -61,6 +61,10 @@ export const courseAssets = (({ studioEndpointUrl, learningContextId }) => ( `${studioEndpointUrl}/assets/${learningContextId}/` )) satisfies UrlFunction; +export const libraryAssets = (({ blockId, studioEndpointUrl }) => ( + `${studioEndpointUrl}/api/libraries/v2/blocks/${blockId}/assets/` +)) satisfies UrlFunction; + export const thumbnailUpload = (({ studioEndpointUrl, learningContextId, videoId }) => ( `${studioEndpointUrl}/video_images/${learningContextId}/${videoId}` )) satisfies UrlFunction; diff --git a/src/editors/data/services/cms/utils.ts b/src/editors/data/services/cms/utils.ts index b7d6276fe7..86ba95d902 100644 --- a/src/editors/data/services/cms/utils.ts +++ b/src/editors/data/services/cms/utils.ts @@ -16,6 +16,15 @@ export const get: Axios['get'] = (...args) => client().get(...args); * @param {object|string} data - post payload */ export const post: Axios['post'] = (...args) => client().post(...args); + +/** + * post(url, data) + * simple wrapper providing an authenticated Http client post action + * @param {string} url - target url + * @param {object|string} data - post payload + */ +export const put: Axios['put'] = (...args) => client().put(...args); + /** * delete(url, data) * simple wrapper providing an authenticated Http client delete action diff --git a/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js b/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js index 7d7120dc25..c33ab2977f 100644 --- a/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js +++ b/src/editors/sharedComponents/TinyMceWidget/pluginConfig.js @@ -1,13 +1,14 @@ +import { isLibraryV1Key } from '../../../generic/key-utils'; import { StrictDict } from '../../utils'; 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 pluginConfig = ({ learningContextId, placeholder, editorType }) => { + const image = isLibraryV1Key(learningContextId) ? '' : plugins.image; + const imageTools = isLibraryV1Key(learningContextId) ? '' : plugins.imagetools; + const imageUploadButton = isLibraryV1Key(learningContextId) ? '' : buttons.imageUploadButton; + const editImageSettings = isLibraryV1Key(learningContextId) ? '' : buttons.editImageSettings; const codePlugin = editorType === 'text' ? plugins.code : ''; const codeButton = editorType === 'text' ? buttons.code : ''; const labelButton = editorType === 'question' ? buttons.customLabelButton : ''; diff --git a/src/editors/utils/formatLibraryImgRequest.ts b/src/editors/utils/formatLibraryImgRequest.ts new file mode 100644 index 0000000000..6a0580cee9 --- /dev/null +++ b/src/editors/utils/formatLibraryImgRequest.ts @@ -0,0 +1,134 @@ +import StrictDict from './StrictDict'; + +/** + * A dictionary that maps file extensions to their corresponding MIME types for images. + * + * @example + * acceptedImgMimeTypes.gif // "image/gif" + */ + +const acceptedImgMimeTypes = StrictDict({ + gif: 'image/gif', + jpg: 'image/jpg', + jpeg: 'image/jpeg', + png: 'image/png', + tif: 'image/tiff', + tiff: 'image/tiff', + ico: 'image/x-icon', +}); + +type TinyMCEImageData = { + displayName: string, + contentType: string, + url: string, + externalUrl: string, + portableUrl: string, + thumbnail: string, + id: string, + locked: boolean, +}; + +export type LibraryAssetResponse = { + path: string, + size: number, + url: string, +}; + +/** + * Extracts the file name from a file path. + * This function strips the directory structure and returns the base file name. + * + * @param data - The asset data containing the file path. + * @returns The file name extracted from the path. + * + * @example + * const data = { path: '/static/example.jpg', size: 12345, url: 'http://example.com/static/example.jpg' }; + * const fileName = getFileName(data); // "example.jpg" + */ + +export const getFileName = (data: LibraryAssetResponse): string => data.path.replace(/^.*[\\/]/, ''); + +/** + * Determines the MIME type of a file based on its extension. + * + * @param data - The asset data containing the file path. + * @returns The MIME type of the file, or 'unknown' if the MIME type is not recognized. + * + * @example + * const data = { path: '/static/example.jpg', size: 12345, url: 'http://example.com/static/example.jpg' }; + * const mimeType = getFileMimeType(data); // "image/jpg" + */ + +export const getFileMimeType = (data: LibraryAssetResponse): string => { + const ext = data.path.split('.').pop()?.toLowerCase(); // Extract and lowercase the file extension + return ext && acceptedImgMimeTypes[ext] ? acceptedImgMimeTypes[ext] : 'unknown'; +}; +/** + * Parses a `LibraryAssetResponse` into a `TinyMCEImageData` object. + * This includes extracting the file name, MIME type, and constructing other image-related metadata. + * + * @param data - The asset data to parse. + * @returns The parsed image data with properties like `displayName`, `contentType`, etc. + * + * @example + * const data = { path: '/static/example.jpg', size: 12345, url: 'http://example.com/static/example.jpg' }; + * const imageData = parseLibraryImageData(data); + * // { + * // displayName: 'example.jpg', + * // contentType: 'image/jpg', + * // url: 'http://example.com/static/example.jpg', + * // externalUrl: 'http://example.com/static/example.jpg', + * // portableUrl: '/static/example.jpg', + * // thumbnail: 'http://example.com/static/example.jpg', + * // id: '/static/example.jpg', + * // locked: false + * // } + */ + +export const parseLibraryImageData = (data: LibraryAssetResponse): TinyMCEImageData => ({ + displayName: getFileName(data), + contentType: getFileMimeType(data), + url: data.url, + externalUrl: data.url, + portableUrl: data.path, + thumbnail: data.url, + id: data.path, + locked: false, +}); + +/** + * Filters and transforms an array of `LibrariesAssetResponse` objects into a dictionary of `TinyMCEImageData`. + * Only assets with recognized MIME types (i.e., valid image files) are included in the result. + * + * @param librariesAssets - The array of asset data to process. + * @returns A dictionary where each key is the file name and the value is the corresponding `TinyMCEImageData`. + * + * @example + * const assets = [ + * { path: '/static/example.jpg', size: 12345, url: 'http://example.com/static/example.jpg' }, + * { path: '/assets/files/unsupported.xyz', size: 67890, url: 'http://example.com/assets/files/unsupported.xyz' } + * ]; + * const imageAssets = getLibraryImageAssets(assets); + * // { + * // 'example.jpg': { + * // displayName: 'example.jpg', + * // contentType: 'image/jpg', + * // url: 'http://example.com/static/example.jpg', + * // externalUrl: 'http://example.com/static/example.jpg', + * // portableUrl: '/static/example.jpg', + * // thumbnail: 'http://example.com/static/example.jpg', + * // id: '/static/example.jpg', + * // locked: false + * // } + * // } + */ + +export const getLibraryImageAssets = ( + librariesAssets: Array, +): Record => librariesAssets.reduce((obj, file) => { + if (getFileMimeType(file) !== 'unknown') { + const imageData = parseLibraryImageData(file); + return { ...obj, [imageData.displayName]: imageData }; + } + return obj; +}, {} as Record); diff --git a/src/editors/utils/formatLibreryImgRequest.test.ts b/src/editors/utils/formatLibreryImgRequest.test.ts new file mode 100644 index 0000000000..b4d23a75ef --- /dev/null +++ b/src/editors/utils/formatLibreryImgRequest.test.ts @@ -0,0 +1,121 @@ +import { + parseLibraryImageData, getLibraryImageAssets, getFileMimeType, getFileName, LibraryAssetResponse, +} from './formatLibraryImgRequest'; + +// Mock the StrictDict function to avoid unnecessary complexity in the test +jest.mock('./StrictDict', () => ({ + __esModule: true, + default: jest.fn().mockReturnValue({ + gif: 'image/gif', + jpg: 'image/jpg', + jpeg: 'image/jpeg', + png: 'image/png', + tif: 'image/tiff', + tiff: 'image/tiff', + ico: 'image/x-icon', + }), +})); + +describe('parseLibraryImageData', () => { + describe('getFileName', () => { + it('should return the file name from the path', () => { + const data: LibraryAssetResponse = { + path: 'static/example.jpg', + size: 12345, + url: 'http://example.com/static/example.jpg', + }; + + const result = getFileName(data); + expect(result).toBe('example.jpg'); + }); + }); + + describe('getFileMimeType', () => { + it('should return the correct MIME type for supported file extensions', () => { + const data: LibraryAssetResponse = { + path: 'static/example.jpg', + size: 12345, + url: 'http://example.com/static/example.jpg', + }; + + const result = getFileMimeType(data); + expect(result).toBe('image/jpg'); + }); + + it('should return "unknown" for unsupported file extensions', () => { + const data: LibraryAssetResponse = { + path: '/assets/files/unknown.xyz', + size: 12345, + url: 'http://example.com/assets/files/unknown.xyz', + }; + + const result = getFileMimeType(data); + expect(result).toBe('unknown'); + }); + }); + + describe('parseLibraryImageData', () => { + it('should correctly parse a valid LibraryAssetResponse into TinyMCEImageData', () => { + const data: LibraryAssetResponse = { + path: 'static/example.jpg', + size: 12345, + url: 'http://example.com/static/example.jpg', + }; + + const result = parseLibraryImageData(data); + expect(result).toEqual({ + displayName: 'example.jpg', + contentType: 'image/jpg', + url: 'http://example.com/static/example.jpg', + externalUrl: 'http://example.com/static/example.jpg', + portableUrl: 'static/example.jpg', + thumbnail: 'http://example.com/static/example.jpg', + id: 'static/example.jpg', + locked: false, + }); + }); + + it('should handle unknown MIME types by setting a fallback MIME type', () => { + const data: LibraryAssetResponse = { + path: '/assets/files/unknown.xyz', + size: 12345, + url: 'http://example.com/assets/files/unknown.xyz', + }; + + const result = parseLibraryImageData(data); + expect(result.contentType).toBe('unknown'); + }); + }); + + describe('getLibraryImageAssets', () => { + it('should filter out assets with unsupported MIME types and return a dictionary of valid images', () => { + const assets: LibraryAssetResponse[] = [ + { path: 'static/example.jpg', size: 12345, url: 'http://example.com/static/example.jpg' }, + { path: '/assets/files/unsupported.xyz', size: 67890, url: 'http://example.com/assets/files/unsupported.xyz' }, + ]; + + const result = getLibraryImageAssets(assets); + expect(result).toEqual({ + 'example.jpg': { + displayName: 'example.jpg', + contentType: 'image/jpg', + url: 'http://example.com/static/example.jpg', + externalUrl: 'http://example.com/static/example.jpg', + portableUrl: 'static/example.jpg', + thumbnail: 'http://example.com/static/example.jpg', + id: 'static/example.jpg', + locked: false, + }, + }); + }); + + it('should return an empty object if no valid images are found', () => { + const assets: LibraryAssetResponse[] = [ + { path: '/assets/files/unsupported.xyz', size: 67890, url: 'http://example.com/assets/files/unsupported.xyz' }, + ]; + + const result = getLibraryImageAssets(assets); + expect(result).toEqual({}); + }); + }); +}); diff --git a/src/editors/utils/index.ts b/src/editors/utils/index.ts index e314669159..34e2a3c355 100644 --- a/src/editors/utils/index.ts +++ b/src/editors/utils/index.ts @@ -5,3 +5,4 @@ export { default as camelizeKeys } from './camelizeKeys'; export { default as removeItemOnce } from './removeOnce'; export { default as formatDuration } from './formatDuration'; export { default as snakeCaseKeys } from './snakeCaseKeys'; +export * from './formatLibraryImgRequest';