Revert "feat: improve asset loading (#484)"
This reverts commit f3ae225d64.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -29,7 +29,6 @@ export const SearchSort = ({
|
||||
onSwitchClick,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<ActionRow>
|
||||
<Form.Group style={{ margin: 0 }}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -56,7 +56,6 @@ export const SelectionModal = ({
|
||||
isLoaded,
|
||||
...galleryProps,
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
close={close}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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="|href="|href=")/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="|href="|href=")/g) : [];
|
||||
assetSrcs.forEach(src => {
|
||||
if (src.startsWith('/asset')) {
|
||||
if (src.startsWith('/asset') && assetUrls.length > 0) {
|
||||
const assetBlockName = src.substring(src.indexOf('@') + 1, src.search(/("|")/));
|
||||
const nameFromEditorSrc = assetBlockName.substring(assetBlockName.indexOf('@') + 1);
|
||||
const portableUrl = getStaticUrl({ displayName: nameFromEditorSrc });
|
||||
const currentSrc = src.substring(0, src.search(/("|")/));
|
||||
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(/("|")/));
|
||||
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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 '';
|
||||
};
|
||||
Reference in New Issue
Block a user