feat: upload images to v2 library components from the TinyMCE in library editor (#1458)
This commit is contained in:
@@ -11,7 +11,7 @@ import EditorPage from './EditorPage';
|
||||
// Mock this plugins component:
|
||||
jest.mock('frontend-components-tinymce-advanced-plugins', () => ({ a11ycheckerCss: '' }));
|
||||
// Always mock out the "fetch course images" endpoint:
|
||||
jest.spyOn(editorCmsApi, 'fetchImages').mockImplementation(async () => ( // eslint-disable-next-line
|
||||
jest.spyOn(editorCmsApi, 'fetchCourseImages').mockImplementation(async () => ( // eslint-disable-next-line
|
||||
{ data: { assets: [], start: 0, end: 0, page: 0, pageSize: 50, totalCount: 0 } }
|
||||
));
|
||||
// Mock out the 'get ancestors' API:
|
||||
|
||||
@@ -13,7 +13,7 @@ import EditorPage from '../../EditorPage';
|
||||
// Mock this plugins component:
|
||||
jest.mock('frontend-components-tinymce-advanced-plugins', () => ({ a11ycheckerCss: '' }));
|
||||
// Always mock out the "fetch course images" endpoint:
|
||||
jest.spyOn(editorCmsApi, 'fetchImages').mockImplementation(async () => ( // eslint-disable-next-line
|
||||
jest.spyOn(editorCmsApi, 'fetchCourseImages').mockImplementation(async () => ( // eslint-disable-next-line
|
||||
{ data: { assets: [], start: 0, end: 0, page: 0, pageSize: 50, totalCount: 0 } }
|
||||
));
|
||||
// Mock out the 'get ancestors' API:
|
||||
|
||||
@@ -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 }) => ({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,8 @@ 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';
|
||||
import { acceptedImgKeys } from '../../../sharedComponents/ImageUploadModal/SelectImageModal/utils';
|
||||
|
||||
// Similar to `import { actions, selectors } from '..';` but avoid circular imports:
|
||||
const actions = { requests: requestsActions };
|
||||
@@ -121,25 +123,55 @@ 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());
|
||||
if (isLibraryKey(learningContextId)) {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.fetchImages,
|
||||
promise: api
|
||||
.fetchLibraryImages({
|
||||
pageNumber,
|
||||
blockId: selectors.app.blockId(getState()),
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
|
||||
learningContextId,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const images = getLibraryImageAssets(data.files, Object.keys(acceptedImgKeys));
|
||||
return { images, imageCount: Object.keys(images).length };
|
||||
}),
|
||||
...rest,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.fetchImages,
|
||||
promise: api
|
||||
.fetchImages({
|
||||
.fetchCourseImages({
|
||||
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 })),
|
||||
...rest,
|
||||
|
||||
@@ -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';
|
||||
@@ -26,7 +26,8 @@ jest.mock('../../services/cms/api', () => ({
|
||||
fetchByUnitId: ({ id, url }) => ({ id, url }),
|
||||
fetchCourseDetails: (args) => args,
|
||||
saveBlock: (args) => args,
|
||||
fetchImages: ({ id, url }) => ({ id, url }),
|
||||
fetchCourseImages: ({ id, url }) => ({ id, url }),
|
||||
fetchLibraryImages: ({ id, url }) => ({ id, url }),
|
||||
fetchVideos: ({ id, url }) => ({ id, url }),
|
||||
uploadAsset: (args) => args,
|
||||
loadImages: jest.fn(),
|
||||
@@ -40,6 +41,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,31 +248,59 @@ 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 } } });
|
||||
}));
|
||||
jest.spyOn(api, apiKeys.fetchImages).mockImplementationOnce(fetchImages);
|
||||
loadImages = jest.spyOn(api, apiKeys.loadImages).mockImplementationOnce(() => ({}));
|
||||
requests.fetchImages({ ...fetchParams, onSuccess, onFailure })(dispatch, () => testState);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
describe('courses', () => {
|
||||
beforeEach(() => {
|
||||
fetchImages = jest.fn((args) => new Promise((resolve) => {
|
||||
resolve({ data: { assets: { fetchImages: args } } });
|
||||
}));
|
||||
jest.spyOn(api, apiKeys.fetchCourseImages).mockImplementationOnce(fetchImages);
|
||||
loadImages = jest.spyOn(api, apiKeys.loadImages).mockImplementationOnce(() => ({}));
|
||||
requests.fetchImages({ ...fetchParams, onSuccess, onFailure })(dispatch, () => testState);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
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 });
|
||||
});
|
||||
});
|
||||
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 });
|
||||
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: { files: { fetchImages: args } } });
|
||||
}));
|
||||
jest.spyOn(api, apiKeys.fetchLibraryImages).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 +351,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';
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -100,9 +105,11 @@ describe('cms api', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchImages', () => {
|
||||
describe('fetchCourseImages', () => {
|
||||
it('should call get with url.courseAssets', () => {
|
||||
apiMethods.fetchImages({ learningContextId, studioEndpointUrl, pageNumber: 0 });
|
||||
apiMethods.fetchCourseImages({
|
||||
learningContextId, studioEndpointUrl, pageNumber: 0,
|
||||
});
|
||||
const params = {
|
||||
asset_type: 'Images',
|
||||
page: 0,
|
||||
@@ -113,6 +120,16 @@ describe('cms api', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('fetchLibraryImages', () => {
|
||||
it('should call get with urls.libraryAssets for library V2', () => {
|
||||
apiMethods.fetchLibraryImages({
|
||||
blockId,
|
||||
});
|
||||
expect(get).toHaveBeenCalledWith(
|
||||
urls.libraryAssets({ blockId }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchCourseDetails', () => {
|
||||
it('should call get with url.courseDetailsUrl', () => {
|
||||
@@ -246,11 +263,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 +280,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, assetName: asset.name })}`,
|
||||
mockFormdata,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadVideo', () => {
|
||||
|
||||
@@ -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 = {};
|
||||
@@ -115,19 +117,11 @@ export const apiMethods = {
|
||||
fetchStudioView: ({ blockId, studioEndpointUrl }) => get(
|
||||
urls.blockStudioView({ studioEndpointUrl, blockId }),
|
||||
),
|
||||
fetchImages: ({
|
||||
fetchCourseImages: ({
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
const params = {
|
||||
asset_type: 'Images',
|
||||
page: pageNumber,
|
||||
@@ -137,6 +131,9 @@ export const apiMethods = {
|
||||
{ params },
|
||||
);
|
||||
},
|
||||
fetchLibraryImages: ({ blockId }) => get(
|
||||
`${urls.libraryAssets({ blockId })}`,
|
||||
),
|
||||
fetchVideos: ({ studioEndpointUrl, learningContextId }) => get(
|
||||
urls.courseVideos({ studioEndpointUrl, learningContextId }),
|
||||
),
|
||||
@@ -147,12 +144,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, assetName: asset.name })}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
return post(
|
||||
urls.courseAssets({ studioEndpointUrl, learningContextId }),
|
||||
data,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isLibraryKey, isLibraryV1Key } from '../../../../generic/key-utils';
|
||||
import { getXBlockAssetsApiUrl } from '../../../../library-authoring/data/api';
|
||||
|
||||
/**
|
||||
* A little helper so we can write the types of these functions more compactly
|
||||
@@ -61,6 +62,12 @@ export const courseAssets = (({ studioEndpointUrl, learningContextId }) => (
|
||||
`${studioEndpointUrl}/assets/${learningContextId}/`
|
||||
)) satisfies UrlFunction;
|
||||
|
||||
export const libraryAssets = (({ blockId, assetName }) => (
|
||||
assetName
|
||||
? `${getXBlockAssetsApiUrl(blockId)}static/${encodeURI(assetName)}`
|
||||
: `${getXBlockAssetsApiUrl(blockId)}`
|
||||
)) satisfies UrlFunction;
|
||||
|
||||
export const thumbnailUpload = (({ studioEndpointUrl, learningContextId, videoId }) => (
|
||||
`${studioEndpointUrl}/video_images/${learningContextId}/${videoId}`
|
||||
)) satisfies UrlFunction;
|
||||
|
||||
@@ -16,6 +16,13 @@ export const get: Axios['get'] = (...args) => client().get(...args);
|
||||
* @param {object|string} data - post payload
|
||||
*/
|
||||
export const post: Axios['post'] = (...args) => client().post(...args);
|
||||
|
||||
/**
|
||||
* put(url, data)
|
||||
* simple wrapper providing an authenticated Http client put action
|
||||
*/
|
||||
export const put: Axios['put'] = (...args) => client().put(...args);
|
||||
|
||||
/**
|
||||
* delete(url, data)
|
||||
* simple wrapper providing an authenticated Http client delete action
|
||||
|
||||
@@ -9,8 +9,10 @@ exports[`SelectImageModal component snapshot 1`] = `
|
||||
"jpeg": ".jpeg",
|
||||
"jpg": ".jpg",
|
||||
"png": ".png",
|
||||
"svg": ".svg",
|
||||
"tif": ".tif",
|
||||
"tiff": ".tiff",
|
||||
"webp": ".webp",
|
||||
}
|
||||
}
|
||||
close={[MockFunction props.close]}
|
||||
@@ -59,7 +61,7 @@ exports[`SelectImageModal component snapshot 1`] = `
|
||||
"id": "authoring.texteditor.selectimagemodal.next.label",
|
||||
},
|
||||
"fetchError": {
|
||||
"defaultMessage": "Failed to obtain course images. Please try again.",
|
||||
"defaultMessage": "Failed to obtain images. Please try again.",
|
||||
"description": "Message presented to user when images are not found",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.fetchImagesError",
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ const SelectImageModal = ({
|
||||
isLoaded,
|
||||
isFetchError,
|
||||
isUploadError,
|
||||
isLibrary,
|
||||
imageCount,
|
||||
}) => {
|
||||
const {
|
||||
@@ -57,6 +58,7 @@ const SelectImageModal = ({
|
||||
isLoaded,
|
||||
isFetchError,
|
||||
isUploadError,
|
||||
isLibrary,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -73,12 +75,14 @@ SelectImageModal.propTypes = {
|
||||
isFetchError: PropTypes.bool.isRequired,
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
imageCount: PropTypes.number.isRequired,
|
||||
isLibrary: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchImages }),
|
||||
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchImages }),
|
||||
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }),
|
||||
isLibrary: selectors.app.isLibrary(state),
|
||||
imageCount: state.app.imageCount,
|
||||
});
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ const messages = defineMessages({
|
||||
},
|
||||
fetchImagesError: {
|
||||
id: 'authoring.texteditor.selectimagemodal.error.fetchImagesError',
|
||||
defaultMessage: 'Failed to obtain course images. Please try again.',
|
||||
defaultMessage: 'Failed to obtain images. Please try again.',
|
||||
description: 'Message presented to user when images are not found',
|
||||
},
|
||||
fileSizeError: {
|
||||
|
||||
@@ -44,4 +44,6 @@ export const acceptedImgKeys = StrictDict({
|
||||
tif: '.tif',
|
||||
tiff: '.tiff',
|
||||
ico: '.ico',
|
||||
svg: '.svg',
|
||||
webp: '.webp',
|
||||
});
|
||||
|
||||
@@ -21,12 +21,17 @@ export const imgProps = ({
|
||||
selection,
|
||||
lmsEndpointUrl,
|
||||
editorType,
|
||||
isLibrary,
|
||||
}) => {
|
||||
let url = selection?.externalUrl;
|
||||
if (url?.startsWith(lmsEndpointUrl) && editorType !== 'expandable') {
|
||||
const sourceEndIndex = lmsEndpointUrl.length;
|
||||
url = url.substring(sourceEndIndex);
|
||||
}
|
||||
if (isLibrary) {
|
||||
const index = url.indexOf('static/');
|
||||
url = url.substring(index);
|
||||
}
|
||||
return {
|
||||
src: url,
|
||||
alt: settings.isDecorative ? '' : settings.altText,
|
||||
@@ -36,13 +41,14 @@ export const imgProps = ({
|
||||
};
|
||||
|
||||
export const saveToEditor = ({
|
||||
settings, selection, lmsEndpointUrl, editorType, editorRef,
|
||||
settings, selection, lmsEndpointUrl, editorType, editorRef, isLibrary,
|
||||
}) => {
|
||||
const newImgTag = module.hooks.imgTag({
|
||||
settings,
|
||||
selection,
|
||||
lmsEndpointUrl,
|
||||
editorType,
|
||||
isLibrary,
|
||||
});
|
||||
|
||||
editorRef.current.execCommand(
|
||||
@@ -103,12 +109,14 @@ export const hooks = {
|
||||
selection,
|
||||
lmsEndpointUrl,
|
||||
editorType,
|
||||
isLibrary,
|
||||
}) => {
|
||||
const props = module.imgProps({
|
||||
settings,
|
||||
selection,
|
||||
lmsEndpointUrl,
|
||||
editorType,
|
||||
isLibrary,
|
||||
});
|
||||
return `<img ${propsString(props)} />`;
|
||||
},
|
||||
@@ -130,6 +138,7 @@ const ImageUploadModal = ({
|
||||
images,
|
||||
editorType,
|
||||
lmsEndpointUrl,
|
||||
isLibrary,
|
||||
}) => {
|
||||
if (selection && selection.externalUrl) {
|
||||
return (
|
||||
@@ -148,6 +157,7 @@ const ImageUploadModal = ({
|
||||
setSelection,
|
||||
lmsEndpointUrl,
|
||||
clearSelection,
|
||||
isLibrary,
|
||||
}),
|
||||
returnToSelection: clearSelection,
|
||||
}}
|
||||
@@ -190,6 +200,7 @@ ImageUploadModal.propTypes = {
|
||||
images: PropTypes.shape({}).isRequired,
|
||||
lmsEndpointUrl: PropTypes.string.isRequired,
|
||||
editorType: PropTypes.string,
|
||||
isLibrary: PropTypes.string,
|
||||
};
|
||||
|
||||
export const ImageUploadModalInternal = ImageUploadModal; // For testing only
|
||||
|
||||
@@ -23,6 +23,7 @@ const Gallery = ({
|
||||
showIdsOnCards,
|
||||
height,
|
||||
isLoaded,
|
||||
isLibrary,
|
||||
thumbnailFallback,
|
||||
allowLazyLoad,
|
||||
fetchNextPage,
|
||||
@@ -79,7 +80,7 @@ const Gallery = ({
|
||||
/>
|
||||
)) }
|
||||
</SelectableBox.Set>
|
||||
{allowLazyLoad && (
|
||||
{(allowLazyLoad && !isLibrary) && (
|
||||
<GalleryLoadMoreButton
|
||||
{...{
|
||||
fetchNextPage,
|
||||
@@ -112,6 +113,7 @@ Gallery.propTypes = {
|
||||
highlighted: PropTypes.string,
|
||||
onHighlightChange: PropTypes.func.isRequired,
|
||||
emptyGalleryLabel: PropTypes.shape({}).isRequired,
|
||||
isLibrary: PropTypes.bool,
|
||||
showIdsOnCards: PropTypes.bool,
|
||||
height: PropTypes.string,
|
||||
thumbnailFallback: PropTypes.element,
|
||||
|
||||
@@ -75,6 +75,7 @@ const GalleryCard = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{asset.dateAdded && (
|
||||
<p className="text-gray-500" style={{ fontSize: '11px' }}>
|
||||
<FormattedMessage
|
||||
{...messages.addedDate}
|
||||
@@ -84,6 +85,7 @@ const GalleryCard = ({
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
|
||||
@@ -34,6 +34,7 @@ const SelectionModal = ({
|
||||
isLoaded,
|
||||
isFetchError,
|
||||
isUploadError,
|
||||
isLibrary,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
@@ -54,6 +55,7 @@ const SelectionModal = ({
|
||||
|
||||
const galleryPropsValues = {
|
||||
isLoaded,
|
||||
isLibrary,
|
||||
...galleryProps,
|
||||
};
|
||||
|
||||
@@ -83,7 +85,7 @@ const SelectionModal = ({
|
||||
)}
|
||||
title={intl.formatMessage(titleMsg)}
|
||||
bodyStyle={{ background }}
|
||||
headerComponent={(
|
||||
headerComponent={!isLibrary && (
|
||||
<div style={{ margin: '18px 0' }}>
|
||||
<SearchSort {...searchSortProps} />
|
||||
</div>
|
||||
@@ -160,6 +162,7 @@ SelectionModal.propTypes = {
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
isFetchError: PropTypes.bool.isRequired,
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
isLibrary: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SelectionModal;
|
||||
|
||||
@@ -34,8 +34,8 @@ exports[`TinyMceWidget snapshots ImageUploadModal is not rendered 1`] = `
|
||||
],
|
||||
},
|
||||
"initializeEditor": undefined,
|
||||
"isLibrary": true,
|
||||
"learningContextId": "course+org+run",
|
||||
"isLibrary": false,
|
||||
"learningContextId": "library-v1:org+t01",
|
||||
"lmsEndpointUrl": "sOmEvaLue.cOm",
|
||||
"minHeight": undefined,
|
||||
"openImgModal": [MockFunction modal.openModal],
|
||||
@@ -77,6 +77,7 @@ exports[`TinyMceWidget snapshots SourcecodeModal is not rendered 1`] = `
|
||||
],
|
||||
}
|
||||
}
|
||||
isLibrary={true}
|
||||
isOpen={false}
|
||||
lmsEndpointUrl="http://localhost:18000"
|
||||
selection="hooks.selectedImage.selection"
|
||||
@@ -146,6 +147,7 @@ exports[`TinyMceWidget snapshots renders as expected with default behavior 1`] =
|
||||
],
|
||||
}
|
||||
}
|
||||
isLibrary={true}
|
||||
isOpen={false}
|
||||
lmsEndpointUrl="http://localhost:18000"
|
||||
selection="hooks.selectedImage.selection"
|
||||
|
||||
@@ -245,7 +245,6 @@ export const editorConfig = ({
|
||||
setEditorRef,
|
||||
editorContentHtml,
|
||||
images,
|
||||
isLibrary,
|
||||
placeholder,
|
||||
initializeEditor,
|
||||
openImgModal,
|
||||
@@ -268,9 +267,8 @@ export const editorConfig = ({
|
||||
imageToolbar,
|
||||
quickbarsInsertToolbar,
|
||||
quickbarsSelectionToolbar,
|
||||
} = pluginConfig({ isLibrary, placeholder, editorType });
|
||||
} = pluginConfig({ learningContextId, placeholder, editorType });
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
return {
|
||||
onInit: (evt, editor) => {
|
||||
setEditorRef(editor);
|
||||
|
||||
@@ -13,6 +13,7 @@ import ImageUploadModal from '../ImageUploadModal';
|
||||
import SourceCodeModal from '../SourceCodeModal';
|
||||
import * as hooks from './hooks';
|
||||
import './customTinyMcePlugins/embedIframePlugin';
|
||||
import { isLibraryV1Key } from '../../../generic/key-utils';
|
||||
|
||||
export { prepareEditorRef } from './hooks';
|
||||
|
||||
@@ -54,7 +55,7 @@ const TinyMceWidget = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLibrary && (
|
||||
{!isLibraryV1Key(learningContextId) && (
|
||||
<ImageUploadModal
|
||||
isOpen={isImgOpen}
|
||||
close={closeImgModal}
|
||||
@@ -62,6 +63,7 @@ const TinyMceWidget = ({
|
||||
images={imagesRef}
|
||||
editorType={editorType}
|
||||
lmsEndpointUrl={getConfig().LMS_BASE_URL}
|
||||
isLibrary
|
||||
{...imageSelection}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('TinyMceWidget', () => {
|
||||
expect(wrapper.instance.findByType(SourceCodeModal).length).toBe(0);
|
||||
});
|
||||
test('ImageUploadModal is not rendered', () => {
|
||||
const wrapper = shallow(<TinyMceWidget {...props} isLibrary />);
|
||||
const wrapper = shallow(<TinyMceWidget {...props} learningContextId="library-v1:org+t01" />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.instance.findByType(ImageUploadModal).length).toBe(0);
|
||||
});
|
||||
|
||||
@@ -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 : '';
|
||||
|
||||
110
src/editors/utils/formatLibraryImgRequest.ts
Normal file
110
src/editors/utils/formatLibraryImgRequest.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { LibraryAssetResponse } from '../../library-authoring/data/api';
|
||||
|
||||
type GalleryImageData = {
|
||||
displayName: string,
|
||||
url: string,
|
||||
externalUrl: string,
|
||||
portableUrl: string,
|
||||
thumbnail: string,
|
||||
id: string,
|
||||
locked: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* 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(/^.*[\\/]/, '');
|
||||
|
||||
/**
|
||||
* Checks if the provided asset data corresponds to an accepted image file type based on its extension.
|
||||
*
|
||||
* @param data - The asset data containing the file path.
|
||||
* @param acceptedImgExt - The array of accepted image extensions.
|
||||
* @returns `true` if the file has an accepted image extension, otherwise `false`.
|
||||
*
|
||||
* @example
|
||||
* const data = { path: '/static/example.jpg', size: 12345, url: 'http://example.com/static/example.jpg' };
|
||||
* const isImg = isImage(data); // Returns true
|
||||
*/
|
||||
|
||||
export const isImage = (data: LibraryAssetResponse, acceptedImgExt:string[]): boolean => {
|
||||
const ext = data.path.split('.').pop()?.toLowerCase() ?? ''; // Extract and lowercase the file extension
|
||||
return ext !== '' && acceptedImgExt.includes(ext);
|
||||
};
|
||||
/**
|
||||
* Parses a `LibraryAssetResponse` into a `GalleryImageData` object.
|
||||
* This includes extracting the file name and constructing other image-related metadata.
|
||||
*
|
||||
* @param data - The asset data to parse.
|
||||
* @returns The parsed image data with properties like `displayName`, `externalUrl`, etc.
|
||||
*
|
||||
* @example
|
||||
* const data = { path: '/static/example.jpg', size: 12345, url: 'http://example.com/static/example.jpg' };
|
||||
* const imageData = parseLibraryImageData(data);
|
||||
* // {
|
||||
* // displayName: 'example.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): GalleryImageData => ({
|
||||
displayName: getFileName(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 `GalleryImageData`.
|
||||
* Only assets with recognized extension (i.e., valid image files) are included in the result.
|
||||
*
|
||||
* @param librariesAssets - The array of asset data to process.
|
||||
* @param acceptedImgExt - The array of accepted image extensions.
|
||||
* @returns A dictionary where each key is the file name and the value is the corresponding `GalleryImageData`.
|
||||
*
|
||||
* @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',
|
||||
* // 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<LibraryAssetResponse>,
|
||||
acceptedImgExt:string[],
|
||||
): Record<string, GalleryImageData> => librariesAssets.reduce((obj, file) => {
|
||||
if (isImage(file, acceptedImgExt)) {
|
||||
const imageData = parseLibraryImageData(file);
|
||||
return { ...obj, [imageData.displayName]: imageData };
|
||||
}
|
||||
return obj;
|
||||
}, {} as Record<string, GalleryImageData>);
|
||||
96
src/editors/utils/formatLibreryImgRequest.test.ts
Normal file
96
src/editors/utils/formatLibreryImgRequest.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
parseLibraryImageData, getLibraryImageAssets, isImage, getFileName,
|
||||
} from './formatLibraryImgRequest';
|
||||
import { LibraryAssetResponse } from '../../library-authoring/data/api';
|
||||
|
||||
const acceptedImgExt = ['jpg'];
|
||||
|
||||
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('isImage', () => {
|
||||
it('should return true for supported file extensions', () => {
|
||||
const data: LibraryAssetResponse = {
|
||||
path: 'static/example.jpg',
|
||||
size: 12345,
|
||||
url: 'http://example.com/static/example.jpg',
|
||||
};
|
||||
const result = isImage(data, acceptedImgExt);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for unsupported file extensions', () => {
|
||||
const data: LibraryAssetResponse = {
|
||||
path: '/assets/files/unknown.xyz',
|
||||
size: 12345,
|
||||
url: 'http://example.com/assets/files/unknown.xyz',
|
||||
};
|
||||
|
||||
const result = isImage(data, acceptedImgExt);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLibraryImageAssets', () => {
|
||||
it('should filter out assets 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, acceptedImgExt);
|
||||
expect(result).toEqual({
|
||||
'example.jpg': {
|
||||
displayName: 'example.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, acceptedImgExt);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -183,6 +183,12 @@ export interface GetLibrariesV2CustomParams {
|
||||
search?: string,
|
||||
}
|
||||
|
||||
export type LibraryAssetResponse = {
|
||||
path: string,
|
||||
size: number,
|
||||
url: string,
|
||||
};
|
||||
|
||||
export interface CreateBlockDataRequest {
|
||||
libraryId: string;
|
||||
blockType: string;
|
||||
@@ -439,7 +445,7 @@ export async function publishXBlock(usageKey: string) {
|
||||
* Fetch the asset (static file) list for the given XBlock.
|
||||
*/
|
||||
// istanbul ignore next
|
||||
export async function getXBlockAssets(usageKey: string): Promise<{ path: string; url: string; size: number }[]> {
|
||||
export async function getXBlockAssets(usageKey: string): Promise<LibraryAssetResponse[]> {
|
||||
const { data } = await getAuthenticatedHttpClient().get(getXBlockAssetsApiUrl(usageKey));
|
||||
return data.files;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user