From f3ae225d641ffde833aec2da42aba178e1c349db Mon Sep 17 00:00:00 2001 From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com> Date: Mon, 17 Jun 2024 09:52:49 -0400 Subject: [PATCH] feat: improve asset loading (#484) * fix: update initialize to only call required functions * feat: update asset urls without asset object * feat: add pagination to select image modal * fix: lint errors * chore: update tests * fix: asset pattern regex match * feat: update pagination to be button to prevent page skipping * fix: e.target.error for feedback fields * fix: failing snapshots --- .../EditProblemView/AnswerWidget/hooks.js | 24 +-- .../__snapshots__/index.test.jsx.snap | 2 +- .../ExplanationWidget/index.jsx | 11 +- .../ExplanationWidget/index.test.jsx | 12 +- .../EditProblemView/QuestionWidget/index.jsx | 11 +- .../QuestionWidget/index.test.jsx | 10 +- .../components/EditProblemView/hooks.js | 5 +- .../components/EditProblemView/index.jsx | 6 - .../containers/ProblemEditor/index.jsx | 13 +- .../containers/ProblemEditor/index.test.jsx | 23 --- .../__snapshots__/index.test.jsx.snap | 28 +--- src/editors/containers/TextEditor/hooks.js | 6 +- .../containers/TextEditor/hooks.test.jsx | 13 +- src/editors/containers/TextEditor/index.jsx | 36 ++--- .../containers/TextEditor/index.test.jsx | 31 ++-- src/editors/data/constants/requests.js | 4 +- src/editors/data/redux/app/reducer.js | 15 +- src/editors/data/redux/app/reducer.test.js | 20 ++- src/editors/data/redux/app/selectors.js | 21 +-- src/editors/data/redux/app/selectors.test.js | 38 +---- src/editors/data/redux/requests/reducer.js | 2 +- src/editors/data/redux/thunkActions/app.js | 40 +++-- .../data/redux/thunkActions/app.test.js | 143 +++++++++++++++--- .../data/redux/thunkActions/requests.js | 11 +- .../data/redux/thunkActions/requests.test.js | 20 +-- src/editors/data/services/cms/api.js | 13 +- src/editors/data/services/cms/api.test.js | 13 +- src/editors/data/services/cms/mockApi.js | 2 +- src/editors/data/services/cms/urls.js | 2 +- src/editors/data/services/cms/urls.test.js | 2 +- .../SelectImageModal/hooks.js | 28 +++- .../SelectImageModal/hooks.test.js | 16 +- .../SelectImageModal/index.jsx | 14 +- .../SelectionModal/Gallery.jsx | 25 ++- .../SelectionModal/Gallery.test.jsx | 3 + .../SelectionModal/GalleryCard.jsx | 5 +- .../SelectionModal/GalleryLoadMoreButton.jsx | 54 +++++++ .../SelectionModal/SearchSort.jsx | 1 + .../__snapshots__/Gallery.test.jsx.snap | 12 ++ .../__snapshots__/GalleryCard.test.jsx.snap | 54 ++++++- .../sharedComponents/SelectionModal/index.jsx | 1 + .../__snapshots__/index.test.jsx.snap | 3 + .../sharedComponents/TinyMceWidget/hooks.js | 137 +++++++---------- .../TinyMceWidget/hooks.test.js | 62 +++----- .../sharedComponents/TinyMceWidget/index.jsx | 14 +- .../TinyMceWidget/index.test.jsx | 18 ++- .../sharedComponents/TinyMceWidget/utils.js | 15 ++ 47 files changed, 635 insertions(+), 404 deletions(-) create mode 100644 src/editors/sharedComponents/SelectionModal/GalleryLoadMoreButton.jsx create mode 100644 src/editors/sharedComponents/TinyMceWidget/utils.js diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js index 7f040f6fd..0d7b423f6 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js @@ -39,19 +39,23 @@ export const setAnswerTitle = ({ }; export const setSelectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (e) => { - dispatch(actions.problem.updateAnswer({ - id: answer.id, - hasSingleAnswer, - selectedFeedback: e.target.value, - })); + if (e.target) { + dispatch(actions.problem.updateAnswer({ + id: answer.id, + hasSingleAnswer, + selectedFeedback: e.target.value, + })); + } }; export const setUnselectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (e) => { - dispatch(actions.problem.updateAnswer({ - id: answer.id, - hasSingleAnswer, - unselectedFeedback: e.target.value, - })); + if (e.target) { + dispatch(actions.problem.updateAnswer({ + id: answer.id, + hasSingleAnswer, + unselectedFeedback: e.target.value, + })); + } }; export const useFeedback = (answer) => { diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/__snapshots__/index.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/__snapshots__/index.test.jsx.snap index 468b4b388..8a9deb930 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/__snapshots__/index.test.jsx.snap @@ -23,7 +23,7 @@ exports[`SolutionWidget render snapshot: renders correct default 1`] = ` /> <[object Object] - editorContentHtml="This is my question" + editorContentHtml="This is my solution" editorType="solution" id="solution" minHeight={150} diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.jsx index 0dccc5a95..308b1165c 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.jsx @@ -6,15 +6,20 @@ import { selectors } from '../../../../../data/redux'; import messages from './messages'; import TinyMceWidget from '../../../../../sharedComponents/TinyMceWidget'; -import { prepareEditorRef } from '../../../../../sharedComponents/TinyMceWidget/hooks'; +import { prepareEditorRef, replaceStaticWithAsset } from '../../../../../sharedComponents/TinyMceWidget/hooks'; export const ExplanationWidget = ({ // redux settings, + learningContextId, // injected intl, }) => { const { editorRef, refReady, setEditorRef } = prepareEditorRef(); + const solutionContent = replaceStaticWithAsset({ + initialContent: settings?.solutionExplanation, + learningContextId, + }); if (!refReady) { return null; } return (
- props.img.displayName
+
- props.img.displayName
+
- props.img.displayName
+
- props.img.displayName
+
useState(val),
});
-export const addImagesAndDimensionsToRef = ({ imagesRef, assets, editorContentHtml }) => {
- const imagesWithDimensions = module.filterAssets({ assets }).map((image) => {
+export const addImagesAndDimensionsToRef = ({ imagesRef, images, editorContentHtml }) => {
+ const imagesWithDimensions = Object.values(images).map((image) => {
const imageFragment = module.getImageFromHtmlString(editorContentHtml, image.url);
return { ...image, width: imageFragment?.width, height: imageFragment?.height };
});
-
imagesRef.current = imagesWithDimensions;
};
-export const useImages = ({ assets, editorContentHtml }) => {
+export const useImages = ({ images, editorContentHtml }) => {
const imagesRef = useRef([]);
useEffect(() => {
- module.addImagesAndDimensionsToRef({ imagesRef, assets, editorContentHtml });
- }, []);
+ module.addImagesAndDimensionsToRef({ imagesRef, images, editorContentHtml });
+ }, [images]);
return { imagesRef };
};
@@ -69,45 +70,45 @@ export const parseContentForLabels = ({ editor, updateContent }) => {
}
};
-export const replaceStaticwithAsset = ({
- editor,
- imageUrls,
+export const replaceStaticWithAsset = ({
+ initialContent,
+ learningContextId,
editorType,
lmsEndpointUrl,
- updateContent,
}) => {
- let content = editor.getContent();
- const imageSrcs = content.split('src="');
- imageSrcs.forEach(src => {
+ 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 => {
const currentContent = content;
let staticFullUrl;
const isStatic = src.startsWith('/static/');
- 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);
- }
+ 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 })}`;
}
+ } 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 }) => () => {
@@ -132,10 +133,10 @@ export const setupCustomBehavior = ({
openImgModal,
openSourceCodeModal,
editorType,
- imageUrls,
images,
setImage,
lmsEndpointUrl,
+ learningContextId,
}) => (editor) => {
// image upload button
editor.ui.registry.addButton(tinyMCE.buttons.imageUploadButton, {
@@ -188,18 +189,24 @@ export const setupCustomBehavior = ({
});
if (editorType === 'expandable') {
editor.on('init', () => {
- module.replaceStaticwithAsset({
- editor,
- imageUrls,
+ const initialContent = editor.getContent();
+ const newContent = module.replaceStaticWithAsset({
+ initialContent,
editorType,
lmsEndpointUrl,
- updateContent,
+ learningContextId,
});
+ updateContent(newContent);
});
}
editor.on('ExecCommand', (e) => {
if (editorType === 'text' && e.command === 'mceFocus') {
- module.replaceStaticwithAsset({ editor, imageUrls });
+ const initialContent = editor.getContent();
+ const newContent = module.replaceStaticWithAsset({
+ initialContent,
+ learningContextId,
+ });
+ editor.setContent(newContent);
}
if (e.command === 'RemoveFormat') {
editor.formatter.remove('blockquote');
@@ -229,6 +236,7 @@ export const editorConfig = ({
updateContent,
content,
minHeight,
+ learningContextId,
}) => {
const {
toolbar,
@@ -267,7 +275,7 @@ export const editorConfig = ({
setImage: setSelection,
content,
images,
- imageUrls: module.fetchImageUrls(images),
+ learningContextId,
}),
quickbars_insert_toolbar: quickbarsInsertToolbar,
quickbars_selection_toolbar: quickbarsSelectionToolbar,
@@ -380,16 +388,7 @@ export const openModalWithSelectedImage = ({
openImgModal();
};
-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 }) => {
+export const setAssetToStaticUrl = ({ editorValue, lmsEndpointUrl }) => {
/* For assets to remain usable across course instances, we convert their url to be course-agnostic.
* For example, /assets/course/