fix: image rendering in single and multi select problems (#1731)

Fix images in single and multi select problems in libraries. Found following issues and fixed them:

* Images were not being rendered in any of the fields in these problems.
* Base url was not being set which is used by tinymce to load images with relative path.
* Answer fields were set to inline mode which does not initialize images or base paths
* If same image twice is used twice in a problem, the logic of replacing `static/image.jpg` with `/static/image.jpg` would replace the first occurrence twice resulting in `//static/image.jpg`, breaking both the links.
* On initialization of answer fields, the absolute static asset urls were being replaced by relative urls causing the editor being set as dirty without user changes.
This commit is contained in:
Navin Karkera
2025-03-13 16:11:44 +00:00
committed by GitHub
parent 77a55d9ad3
commit e66da2cb49
17 changed files with 103 additions and 16 deletions

View File

@@ -9,12 +9,14 @@ exports[`ExpandableTextArea snapshots renders as expected with default behavior
editorContentHtml="text"
editorRef={
{
"current": null,
"current": {
"value": "something",
},
}
}
editorType="expandable"
placeholder={null}
setEditorRef={[Function]}
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
updateContent={[MockFunction]}
/>
</div>
@@ -30,12 +32,14 @@ exports[`ExpandableTextArea snapshots renders error message 1`] = `
editorContentHtml="text"
editorRef={
{
"current": null,
"current": {
"value": "something",
},
}
}
editorType="expandable"
placeholder={null}
setEditorRef={[Function]}
setEditorRef={[MockFunction hooks.prepareEditorRef.setEditorRef]}
updateContent={[MockFunction]}
/>
</div>

View File

@@ -11,8 +11,9 @@ const ExpandableTextArea = ({
errorMessage,
...props
}) => {
const { editorRef, setEditorRef } = prepareEditorRef();
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
if (!refReady) { return null; }
return (
<>
<div className="expandable-mce error">

View File

@@ -14,7 +14,7 @@
margin: 16px 40px;
}
}
*[contentEditable="false"] {
outline: 1px solid #D7D3D1;
}

View File

@@ -15,6 +15,15 @@ jest.mock('@tinymce/tinymce-react', () => {
jest.mock('../TinyMceWidget', () => 'TinyMceWidget');
// Mock the TinyMceWidget
jest.mock('../TinyMceWidget/hooks', () => ({
prepareEditorRef: jest.fn(() => ({
editorRef: { current: { value: 'something' } },
refReady: true,
setEditorRef: jest.fn().mockName('hooks.prepareEditorRef.setEditorRef'),
})),
}));
describe('ExpandableTextArea', () => {
const props = {
value: 'text',

View File

@@ -250,7 +250,12 @@ export const setupCustomBehavior = ({
lmsEndpointUrl,
learningContextId,
});
if (newContent) { updateContent(newContent); }
// istanbul ignore if
if (newContent) {
// update content but mark as not dirty as user did not change anything
updateContent(newContent, false);
editor.setDirty(false);
}
});
}
@@ -479,14 +484,21 @@ export const setAssetToStaticUrl = ({ editorValue, lmsEndpointUrl }) => {
content = updatedContent;
});
/* istanbul ignore next */
const updatedStaticUrls = [];
assetSrcs.filter(src => src.startsWith('static/')).forEach(src => {
// Before storing assets we make sure that library static assets points again to
// `/static/dummy.jpg` instead of using the relative url `static/dummy.jpg`
const nameFromEditorSrc = parseAssetName(src);
const portableUrl = `/${ nameFromEditorSrc}`;
const portableUrl = `/${nameFromEditorSrc}`;
if (updatedStaticUrls.includes(portableUrl)) {
// If same image is used multiple times in the same src,
// replace all occurence once and do not process them again.
return;
}
// track updated urls to process only once.
updatedStaticUrls.push(portableUrl);
const currentSrc = src.substring(0, src.search(/("|&quot;)/));
const updatedContent = content.replace(currentSrc, portableUrl);
const updatedContent = content.replaceAll(currentSrc, portableUrl);
content = updatedContent;
});
return content;

View File

@@ -221,6 +221,12 @@ describe('TinyMceEditor hooks', () => {
const content = module.setAssetToStaticUrl({ editorValue, lmsEndpointUrl });
expect(content).toEqual('<img src="/static/soME_ImagE_URl1"/> <a href="/static/soMEImagEURl">testing link</a>');
});
it('returns content with updated static img links', () => {
const editorValue = '<img src="static/goku.img"/> <a href="static/goku.img">testing link</a>';
const lmsEndpointUrl = getConfig().LMS_BASE_URL;
const content = module.setAssetToStaticUrl({ editorValue, lmsEndpointUrl });
expect(content).toEqual('<img src="/static/goku.img"/> <a href="/static/goku.img">testing link</a>');
});
});
describe('editorConfig', () => {

View File

@@ -12,8 +12,9 @@ const pluginConfig = ({ placeholder, editorType, enableImageUpload }) => {
const codeButton = editorType === 'text' ? buttons.code : '';
const labelButton = editorType === 'question' ? buttons.customLabelButton : '';
const quickToolbar = editorType === 'expandable' ? plugins.quickbars : '';
const inline = editorType === 'expandable';
const statusbar = editorType !== 'expandable';
const toolbar = editorType !== 'expandable';
const autoresizeBottomMargin = editorType === 'expandable' ? 10 : 50;
const defaultFormat = (editorType === 'question' || editorType === 'expandable') ? 'div' : 'p';
const hasStudioHeader = document.querySelector('.studio-header');
@@ -90,13 +91,14 @@ const pluginConfig = ({ placeholder, editorType, enableImageUpload }) => {
relative_urls: true,
convert_urls: false,
placeholder,
inline,
statusbar,
block_formats: 'Header 1=h1;Header 2=h2;Header 3=h3;Header 4=h4;Header 5=h5;Header 6=h6;Div=div;Paragraph=p;Preformatted=pre',
forced_root_block: defaultFormat,
powerpaste_allow_local_images: true,
powerpaste_word_import: 'prompt',
powerpaste_html_import: 'prompt',
powerpaste_googledoc_import: 'prompt',
autoresize_bottom_margin: autoresizeBottomMargin,
},
})
);