diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 5be4c50d05..bacd4ffc42 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -272,6 +272,7 @@
"clear_search": "",
"clearing": "",
"click_here_to_view_sl_in_lng": "",
+ "click_recompile_and_check_your_pdf_to_see_how_its_looking": "",
"click_to_unpause": "",
"clicking_delete_will_remove_sso_config_and_clear_saml_data": "",
"clone_with_git": "",
@@ -1837,6 +1838,7 @@
"the_primary_file_for_compiling_your_project": "",
"the_target_folder_could_not_be_found": "",
"the_version_of_tex_live_used_for_compiling": "",
+ "the_visual_editor_cant_preview_this_type_of_image_file": "",
"the_width_you_choose_here_is_based_on_the_width_of_the_text_in_your_document": "",
"their_projects_will_be_transferred_to_another_user": "",
"then_x_price_per_month": "",
diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts
index 255acae0a6..2832cc042a 100644
--- a/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts
@@ -26,6 +26,15 @@ export const visualHighlightStyle = syntaxHighlighting(
])
)
+const graphicsErrorColorTheme = EditorView.baseTheme({
+ '&dark .ol-cm-graphics-loading-error': {
+ '--graphics-loading-error-color': 'var(--content-placeholder-dark)',
+ },
+ '&light .ol-cm-graphics-loading-error': {
+ '--graphics-loading-error-color': 'var(--content-placeholder)',
+ },
+})
+
const mainVisualTheme = EditorView.theme({
'&.cm-editor': {
'--visual-font-family':
@@ -351,6 +360,30 @@ const mainVisualTheme = EditorView.theme({
border: '1px solid red',
padding: '8px',
},
+ '.ol-cm-graphics-loading-error': {
+ color: 'var(--graphics-loading-error-color)',
+ minHeight: '300px',
+ textAlign: 'left',
+ fontFamily: 'var(--font-sans)',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ gap: 'var(--spacing-03)',
+ padding: '0 var(--spacing-10)',
+ maxWidth: '500px',
+ margin: '0 auto',
+ },
+ '.ol-cm-graphics-loading-error-title, .ol-cm-graphics-loading-error-subtitle':
+ {
+ display: 'block',
+ },
+ '.ol-cm-graphics-loading-error-title': {
+ fontSize: 'var(--font-size-04)',
+ fontWeight: '600',
+ },
+ '.ol-cm-graphics-loading-error-subtitle': {
+ fontSize: 'var(--font-size-02)',
+ },
'.ol-cm-environment-centered .ol-cm-graphics': {
margin: '0 auto',
},
@@ -491,5 +524,6 @@ const contentWidthSetter = EditorView.updateListener.of(update => {
export const visualTheme: Extension = [
contentWidthThemeConf.of(createContentWidthTheme('100%')),
mainVisualTheme,
+ graphicsErrorColorTheme,
contentWidthSetter,
]
diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/graphics.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/graphics.ts
index 2480f2235c..7c6ce50f2d 100644
--- a/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/graphics.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/graphics.ts
@@ -122,6 +122,7 @@ export class GraphicsWidget extends WidgetType {
}
createImage(view: EditorView, url: string) {
+ const wrapper = document.createElement('div')
const image = document.createElement('img')
image.classList.add('ol-cm-graphics')
image.classList.add('ol-cm-graphics-loading')
@@ -135,8 +136,33 @@ export class GraphicsWidget extends WidgetType {
this.height = image.height // for estimatedHeight
view.requestMeasure()
})
+ image.addEventListener('error', () => {
+ const errorElement = this.createErrorElement(view)
+ wrapper.replaceChildren(errorElement)
+ this.height = wrapper.clientHeight
+ view.requestMeasure()
+ })
- return image
+ wrapper.appendChild(image)
+ return wrapper
+ }
+
+ createErrorElement(view: EditorView): HTMLElement {
+ const wrapper = document.createElement('div')
+ wrapper.classList.add('ol-cm-graphics-loading-error')
+ const title = document.createElement('span')
+ title.classList.add('ol-cm-graphics-loading-error-title')
+ title.textContent = view.state.phrase(
+ 'the_visual_editor_cant_preview_this_type_of_image_file'
+ )
+ const subtitle = document.createElement('span')
+ subtitle.classList.add('ol-cm-graphics-loading-error-subtitle')
+ subtitle.textContent = view.state.phrase(
+ 'click_recompile_and_check_your_pdf_to_see_how_its_looking'
+ )
+ wrapper.appendChild(title)
+ wrapper.appendChild(subtitle)
+ return wrapper
}
async renderPDF(view: EditorView, canvas: HTMLCanvasElement, url: string) {
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index b7b2617edb..dc4c9e4657 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -349,6 +349,7 @@
"clearing": "Clearing",
"click_here_to_view_sl_in_lng": "Click here to use __appName__ in <0>__lngName__0>",
"click_link_to_proceed": "Click __clickText__ below to proceed.",
+ "click_recompile_and_check_your_pdf_to_see_how_its_looking": "Click Recompile and check your PDF to see how it’s looking.",
"click_to_unpause": "Click to unpause and reactivate your Overleaf premium features.",
"clicking_delete_will_remove_sso_config_and_clear_saml_data": "Clicking <0>Delete0> will remove your SSO configuration and unlink all users. You can only do this when SSO is disabled in your group settings.",
"clone_with_git": "Clone with Git",
@@ -2353,6 +2354,7 @@
"the_supplied_uri_is_invalid": "The link to open this content on Overleaf included an invalid URI. If this keeps happening for links on a particular site, please report this to them.",
"the_target_folder_could_not_be_found": "The target folder could not be found.",
"the_version_of_tex_live_used_for_compiling": "The version of TeX Live used for compiling",
+ "the_visual_editor_cant_preview_this_type_of_image_file": "The Visual Editor can’t preview this type of image file.",
"the_width_you_choose_here_is_based_on_the_width_of_the_text_in_your_document": "The width you choose here is based on the width of the text in your document. Alternatively, you can customize the image size directly in the LaTeX code.",
"their_projects_will_be_transferred_to_another_user": "Their projects will all be transferred to another user of your choice",
"theme": "Theme",
diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-figure-modal.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-figure-modal.spec.tsx
index 696b6fd1d8..5a98ecb671 100644
--- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-figure-modal.spec.tsx
+++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-figure-modal.spec.tsx
@@ -11,6 +11,7 @@ import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-pat
import { TestContainer } from '../helpers/test-container'
import getMeta from '@/utils/meta'
import { mockProject } from '../helpers/mock-project'
+import { base64image } from '../fixtures/image'
const clickToolbarButton = (text: string) => {
cy.findByLabelText(text).click()
@@ -64,7 +65,7 @@ describe('', function () {
previewByPath: cy
.stub()
.as('previewByPath')
- .returns({ url: 'frog.jpg', extension: 'jpg' }),
+ .returns({ url: base64image, extension: 'png' }),
}}
>
{children}
@@ -631,20 +632,22 @@ text below`,
})
it('Opens figure modal on pasting image', function () {
- cy.fixture('images/gradient.png').then(gradientBuffer => {
- const gradientFile = new File([gradientBuffer], 'gradient.png', {
- type: 'image/png',
- })
- const clipboardData = new DataTransfer()
- clipboardData.items.add(gradientFile)
- cy.wrap(clipboardData.files).should('have.length', 1)
- cy.get('.cm-content').trigger('paste', { clipboardData })
- cy.findByText('Upload from computer').should('be.visible')
- cy.findByLabelText('File name in this project').should(
- 'have.value',
- 'gradient.png'
- )
- })
+ cy.fixture>('images/gradient.png').then(
+ gradientBuffer => {
+ const gradientFile = new File([gradientBuffer], 'gradient.png', {
+ type: 'image/png',
+ })
+ const clipboardData = new DataTransfer()
+ clipboardData.items.add(gradientFile)
+ cy.wrap(clipboardData.files).should('have.length', 1)
+ cy.get('.cm-content').trigger('paste', { clipboardData })
+ cy.findByText('Upload from computer').should('be.visible')
+ cy.findByLabelText('File name in this project').should(
+ 'have.value',
+ 'gradient.png'
+ )
+ }
+ )
})
// TODO: Add tests for replacing image when we can match on image path
diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx
index 45f68f359b..f6f6cd6635 100644
--- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx
+++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx
@@ -10,6 +10,7 @@ import { mockScope } from '../helpers/mock-scope'
import forEach from 'mocha-each'
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { TestContainer } from '../helpers/test-container'
+import { base64image } from '../fixtures/image'
describe(' in Visual mode', function () {
beforeEach(function () {
@@ -34,7 +35,16 @@ describe(' in Visual mode', function () {
previewByPath: cy
.stub()
.as('previewByPath')
- .callsFake(path => ({ url: path, extension: 'png' })),
+ .callsFake(path =>
+ path === 'valid.png'
+ ? { url: base64image, extension: 'png' }
+ : path === 'graphic.eps'
+ ? {
+ url: 'data:application/postscript,0 0 moveto (hello) show',
+ extension: 'eps',
+ }
+ : null
+ ),
}}
>
{children}
@@ -256,11 +266,11 @@ describe(' in Visual mode', function () {
})
it('loads figures', function () {
- cy.get('@third-line').type('path/to/image')
+ cy.get('@third-line').type('valid.png')
cy.get('@third-line').should(
'contain.text',
- ' \\includegraphics[width=0.5\\linewidth]{path/to/image}'
+ ' \\includegraphics[width=0.5\\linewidth]{valid.png}'
)
// move the cursor out of the figure
@@ -269,10 +279,56 @@ describe(' in Visual mode', function () {
// Should be removed from dom when line is hidden
cy.get('.cm-content').should(
'not.contain.text',
- '\\includegraphics[width=0.5\\linewidth]{path/to/image}'
+ '\\includegraphics[width=0.5\\linewidth]{valid.png}'
)
- cy.get('img.ol-cm-graphics').should('have.attr', 'src', 'path/to/image')
+ cy.get('img.ol-cm-graphics').should('have.attr', 'src', base64image)
+ })
+
+ it('shows error state for figures we cannot render', function () {
+ cy.get('@third-line').type('graphic.eps')
+
+ cy.get('@third-line').should(
+ 'contain.text',
+ ' \\includegraphics[width=0.5\\linewidth]{graphic.eps}'
+ )
+
+ // move the cursor out of the figure
+ cy.get('@third-line').type('{DownArrow}{DownArrow}{DownArrow}{DownArrow}')
+
+ // Should be removed from dom when line is hidden
+ cy.get('.cm-content').should(
+ 'not.contain.text',
+ '\\includegraphics[width=0.5\\linewidth]{graphic.eps}'
+ )
+
+ // Should show error state
+ cy.get('div.ol-cm-graphics-loading-error').should('be.visible')
+ cy.findByText(
+ /The Visual Editor can’t preview this type of image file./
+ ).should('be.visible')
+ })
+
+ it('shows error state for missing figures', function () {
+ cy.get('@third-line').type('missing.png')
+
+ cy.get('@third-line').should(
+ 'contain.text',
+ ' \\includegraphics[width=0.5\\linewidth]{missing.png}'
+ )
+
+ // move the cursor out of the figure
+ cy.get('@third-line').type('{DownArrow}{DownArrow}{DownArrow}{DownArrow}')
+
+ // Should be removed from dom when line is hidden
+ cy.get('.cm-content').should(
+ 'not.contain.text',
+ '\\includegraphics[width=0.5\\linewidth]{missing.png}'
+ )
+
+ // Should show error state
+ cy.get('div.ol-cm-graphics-error').should('be.visible')
+ cy.findByText(/missing.png/).should('be.visible')
})
it('marks lines as figure environments', function () {
diff --git a/services/web/test/frontend/features/source-editor/fixtures/image.ts b/services/web/test/frontend/features/source-editor/fixtures/image.ts
new file mode 100644
index 0000000000..27fc9f09db
--- /dev/null
+++ b/services/web/test/frontend/features/source-editor/fixtures/image.ts
@@ -0,0 +1,2 @@
+export const base64image =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpaJVETuIOGSogmBBVMRRq1CECqFWaNXB5NIvaNKQpLg4Cq4FBz8Wqw4uzro6uAqC4AeIq4uToouU+L+k0CLWg+N+vLv3uHsHCNUi06y2cUDTbTMRi4qp9KoYeIWAPnShB6Mys4w5SYqj5fi6h4+vdxGe1frcn6NbzVgM8InEs8wwbeIN4ulN2+C8TxxieVklPiceM+mCxI9cVzx+45xzWeCZITOZmCcOEYu5JlaamOVNjXiKOKxqOuULKY9VzluctWKZ1e/JXxjM6CvLXKc5hBgWsQQJIhSUUUARNiK06qRYSNB+tIV/0PVL5FLIVQAjxwJK0CC7fvA/+N2tlZ2c8JKCUaD9xXE+hoHALlCrOM73sePUTgD/M3ClN/ylKjDzSXqloYWPgN5t4OK6oSl7wOUOMPBkyKbsSn6aQjYLvJ/RN6WB/lugc83rrb6P0wcgSV3Fb4CDQ2AkR9nrLd7d0dzbv2fq/f0ARfNylZJUgMQAAAAGYktHRABuAP8AAGHZRr4AAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnAhELEhgyPeVkAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAyVJREFUeNrt1rEJgDAURVGVNCmS2hS6PziCteIYWjuEbiEfOWeEV1xe35bt6QhjnlYjBJLzbYRABhMAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBbwX6m13QqB5HwbIZBSLyN4WACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBfCFVMtphUBKvYwQSBsPI3hYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIF8IUXjtUMuBMh1xAAAAAASUVORK5CYII='