Revert "feat: improve asset loading (#484)"

This reverts commit f3ae225d64.
This commit is contained in:
bszabo
2024-06-26 10:02:59 -04:00
committed by GitHub
parent c84e3229f6
commit ba8141ea6a
47 changed files with 404 additions and 635 deletions

View File

@@ -43,14 +43,7 @@ export const displayList = ({ sortBy, searchString, images }) => (
imageList: images,
}).sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest]));
export const imgListHooks = ({
searchSortProps,
setSelection,
images,
imageCount,
}) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const dispatch = useDispatch();
export const imgListHooks = ({ searchSortProps, setSelection, images }) => {
const [highlighted, setHighlighted] = module.state.highlighted(null);
const [
showSelectImageError,
@@ -80,9 +73,6 @@ export const imgListHooks = ({
highlighted,
onHighlightChange: (e) => setHighlighted(e.target.value),
emptyGalleryLabel: messages.emptyGalleryLabel,
allowLazyLoad: true,
fetchNextPage: ({ pageNumber }) => dispatch(thunkActions.app.fetchImages({ pageNumber })),
assetCount: imageCount,
},
// highlight by id
selectBtnProps: {
@@ -128,7 +118,7 @@ export const fileInputHooks = ({ setSelection, clearSelection, imgList }) => {
},
})) {
dispatch(
thunkActions.app.uploadAsset({
thunkActions.app.uploadImage({
file: selectedFile,
setSelection,
}),
@@ -143,19 +133,9 @@ export const fileInputHooks = ({ setSelection, clearSelection, imgList }) => {
};
};
export const imgHooks = ({
setSelection,
clearSelection,
images,
imageCount,
}) => {
export const imgHooks = ({ setSelection, clearSelection, images }) => {
const searchSortProps = module.searchAndSortHooks();
const imgList = module.imgListHooks({
setSelection,
searchSortProps,
images,
imageCount,
});
const imgList = module.imgListHooks({ setSelection, searchSortProps, images });
const fileInput = module.fileInputHooks({
setSelection,
clearSelection,

View File

@@ -27,7 +27,7 @@ jest.mock('react-redux', () => {
jest.mock('../../../data/redux', () => ({
thunkActions: {
app: {
uploadAsset: jest.fn(),
uploadImage: jest.fn(),
},
},
}));
@@ -248,7 +248,7 @@ describe('SelectImageModal hooks', () => {
hook.click();
expect(click).toHaveBeenCalled();
});
describe('addFile (uploadAsset args)', () => {
describe('addFile (uploadImage args)', () => {
const eventSuccess = { target: { files: [{ value: testValue, size: 2000 }] } };
const eventFailure = { target: { files: [testValueInvalidImage] } };
it('image fails to upload if file size is greater than 1000000', () => {
@@ -259,14 +259,14 @@ describe('SelectImageModal hooks', () => {
expect(spies.checkValidFileSize.mock.calls.length).toEqual(1);
expect(spies.checkValidFileSize).toHaveReturnedWith(false);
});
it('dispatches uploadAsset thunkAction with the first target file and setSelection', () => {
it('dispatches uploadImage thunkAction with the first target file and setSelection', () => {
const checkValidFileSize = true;
spies.checkValidFileSize = jest.spyOn(hooks, hookKeys.checkValidFileSize)
.mockReturnValueOnce(checkValidFileSize);
hook.addFile(eventSuccess);
expect(spies.checkValidFileSize.mock.calls.length).toEqual(1);
expect(spies.checkValidFileSize).toHaveReturnedWith(true);
expect(dispatch).toHaveBeenCalledWith(thunkActions.app.uploadAsset({
expect(dispatch).toHaveBeenCalledWith(thunkActions.app.uploadImage({
file: testValue,
setSelection,
}));
@@ -281,7 +281,6 @@ describe('SelectImageModal hooks', () => {
const searchAndSortHooks = { search: 'props' };
const fileInputHooks = { file: 'input hooks' };
const images = { sOmEuiMAge: { staTICUrl: '/assets/sOmEuiMAge' } };
const imageCount = 1;
const setSelection = jest.fn();
const clearSelection = jest.fn();
@@ -293,11 +292,9 @@ describe('SelectImageModal hooks', () => {
.mockReturnValueOnce(searchAndSortHooks);
spies.file = jest.spyOn(hooks, hookKeys.fileInputHooks)
.mockReturnValueOnce(fileInputHooks);
hook = hooks.imgHooks({
setSelection, clearSelection, images, imageCount,
});
hook = hooks.imgHooks({ setSelection, clearSelection, images });
});
it('forwards fileInputHooks as fileInput, called with uploadAsset prop', () => {
it('forwards fileInputHooks as fileInput, called with uploadImage prop', () => {
expect(hook.fileInput).toEqual(fileInputHooks);
expect(spies.file.mock.calls.length).toEqual(1);
expect(spies.file).toHaveBeenCalledWith({
@@ -310,7 +307,6 @@ describe('SelectImageModal hooks', () => {
setSelection,
searchSortProps: searchAndSortHooks,
images,
imageCount,
});
});
it('forwards searchAndSortHooks as searchSortProps', () => {

View File

@@ -17,7 +17,6 @@ export const SelectImageModal = ({
isLoaded,
isFetchError,
isUploadError,
imageCount,
}) => {
const {
galleryError,
@@ -26,12 +25,7 @@ export const SelectImageModal = ({
galleryProps,
searchSortProps,
selectBtnProps,
} = hooks.imgHooks({
setSelection,
clearSelection,
images: images.current,
imageCount,
});
} = hooks.imgHooks({ setSelection, clearSelection, images: images.current });
const modalMessages = {
confirmMsg: messages.nextButtonLabel,
@@ -72,14 +66,12 @@ SelectImageModal.propTypes = {
isLoaded: PropTypes.bool.isRequired,
isFetchError: PropTypes.bool.isRequired,
isUploadError: PropTypes.bool.isRequired,
imageCount: PropTypes.number.isRequired,
};
export const mapStateToProps = (state) => ({
isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchImages }),
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchImages }),
isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchAssets }),
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchAssets }),
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }),
imageCount: state.app.imageCount,
});
export const mapDispatchToProps = {};

View File

@@ -11,7 +11,6 @@ import {
import SelectableBox from '../SelectableBox';
import messages from './messages';
import GalleryCard from './GalleryCard';
import GalleryLoadMoreButton from './GalleryLoadMoreButton';
export const Gallery = ({
galleryIsEmpty,
@@ -24,13 +23,9 @@ export const Gallery = ({
height,
isLoaded,
thumbnailFallback,
allowLazyLoad,
fetchNextPage,
assetCount,
}) => {
const intl = useIntl();
if (!isLoaded && !allowLazyLoad) {
if (!isLoaded) {
return (
<div style={{
position: 'absolute',
@@ -70,7 +65,7 @@ export const Gallery = ({
type="radio"
value={highlighted}
>
{displayList.map(asset => (
{ displayList.map(asset => (
<GalleryCard
key={asset.id}
asset={asset}
@@ -79,16 +74,6 @@ export const Gallery = ({
/>
)) }
</SelectableBox.Set>
{allowLazyLoad && (
<GalleryLoadMoreButton
{...{
fetchNextPage,
assetCount,
displayListLength: displayList.length,
isLoaded,
}}
/>
)}
</div>
);
};
@@ -99,9 +84,6 @@ Gallery.defaultProps = {
height: '375px',
show: true,
thumbnailFallback: undefined,
allowLazyLoad: false,
fetchNextPage: null,
assetCount: 0,
};
Gallery.propTypes = {
show: PropTypes.bool,
@@ -115,9 +97,6 @@ Gallery.propTypes = {
showIdsOnCards: PropTypes.bool,
height: PropTypes.string,
thumbnailFallback: PropTypes.element,
allowLazyLoad: PropTypes.bool,
fetchNextPage: PropTypes.func,
assetCount: PropTypes.number,
};
export default Gallery;

View File

@@ -28,9 +28,6 @@ describe('TextEditor Image Gallery component', () => {
highlighted: 'props.highlighted',
onHighlightChange: jest.fn().mockName('props.onHighlightChange'),
isLoaded: true,
fetchNextPage: null,
assetCount: 0,
allowLazyLoad: false,
};
const shallowWithIntl = (component) => shallow(<IntlProvider locale="en">{component}</IntlProvider>);
test('snapshot: not loaded, show spinner', () => {

View File

@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import {
Badge,
Image,
Truncate,
} from '@openedx/paragon';
import { FormattedMessage, FormattedDate, FormattedTime } from '@edx/frontend-platform/i18n';
@@ -65,9 +64,7 @@ export const GalleryCard = ({
)}
</div>
<div className="card-text px-3 py-2" style={{ marginTop: '10px' }}>
<h3 className="text-primary-500">
<Truncate>{asset.displayName}</Truncate>
</h3>
<h3 className="text-primary-500">{asset.displayName}</h3>
{ asset.transcripts && (
<div style={{ margin: '0 0 5px 0' }}>
<LanguageNamesWidget

View File

@@ -1,54 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Icon, StatefulButton } from '@openedx/paragon';
import { ExpandMore, SpinnerSimple } from '@openedx/paragon/icons';
const GalleryLoadMoreButton = ({
assetCount,
displayListLength,
fetchNextPage,
isLoaded,
}) => {
const [currentPage, setCurrentPage] = useState(1);
const handlePageChange = () => {
fetchNextPage({ pageNumber: currentPage });
setCurrentPage(currentPage + 1);
};
const buttonState = isLoaded ? 'default' : 'pending';
const buttonProps = {
labels: {
default: 'Load more',
pending: 'Loading',
},
icons: {
default: <Icon src={ExpandMore} />,
pending: <Icon src={SpinnerSimple} className="icon-spin" />,
},
disabledStates: ['pending'],
variant: 'primary',
};
return (
<div className="row justify-content-center py-3">
{displayListLength !== assetCount && (
<StatefulButton
{...buttonProps}
onClick={handlePageChange}
state={buttonState}
/>
)}
</div>
);
};
GalleryLoadMoreButton.propTypes = {
assetCount: PropTypes.number.isRequired,
displayListLength: PropTypes.number.isRequired,
fetchNextPage: PropTypes.func.isRequired,
currentPage: PropTypes.number.isRequired,
setCurrentPage: PropTypes.func.isRequired,
isLoaded: PropTypes.bool.isRequired,
};
export default GalleryLoadMoreButton;

View File

@@ -29,7 +29,6 @@ export const SearchSort = ({
onSwitchClick,
}) => {
const intl = useIntl();
return (
<ActionRow>
<Form.Group style={{ margin: 0 }}>

View File

@@ -43,8 +43,6 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no im
}
>
<Gallery
allowLazyLoad={false}
assetCount={0}
displayList={
[
{
@@ -64,7 +62,6 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no im
"id": "emptyGalleryMsg",
}
}
fetchNextPage={null}
galleryIsEmpty={true}
height="375px"
highlighted="props.highlighted"
@@ -120,8 +117,6 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but searc
}
>
<Gallery
allowLazyLoad={false}
assetCount={0}
displayList={
[
{
@@ -141,7 +136,6 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but searc
"id": "emptyGalleryMsg",
}
}
fetchNextPage={null}
galleryIsEmpty={false}
height="375px"
highlighted="props.highlighted"
@@ -197,8 +191,6 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal
}
>
<Gallery
allowLazyLoad={false}
assetCount={0}
displayList={
[
{
@@ -218,7 +210,6 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal
"id": "emptyGalleryMsg",
}
}
fetchNextPage={null}
galleryIsEmpty={false}
height="375px"
highlighted="props.highlighted"
@@ -274,8 +265,6 @@ exports[`TextEditor Image Gallery component component snapshot: not loaded, show
}
>
<Gallery
allowLazyLoad={false}
assetCount={0}
displayList={
[
{
@@ -295,7 +284,6 @@ exports[`TextEditor Image Gallery component component snapshot: not loaded, show
"id": "emptyGalleryMsg",
}
}
fetchNextPage={null}
galleryIsEmpty={false}
height="375px"
highlighted="props.highlighted"

View File

@@ -59,14 +59,7 @@ exports[`GalleryCard component snapshot with duration badge 1`] = `
<h3
className="text-primary-500"
>
<Truncate
elementType="div"
ellipsis="..."
lines={1}
whiteSpace={false}
>
props.img.displayName
</Truncate>
props.img.displayName
</h3>
<p
className="text-gray-500"
@@ -143,14 +136,7 @@ exports[`GalleryCard component snapshot with duration transcripts 1`] = `
<h3
className="text-primary-500"
>
<Truncate
elementType="div"
ellipsis="..."
lines={1}
whiteSpace={false}
>
props.img.displayName
</Truncate>
props.img.displayName
</h3>
<div
style={
@@ -242,14 +228,7 @@ exports[`GalleryCard component snapshot with status badge 1`] = `
<h3
className="text-primary-500"
>
<Truncate
elementType="div"
ellipsis="..."
lines={1}
whiteSpace={false}
>
props.img.displayName
</Truncate>
props.img.displayName
</h3>
<p
className="text-gray-500"
@@ -327,14 +306,7 @@ exports[`GalleryCard component snapshot with thumbnail fallback and load error 1
<h3
className="text-primary-500"
>
<Truncate
elementType="div"
ellipsis="..."
lines={1}
whiteSpace={false}
>
props.img.displayName
</Truncate>
props.img.displayName
</h3>
<p
className="text-gray-500"
@@ -412,14 +384,7 @@ exports[`GalleryCard component snapshot with thumbnail fallback and no error 1`]
<h3
className="text-primary-500"
>
<Truncate
elementType="div"
ellipsis="..."
lines={1}
whiteSpace={false}
>
props.img.displayName
</Truncate>
props.img.displayName
</h3>
<p
className="text-gray-500"
@@ -496,14 +461,7 @@ exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
<h3
className="text-primary-500"
>
<Truncate
elementType="div"
ellipsis="..."
lines={1}
whiteSpace={false}
>
props.img.displayName
</Truncate>
props.img.displayName
</h3>
<p
className="text-gray-500"

View File

@@ -56,7 +56,6 @@ export const SelectionModal = ({
isLoaded,
...galleryProps,
};
return (
<BaseModal
close={close}

View File

@@ -45,7 +45,6 @@ exports[`TinyMceWidget snapshots ImageUploadModal is not rendered 1`] = `
},
"initializeEditor": undefined,
"isLibrary": true,
"learningContextId": "course+org+run",
"lmsEndpointUrl": "sOmEvaLue.cOm",
"minHeight": undefined,
"openImgModal": [MockFunction modal.openModal],
@@ -123,7 +122,6 @@ exports[`TinyMceWidget snapshots SourcecodeModal is not rendered 1`] = `
},
"initializeEditor": undefined,
"isLibrary": false,
"learningContextId": "course+org+run",
"lmsEndpointUrl": "sOmEvaLue.cOm",
"minHeight": undefined,
"openImgModal": [MockFunction modal.openModal],
@@ -212,7 +210,6 @@ exports[`TinyMceWidget snapshots renders as expected with default behavior 1`] =
},
"initializeEditor": undefined,
"isLibrary": false,
"learningContextId": "course+org+run",
"lmsEndpointUrl": "sOmEvaLue.cOm",
"minHeight": undefined,
"openImgModal": [MockFunction modal.openModal],

View File

@@ -5,13 +5,11 @@ import {
useEffect,
} from 'react';
import { a11ycheckerCss } from 'frontend-components-tinymce-advanced-plugins';
import { isEmpty } from 'lodash-es';
import tinyMCEStyles from '../../data/constants/tinyMCEStyles';
import { StrictDict } from '../../utils';
import pluginConfig from './pluginConfig';
import * as module from './hooks';
import tinyMCE from '../../data/constants/tinyMCE';
import { getRelativeUrl, getStaticUrl } from './utils';
export const state = StrictDict({
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -24,20 +22,21 @@ export const state = StrictDict({
refReady: (val) => useState(val),
});
export const addImagesAndDimensionsToRef = ({ imagesRef, images, editorContentHtml }) => {
const imagesWithDimensions = Object.values(images).map((image) => {
export const addImagesAndDimensionsToRef = ({ imagesRef, assets, editorContentHtml }) => {
const imagesWithDimensions = module.filterAssets({ assets }).map((image) => {
const imageFragment = module.getImageFromHtmlString(editorContentHtml, image.url);
return { ...image, width: imageFragment?.width, height: imageFragment?.height };
});
imagesRef.current = imagesWithDimensions;
};
export const useImages = ({ images, editorContentHtml }) => {
export const useImages = ({ assets, editorContentHtml }) => {
const imagesRef = useRef([]);
useEffect(() => {
module.addImagesAndDimensionsToRef({ imagesRef, images, editorContentHtml });
}, [images]);
module.addImagesAndDimensionsToRef({ imagesRef, assets, editorContentHtml });
}, []);
return { imagesRef };
};
@@ -70,45 +69,45 @@ export const parseContentForLabels = ({ editor, updateContent }) => {
}
};
export const replaceStaticWithAsset = ({
initialContent,
learningContextId,
export const replaceStaticwithAsset = ({
editor,
imageUrls,
editorType,
lmsEndpointUrl,
updateContent,
}) => {
let content = initialContent;
const srcs = content.split(/(src="|src=&quot;|href="|href=&quot)/g).filter(
src => src.startsWith('/static') || src.startsWith('/asset'),
);
if (isEmpty(srcs)) {
return initialContent;
}
srcs.forEach(src => {
let content = editor.getContent();
const imageSrcs = content.split('src="');
imageSrcs.forEach(src => {
const currentContent = content;
let staticFullUrl;
const isStatic = src.startsWith('/static/');
const assetSrc = src.substring(0, src.indexOf('"'));
const staticName = assetSrc.substring(8);
const assetName = assetSrc.replace(/\/assets\/.+[^/]\//g, '');
const displayName = isStatic ? staticName : assetName;
const isCorrectAssetFormat = assetSrc.match(/\/asset-v1:\S+[+]\S+[@]\S+[+]\S+[@]/g)?.length >= 1;
// assets in expandable text areas so not support relative urls so all assets must have the lms
// endpoint prepended to the relative url
if (editorType === 'expandable') {
if (isCorrectAssetFormat) {
staticFullUrl = `${lmsEndpointUrl}${assetSrc}`;
} else {
staticFullUrl = `${lmsEndpointUrl}${getRelativeUrl({ courseId: learningContextId, displayName })}`;
const isExpandableAsset = src.startsWith('/assets/') && editorType === 'expandable';
if ((isStatic || isExpandableAsset) && imageUrls.length > 0) {
const assetSrc = src.substring(0, src.indexOf('"'));
const assetName = assetSrc.replace(/\/assets\/.+[^/]\//g, '');
const staticName = assetSrc.substring(8);
imageUrls.forEach((url) => {
if (isExpandableAsset && assetName === url.displayName) {
staticFullUrl = `${lmsEndpointUrl}${url.staticFullUrl}`;
} else if (staticName === url.displayName) {
staticFullUrl = url.staticFullUrl;
if (isExpandableAsset) {
staticFullUrl = `${lmsEndpointUrl}${url.staticFullUrl}`;
}
}
});
if (staticFullUrl) {
const currentSrc = src.substring(0, src.indexOf('"'));
content = currentContent.replace(currentSrc, staticFullUrl);
if (editorType === 'expandable') {
updateContent(content);
} else {
editor.setContent(content);
}
}
} else if (!isCorrectAssetFormat) {
staticFullUrl = getRelativeUrl({ courseId: learningContextId, displayName });
}
if (staticFullUrl) {
const currentSrc = src.substring(0, src.indexOf('"'));
content = currentContent.replace(currentSrc, staticFullUrl);
}
});
return content;
};
export const getImageResizeHandler = ({ editor, imagesRef, setImage }) => () => {
@@ -133,10 +132,10 @@ export const setupCustomBehavior = ({
openImgModal,
openSourceCodeModal,
editorType,
imageUrls,
images,
setImage,
lmsEndpointUrl,
learningContextId,
}) => (editor) => {
// image upload button
editor.ui.registry.addButton(tinyMCE.buttons.imageUploadButton, {
@@ -189,24 +188,18 @@ export const setupCustomBehavior = ({
});
if (editorType === 'expandable') {
editor.on('init', () => {
const initialContent = editor.getContent();
const newContent = module.replaceStaticWithAsset({
initialContent,
module.replaceStaticwithAsset({
editor,
imageUrls,
editorType,
lmsEndpointUrl,
learningContextId,
updateContent,
});
updateContent(newContent);
});
}
editor.on('ExecCommand', (e) => {
if (editorType === 'text' && e.command === 'mceFocus') {
const initialContent = editor.getContent();
const newContent = module.replaceStaticWithAsset({
initialContent,
learningContextId,
});
editor.setContent(newContent);
module.replaceStaticwithAsset({ editor, imageUrls });
}
if (e.command === 'RemoveFormat') {
editor.formatter.remove('blockquote');
@@ -236,7 +229,6 @@ export const editorConfig = ({
updateContent,
content,
minHeight,
learningContextId,
}) => {
const {
toolbar,
@@ -275,7 +267,7 @@ export const editorConfig = ({
setImage: setSelection,
content,
images,
learningContextId,
imageUrls: module.fetchImageUrls(images),
}),
quickbars_insert_toolbar: quickbarsInsertToolbar,
quickbars_selection_toolbar: quickbarsSelectionToolbar,
@@ -388,7 +380,16 @@ export const openModalWithSelectedImage = ({
openImgModal();
};
export const setAssetToStaticUrl = ({ editorValue, lmsEndpointUrl }) => {
export const filterAssets = ({ assets }) => {
let images = [];
const assetsList = Object.values(assets);
if (assetsList.length > 0) {
images = assetsList.filter(asset => asset?.contentType?.startsWith('image/'));
}
return images;
};
export const setAssetToStaticUrl = ({ editorValue, assets, lmsEndpointUrl }) => {
/* For assets to remain usable across course instances, we convert their url to be course-agnostic.
* For example, /assets/course/<asset hash>/filename gets converted to /static/filename. This is
* important for rerunning courses and importing/exporting course as the /static/ part of the url
@@ -400,20 +401,42 @@ export const setAssetToStaticUrl = ({ editorValue, lmsEndpointUrl }) => {
const regExLmsEndpointUrl = RegExp(lmsEndpointUrl, 'g');
let content = editorValue.replace(regExLmsEndpointUrl, '');
const assetUrls = [];
const assetsList = Object.values(assets);
assetsList.forEach(asset => {
assetUrls.push({ portableUrl: asset.portableUrl, displayName: asset.displayName });
});
const assetSrcs = typeof content === 'string' ? content.split(/(src="|src=&quot;|href="|href=&quot)/g) : [];
assetSrcs.forEach(src => {
if (src.startsWith('/asset')) {
if (src.startsWith('/asset') && assetUrls.length > 0) {
const assetBlockName = src.substring(src.indexOf('@') + 1, src.search(/("|&quot;)/));
const nameFromEditorSrc = assetBlockName.substring(assetBlockName.indexOf('@') + 1);
const portableUrl = getStaticUrl({ displayName: nameFromEditorSrc });
const currentSrc = src.substring(0, src.search(/("|&quot;)/));
const updatedContent = content.replace(currentSrc, portableUrl);
content = updatedContent;
const nameFromStudioSrc = assetBlockName.substring(assetBlockName.indexOf('/') + 1);
let portableUrl;
assetUrls.forEach((url) => {
const displayName = url.displayName.replace(/\s/g, '_');
if (displayName === nameFromEditorSrc || displayName === nameFromStudioSrc) {
portableUrl = url.portableUrl;
}
});
if (portableUrl) {
const currentSrc = src.substring(0, src.search(/("|&quot;)/));
const updatedContent = content.replace(currentSrc, portableUrl);
content = updatedContent;
}
}
});
return content;
};
export const fetchImageUrls = (images) => {
const imageUrls = [];
images.current.forEach(image => {
imageUrls.push({ staticFullUrl: image.staticFullUrl, displayName: image.displayName });
});
return imageUrls;
};
export const selectedImage = (val) => {
const [selection, setSelection] = module.state.imageSelection(val);
return {

View File

@@ -49,7 +49,7 @@ const mockImage = {
height: initialContentHeight,
};
const mockImages = {
const mockAssets = {
[mockImage.id]: mockImage,
};
@@ -181,32 +181,41 @@ describe('TinyMceEditor hooks', () => {
});
});
describe('replaceStaticWithAsset', () => {
const initialContent = '<img src="/static/soMEImagEURl1.jpeg"/><a href="/assets/v1/some-key/test.pdf">test</a>';
const learningContextId = 'course+test+run';
const lmsEndpointUrl = 'sOmEvaLue.cOm';
it('it returns updated src for text editor to update content', () => {
const expected = '<img src="/asset+test+run+type@asset+block@soMEImagEURl1.jpeg"/><a href="/asset+test+run+type@asset+block@test.pdf">test</a>';
const actual = module.replaceStaticWithAsset({ initialContent, learningContextId });
expect(actual).toEqual(expected);
describe('replaceStaticwithAsset', () => {
test('it calls getContent and setContent for text editor', () => {
const editor = { getContent: jest.fn(() => '<img src="/static/soMEImagEURl1.jpeg"/>'), setContent: jest.fn() };
const imageUrls = [{ staticFullUrl: '/assets/soMEImagEURl1.jpeg', displayName: 'soMEImagEURl1.jpeg' }];
const lmsEndpointUrl = 'sOmEvaLue.cOm';
module.replaceStaticwithAsset({ editor, imageUrls, lmsEndpointUrl });
expect(editor.getContent).toHaveBeenCalled();
expect(editor.setContent).toHaveBeenCalled();
});
it('it returs updated src with absolute url for expandable editor to update content', () => {
test('it calls getContent and updateContent for expandable editor', () => {
const editor = { getContent: jest.fn(() => '<img src="/static/soMEImagEURl1.jpeg"/>') };
const imageUrls = [{ staticFullUrl: '/assets/soMEImagEURl1.jpeg', displayName: 'soMEImagEURl1.jpeg' }];
const lmsEndpointUrl = 'sOmEvaLue.cOm';
const editorType = 'expandable';
const expected = `<img src="${lmsEndpointUrl}/asset+test+run+type@asset+block@soMEImagEURl1.jpeg"/><a href="${lmsEndpointUrl}/asset+test+run+type@asset+block@test.pdf">test</a>`;
const actual = module.replaceStaticWithAsset({
initialContent,
const updateContent = jest.fn();
module.replaceStaticwithAsset({
editor,
imageUrls,
editorType,
lmsEndpointUrl,
learningContextId,
updateContent,
});
expect(actual).toEqual(expected);
expect(editor.getContent).toHaveBeenCalled();
expect(updateContent).toHaveBeenCalled();
});
});
describe('setAssetToStaticUrl', () => {
it('returns content with updated img links', () => {
const editorValue = '<img src="/asset@/soME_ImagE_URl1"/> <a href="/asset@soMEImagEURl">testing link</a>';
const editorValue = '<img src="/asset@asset-block/soME_ImagE_URl1"/> <a href="/asset@soMEImagEURl">testing link</a>';
const assets = [
{ portableUrl: '/static/soMEImagEURl', displayName: 'soMEImagEURl' },
{ portableUrl: '/static/soME_ImagE_URl1', displayName: 'soME ImagE URl1' },
];
const lmsEndpointUrl = 'sOmEvaLue.cOm';
const content = module.setAssetToStaticUrl({ editorValue, lmsEndpointUrl });
const content = module.setAssetToStaticUrl({ editorValue, assets, lmsEndpointUrl });
expect(content).toEqual('<img src="/static/soME_ImagE_URl1"/> <a href="/static/soMEImagEURl">testing link</a>');
});
});
@@ -219,7 +228,6 @@ describe('TinyMceEditor hooks', () => {
studioEndpointUrl: 'sOmEoThEruRl.cOm',
images: mockImagesRef,
isLibrary: false,
learningContextId: 'course+org+run',
};
const evt = 'fakeEvent';
const editor = 'myEditor';
@@ -336,14 +344,27 @@ describe('TinyMceEditor hooks', () => {
openImgModal: props.openImgModal,
openSourceCodeModal: props.openSourceCodeModal,
setImage: props.setSelection,
imageUrls: module.fetchImageUrls(props.images),
images: mockImagesRef,
lmsEndpointUrl: props.lmsEndpointUrl,
learningContextId: props.learningContextId,
}),
);
});
});
describe('filterAssets', () => {
const emptyAssets = {};
const assets = { sOmEaSsET: { contentType: 'image/' } };
test('returns an empty array', () => {
const emptyFilterAssets = module.filterAssets({ assets: emptyAssets });
expect(emptyFilterAssets).toEqual([]);
});
test('returns filtered array of images', () => {
const FilteredAssets = module.filterAssets({ assets });
expect(FilteredAssets).toEqual([{ contentType: 'image/' }]);
});
});
describe('imgModalToggle', () => {
const hookKey = state.keys.isImageModalOpen;
beforeEach(() => {
@@ -501,10 +522,11 @@ describe('TinyMceEditor hooks', () => {
describe('addImagesAndDimensionsToRef', () => {
it('should add images to ref', () => {
const imagesRef = { current: null };
const assets = { ...mockAssets, height: undefined, width: undefined };
module.addImagesAndDimensionsToRef(
{
imagesRef,
images: mockImages,
assets,
editorContentHtml: mockEditorContentHtml,
},
);

View File

@@ -41,8 +41,7 @@ export const TinyMceWidget = ({
id,
editorContentHtml, // editorContent in html form
// redux
learningContextId,
images,
assets,
isLibrary,
lmsEndpointUrl,
studioEndpointUrl,
@@ -51,7 +50,7 @@ export const TinyMceWidget = ({
}) => {
const { isImgOpen, openImgModal, closeImgModal } = hooks.imgModalToggle();
const { isSourceCodeOpen, openSourceCodeModal, closeSourceCodeModal } = hooks.sourceCodeModalToggle(editorRef);
const { imagesRef } = hooks.useImages({ images, editorContentHtml });
const { imagesRef } = hooks.useImages({ assets, editorContentHtml });
const imageSelection = hooks.selectedImage(null);
@@ -86,7 +85,6 @@ export const TinyMceWidget = ({
editorType,
editorRef,
isLibrary,
learningContextId,
lmsEndpointUrl,
studioEndpointUrl,
images: imagesRef,
@@ -105,7 +103,7 @@ TinyMceWidget.defaultProps = {
editorRef: null,
lmsEndpointUrl: null,
studioEndpointUrl: null,
images: null,
assets: null,
id: null,
disabled: false,
editorContentHtml: undefined,
@@ -114,10 +112,9 @@ TinyMceWidget.defaultProps = {
...editorConfigDefaultProps,
};
TinyMceWidget.propTypes = {
learningContextId: PropTypes.string,
editorType: PropTypes.string,
isLibrary: PropTypes.bool,
images: PropTypes.shape({}),
assets: PropTypes.shape({}),
editorRef: PropTypes.shape({}),
lmsEndpointUrl: PropTypes.string,
studioEndpointUrl: PropTypes.string,
@@ -130,11 +127,10 @@ TinyMceWidget.propTypes = {
};
export const mapStateToProps = (state) => ({
images: selectors.app.images(state),
assets: selectors.app.assets(state),
lmsEndpointUrl: selectors.app.lmsEndpointUrl(state),
studioEndpointUrl: selectors.app.studioEndpointUrl(state),
isLibrary: selectors.app.isLibrary(state),
learningContextId: selectors.app.learningContextId(state),
});
export default (connect(mapStateToProps)(TinyMceWidget));

View File

@@ -30,8 +30,7 @@ jest.mock('../../data/redux', () => ({
lmsEndpointUrl: jest.fn(state => ({ lmsEndpointUrl: state })),
studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
isLibrary: jest.fn(state => ({ isLibrary: state })),
images: jest.fn(state => ({ images: state })),
learningContextId: jest.fn(state => ({ learningContextId: state })),
assets: jest.fn(state => ({ assets: state })),
},
},
}));
@@ -53,6 +52,7 @@ jest.mock('./hooks', () => ({
setSelection: jest.fn().mockName('hooks.selectedImage.setSelection'),
clearSelection: jest.fn().mockName('hooks.selectedImage.clearSelection'),
})),
filterAssets: jest.fn(() => [{ staTICUrl: staticUrl }]),
useImages: jest.fn(() => ({ imagesRef: { current: [{ externalUrl: staticUrl }] } })),
}));
@@ -70,13 +70,12 @@ describe('TinyMceWidget', () => {
editorType: 'text',
editorRef: { current: { value: 'something' } },
isLibrary: false,
images: { sOmEaSsET: { staTICUrl: staticUrl } },
assets: { sOmEaSsET: { staTICUrl: staticUrl } },
lmsEndpointUrl: 'sOmEvaLue.cOm',
studioEndpointUrl: 'sOmEoThERvaLue.cOm',
disabled: false,
id: 'sOMeiD',
updateContent: () => ({}),
learningContextId: 'course+org+run',
};
describe('snapshots', () => {
imgModalToggle.mockReturnValue({
@@ -115,20 +114,15 @@ describe('TinyMceWidget', () => {
mapStateToProps(testState).studioEndpointUrl,
).toEqual(selectors.app.studioEndpointUrl(testState));
});
test('images from app.images', () => {
test('assets from app.assets', () => {
expect(
mapStateToProps(testState).images,
).toEqual(selectors.app.images(testState));
mapStateToProps(testState).assets,
).toEqual(selectors.app.assets(testState));
});
test('isLibrary from app.isLibrary', () => {
expect(
mapStateToProps(testState).isLibrary,
).toEqual(selectors.app.isLibrary(testState));
});
test('learningContextId from app.learningContextId', () => {
expect(
mapStateToProps(testState).learningContextId,
).toEqual(selectors.app.learningContextId(testState));
});
});
});

View File

@@ -1,15 +0,0 @@
const getLocatorSafeName = ({ displayName }) => {
const locatorSafeName = displayName.replace(/[^\w.%-]/gm, '');
return locatorSafeName;
};
export const getStaticUrl = ({ displayName }) => (`/static/${getLocatorSafeName({ displayName })}`);
export const getRelativeUrl = ({ courseId, displayName }) => {
if (displayName) {
const assetCourseId = courseId.replace('course', 'asset');
const assetPathShell = `/${assetCourseId}+type@asset+block@`;
return `${assetPathShell}${displayName}`;
}
return '';
};