Commit a750ea92 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '39046-improve-ui-for-broken-images-in-design-management' into 'master'

Display "media broken" icon for broken images in Design Management

See merge request gitlab-org/gitlab!27460
parents 94f2bed8 4c9747b2
<script>
import { throttle } from 'lodash';
import { GlIcon } from '@gitlab/ui';
export default {
components: {
GlIcon,
},
props: {
image: {
type: String,
......@@ -23,6 +27,7 @@ export default {
return {
baseImageSize: null,
imageStyle: null,
imageError: false,
};
},
watch: {
......@@ -49,6 +54,9 @@ export default {
onImgLoad() {
requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
},
onImgError() {
this.imageError = true;
},
setBaseImageSize() {
const { contentImg } = this.$refs;
if (!contentImg || contentImg.offsetHeight === 0 || contentImg.offsetWidth === 0) return;
......@@ -86,13 +94,16 @@ export default {
<template>
<div class="m-auto js-design-image">
<gl-icon v-if="imageError" class="text-secondary-100" name="media-broken" :size="48" />
<img
v-show="!imageError"
ref="contentImg"
class="mh-100"
:src="image"
:alt="name"
:style="imageStyle"
:class="{ 'img-fluid': !imageStyle }"
@error="onImgError"
@load="onImgLoad"
/>
</div>
......
<script>
import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { n__, __ } from '~/locale';
......@@ -9,6 +9,7 @@ export default {
components: {
GlLoadingIcon,
GlIntersectionObserver,
GlIcon,
Icon,
Timeago,
},
......@@ -52,6 +53,7 @@ export default {
data() {
return {
imageLoading: true,
imageError: false,
isInView: false,
};
},
......@@ -81,16 +83,31 @@ export default {
notesLabel() {
return n__('%d comment', '%d comments', this.notesCount);
},
imageLink() {
return this.isInView ? this.imageV432x230 || this.image : '';
},
showLoadingSpinner() {
return this.imageLoading || this.isUploading;
},
imageLink() {
return this.isInView ? this.imageV432x230 || this.image : '';
showImageErrorIcon() {
return this.imageError && this.isInView;
},
showImage() {
return !this.showLoadingSpinner && !this.showImageErrorIcon;
},
},
methods: {
onImageLoad() {
this.imageLoading = false;
this.imageError = false;
},
onImageError() {
this.imageLoading = false;
this.imageError = true;
},
onAppear() {
this.isInView = true;
this.imageLoading = true;
},
},
DESIGN_ROUTE_NAME,
......@@ -112,15 +129,22 @@ export default {
<icon :name="icon.name" :size="18" :class="icon.classes" />
</span>
</div>
<gl-loading-icon v-show="showLoadingSpinner" size="md" />
<gl-intersection-observer @appear="isInView = true">
<gl-intersection-observer @appear="onAppear">
<gl-loading-icon v-if="showLoadingSpinner" size="md" />
<gl-icon
v-else-if="showImageErrorIcon"
name="media-broken"
class="text-secondary"
:size="32"
/>
<img
v-show="!showLoadingSpinner"
v-show="showImage"
:src="imageLink"
:alt="filename"
class="block mx-auto mw-100 mh-100 design-img"
data-qa-selector="design_image"
@load="onImageLoad"
@error="onImageError"
/>
</gl-intersection-observer>
</div>
......
---
title: Show custom 'media broken' icon for broken images in Design Management
merge_request: 27460
author:
type: added
......@@ -4,6 +4,8 @@ exports[`Design management large image component renders image 1`] = `
<div
class="m-auto js-design-image"
>
<!---->
<img
alt="test"
class="mh-100 img-fluid"
......@@ -17,6 +19,8 @@ exports[`Design management large image component renders loading state 1`] = `
class="m-auto js-design-image"
isloading="true"
>
<!---->
<img
alt=""
class="mh-100 img-fluid"
......@@ -25,10 +29,20 @@ exports[`Design management large image component renders loading state 1`] = `
</div>
`;
exports[`Design management large image component renders media broken icon on error 1`] = `
<gl-icon-stub
class="text-secondary-100"
name="media-broken"
size="48"
/>
`;
exports[`Design management large image component sets correct classes and styles if imageStyle is set 1`] = `
<div
class="m-auto js-design-image"
>
<!---->
<img
alt="test"
class="mh-100"
......@@ -42,6 +56,8 @@ exports[`Design management large image component zoom sets image style when zoom
<div
class="m-auto js-design-image"
>
<!---->
<img
alt="test"
class="mh-100"
......
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import DesignImage from 'ee/design_management/components/image.vue';
describe('Design management large image component', () => {
......@@ -52,6 +53,21 @@ describe('Design management large image component', () => {
});
});
it('renders media broken icon on error', () => {
createComponent({
isLoading: false,
image: 'test.jpg',
name: 'test',
});
const image = wrapper.find('img');
image.trigger('error');
return wrapper.vm.$nextTick().then(() => {
expect(image.isVisible()).toBe(false);
expect(wrapper.find(GlIcon).element).toMatchSnapshot();
});
});
describe('zoom', () => {
const baseImageWidth = 100;
const baseImageHeight = 100;
......@@ -75,12 +91,10 @@ describe('Design management large image component', () => {
},
);
jest
.spyOn(wrapper.vm.$refs.contentImg, 'offsetWidth', 'get')
.mockImplementation(() => baseImageWidth);
jest.spyOn(wrapper.vm.$refs.contentImg, 'offsetWidth', 'get').mockReturnValue(baseImageWidth);
jest
.spyOn(wrapper.vm.$refs.contentImg, 'offsetHeight', 'get')
.mockImplementation(() => baseImageHeight);
.mockReturnValue(baseImageHeight);
});
it('emits @resize event on zoom', () => {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management list item component when item appears in view renders media broken icon when image onerror triggered 1`] = `
<gl-icon-stub
class="text-secondary"
name="media-broken"
size="32"
/>
`;
exports[`Design management list item component with no notes renders item with correct status icon for creation event 1`] = `
<router-link-stub
class="card cursor-pointer text-plain js-design-list-item design-list-item"
......@@ -23,16 +31,11 @@ exports[`Design management list item component with no notes renders item with c
</span>
</div>
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub
options="[object Object]"
>
<!---->
<img
alt="test"
class="block mx-auto mw-100 mh-100 design-img"
......@@ -96,16 +99,11 @@ exports[`Design management list item component with no notes renders item with c
</span>
</div>
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub
options="[object Object]"
>
<!---->
<img
alt="test"
class="block mx-auto mw-100 mh-100 design-img"
......@@ -169,16 +167,11 @@ exports[`Design management list item component with no notes renders item with c
</span>
</div>
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub
options="[object Object]"
>
<!---->
<img
alt="test"
class="block mx-auto mw-100 mh-100 design-img"
......@@ -229,16 +222,11 @@ exports[`Design management list item component with no notes renders item with n
>
<!---->
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub
options="[object Object]"
>
<!---->
<img
alt="test"
class="block mx-auto mw-100 mh-100 design-img"
......@@ -289,15 +277,15 @@ exports[`Design management list item component with no notes renders loading spi
>
<!---->
<gl-intersection-observer-stub
options="[object Object]"
>
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
/>
<gl-intersection-observer-stub
options="[object Object]"
>
<img
alt="test"
class="block mx-auto mw-100 mh-100 design-img"
......@@ -349,16 +337,11 @@ exports[`Design management list item component with notes renders item with mult
>
<!---->
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub
options="[object Object]"
>
<!---->
<img
alt="test"
class="block mx-auto mw-100 mh-100 design-img"
......@@ -426,16 +409,11 @@ exports[`Design management list item component with notes renders item with sing
>
<!---->
<gl-loading-icon-stub
color="orange"
label="Loading"
size="md"
style="display: none;"
/>
<gl-intersection-observer-stub
options="[object Object]"
>
<!---->
<img
alt="test"
class="block mx-auto mw-100 mh-100 design-img"
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import VueRouter from 'vue-router';
import Item from 'ee/design_management/components/list/item.vue';
import { GlIntersectionObserver } from '@gitlab/ui';
const localVue = createLocalVue();
localVue.use(VueRouter);
......@@ -59,25 +59,36 @@ describe('Design management list item component', () => {
});
describe('when item appears in view', () => {
let image;
beforeEach(() => {
createComponent();
image = wrapper.find('img');
expect(image.attributes('src')).toBe('');
wrapper.find(GlIntersectionObserver).vm.$emit('appear');
return wrapper.vm.$nextTick();
});
it('renders an image', () => {
const image = wrapper.find('img');
expect(image.attributes('src')).toBe('http://via.placeholder.com/300');
});
it('renders media broken icon when image onerror triggered', () => {
image.trigger('error');
return wrapper.vm.$nextTick().then(() => {
expect(image.isVisible()).toBe(false);
expect(wrapper.find(GlIcon).element).toMatchSnapshot();
});
});
describe('when imageV432x230 and image provided', () => {
it('renders imageV432x230 image', () => {
const mockSrc = 'mock-imageV432x230-url';
wrapper.setProps({ imageV432x230: mockSrc });
return wrapper.vm.$nextTick().then(() => {
const image = wrapper.find('img');
expect(image.attributes('src')).toBe(mockSrc);
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment