Commit 7d4f1cb7 authored by Tom Quirk's avatar Tom Quirk Committed by Kushal Pandya

Implement design_presentation component

This component contains all logic related to the presentation
of a design, with comment pins overlayed, to the user.
parent c72565ae
...@@ -7,6 +7,10 @@ export default { ...@@ -7,6 +7,10 @@ export default {
Icon, Icon,
}, },
props: { props: {
dimensions: {
type: Object,
required: true,
},
position: { position: {
type: Object, type: Object,
required: true, required: true,
...@@ -23,12 +27,11 @@ export default { ...@@ -23,12 +27,11 @@ export default {
}, },
}, },
computed: { computed: {
overlayDimensions() { overlayStyle() {
return { return {
width: `${this.position.width}px`, width: `${this.dimensions.width}px`,
height: `${this.position.height}px`, height: `${this.dimensions.height}px`,
left: `calc(50% - ${this.position.width / 2}px)`, ...this.position,
top: `calc(50% - ${this.position.height / 2}px)`,
}; };
}, },
}, },
...@@ -38,8 +41,8 @@ export default { ...@@ -38,8 +41,8 @@ export default {
}, },
getNotePosition(data) { getNotePosition(data) {
const { x, y, width, height } = data; const { x, y, width, height } = data;
const widthRatio = this.position.width / width; const widthRatio = this.dimensions.width / width;
const heightRatio = this.position.height / height; const heightRatio = this.dimensions.height / height;
return { return {
left: `${Math.round(x * widthRatio)}px`, left: `${Math.round(x * widthRatio)}px`,
top: `${Math.round(y * heightRatio)}px`, top: `${Math.round(y * heightRatio)}px`,
...@@ -50,7 +53,7 @@ export default { ...@@ -50,7 +53,7 @@ export default {
</script> </script>
<template> <template>
<div class="position-absolute image-diff-overlay frame" :style="overlayDimensions"> <div class="position-absolute image-diff-overlay frame" :style="overlayStyle">
<button <button
type="button" type="button"
class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button" class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
......
<script>
import DesignImage from './image.vue';
import DesignOverlay from './design_overlay.vue';
export default {
components: {
DesignImage,
DesignOverlay,
},
props: {
image: {
type: String,
required: false,
default: '',
},
imageName: {
type: String,
required: false,
default: '',
},
discussions: {
type: Array,
required: true,
},
isAnnotating: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
overlayDimensions: null,
overlayPosition: null,
currentAnnotationCoordinates: null,
};
},
computed: {
discussionStartingNotes() {
return this.discussions.map(discussion => discussion.notes[0]);
},
currentCommentForm() {
return (this.isAnnotating && this.currentAnnotationCoordinates) || null;
},
},
methods: {
setOverlayDimensions(overlayDimensions) {
this.overlayDimensions = overlayDimensions;
},
setOverlayPosition() {
if (!this.overlayDimensions) {
this.overlayPosition = {};
}
const { presentationViewport } = this.$refs;
if (!presentationViewport) return;
// default to center
this.overlayPosition = {
left: `calc(50% - ${this.overlayDimensions.width / 2}px)`,
top: `calc(50% - ${this.overlayDimensions.height / 2}px)`,
};
// if the overlay overflows, then don't center
if (this.overlayDimensions.width > presentationViewport.offsetWidth) {
this.overlayPosition.left = '0';
}
if (this.overlayDimensions.height > presentationViewport.offsetHeight) {
this.overlayPosition.top = '0';
}
},
onImageResize(imageDimensions) {
this.setOverlayDimensions(imageDimensions);
this.setOverlayPosition();
},
openCommentForm(position) {
const { x, y } = position;
const { width, height } = this.overlayDimensions;
this.currentAnnotationCoordinates = {
x,
y,
width,
height,
};
this.$emit('openCommentForm', this.currentAnnotationCoordinates);
},
},
};
</script>
<template>
<div ref="presentationViewport" class="d-flex flex-column h-100 mh-100 position-relative">
<design-image v-if="image" :image="image" :name="imageName" @resize="onImageResize" />
<design-overlay
v-if="overlayDimensions && overlayPosition"
:dimensions="overlayDimensions"
:position="overlayPosition"
:notes="discussionStartingNotes"
:current-comment-form="currentCommentForm"
@openCommentForm="openCommentForm"
/>
</div>
</template>
...@@ -44,7 +44,7 @@ export default { ...@@ -44,7 +44,7 @@ export default {
height: contentImg.clientHeight, height: contentImg.clientHeight,
}; };
this.$emit('setOverlayDimensions', position); this.$emit('resize', position);
}); });
}, },
}, },
......
...@@ -5,12 +5,11 @@ import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; ...@@ -5,12 +5,11 @@ import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import createFlash from '~/flash'; import createFlash from '~/flash';
import allVersionsMixin from '../../mixins/all_versions'; import allVersionsMixin from '../../mixins/all_versions';
import Toolbar from '../../components/toolbar/index.vue'; import Toolbar from '../../components/toolbar/index.vue';
import DesignImage from '../../components/image.vue';
import DesignOverlay from '../../components/design_overlay.vue';
import DesignDiscussion from '../../components/design_notes/design_discussion.vue'; import DesignDiscussion from '../../components/design_notes/design_discussion.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue'; import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignDestroyer from '../../components/design_destroyer.vue'; import DesignDestroyer from '../../components/design_destroyer.vue';
import Participants from '~/sidebar/components/participants/participants.vue'; import Participants from '~/sidebar/components/participants/participants.vue';
import DesignPresentation from '../../components/design_presentation.vue';
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql'; import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
import appDataQuery from '../../graphql/queries/appData.query.graphql'; import appDataQuery from '../../graphql/queries/appData.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql'; import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql';
...@@ -30,8 +29,7 @@ import { ...@@ -30,8 +29,7 @@ import {
export default { export default {
components: { components: {
ApolloMutation, ApolloMutation,
DesignImage, DesignPresentation,
DesignOverlay,
DesignDiscussion, DesignDiscussion,
DesignDestroyer, DesignDestroyer,
Toolbar, Toolbar,
...@@ -52,10 +50,6 @@ export default { ...@@ -52,10 +50,6 @@ export default {
design: {}, design: {},
comment: '', comment: '',
annotationCoordinates: null, annotationCoordinates: null,
overlayDimensions: {
width: 0,
height: 0,
},
projectPath: '', projectPath: '',
errorMessage: '', errorMessage: '',
issueIid: '', issueIid: '',
...@@ -97,9 +91,6 @@ export default { ...@@ -97,9 +91,6 @@ export default {
discussions() { discussions() {
return extractDiscussions(this.design.discussions); return extractDiscussions(this.design.discussions);
}, },
discussionStartingNotes() {
return this.discussions.map(discussion => discussion.notes[0]);
},
discussionParticipants() { discussionParticipants() {
return extractParticipants(this.design.issue.participants); return extractParticipants(this.design.issue.participants);
}, },
...@@ -145,6 +136,9 @@ export default { ...@@ -145,6 +136,9 @@ export default {
webPath: this.design.issue.webPath.substr(1), webPath: this.design.issue.webPath.substr(1),
}; };
}, },
isAnnotating() {
return Boolean(this.annotationCoordinates);
},
}, },
mounted() { mounted() {
Mousetrap.bind('esc', this.closeDesign); Mousetrap.bind('esc', this.closeDesign);
...@@ -180,25 +174,13 @@ export default { ...@@ -180,25 +174,13 @@ export default {
this.errorMessage = designDeletionError({ singular: true }); this.errorMessage = designDeletionError({ singular: true });
throw e; throw e;
}, },
openCommentForm(position) { openCommentForm(annotationCoordinates) {
const { x, y } = position; this.annotationCoordinates = annotationCoordinates;
const { width, height } = this.overlayDimensions;
this.annotationCoordinates = {
...this.annotationCoordinates,
x,
y,
width,
height,
};
}, },
closeCommentForm() { closeCommentForm() {
this.comment = ''; this.comment = '';
this.annotationCoordinates = null; this.annotationCoordinates = null;
}, },
setOverlayDimensions(position) {
this.overlayDimensions.width = position.width;
this.overlayDimensions.height = position.height;
},
closeDesign() { closeDesign() {
this.$router.push({ this.$router.push({
name: 'designs', name: 'designs',
...@@ -245,19 +227,13 @@ export default { ...@@ -245,19 +227,13 @@ export default {
</gl-alert> </gl-alert>
</div> </div>
<div class="d-flex flex-column h-100 mh-100 position-relative"> <design-presentation
<design-image :image="design.image"
:image="design.image" :image-name="design.filename"
:name="design.filename" :discussions="discussions"
@setOverlayDimensions="setOverlayDimensions" :is-annotating="isAnnotating"
/> @openCommentForm="openCommentForm"
<design-overlay />
:position="overlayDimensions"
:notes="discussionStartingNotes"
:current-comment-form="annotationCoordinates"
@openCommentForm="openCommentForm"
/>
</div>
</div> </div>
<div class="image-notes"> <div class="image-notes">
<h2 class="gl-font-size-20 font-weight-bold mt-0">{{ issue.title }}</h2> <h2 class="gl-font-size-20 font-weight-bold mt-0">{{ issue.title }}</h2>
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
object-fit: contain; object-fit: contain;
} }
.design-presentation-wrapper {
top: 0;
left: 0;
}
.design-list-item .design-event { .design-list-item .design-event {
top: $gl-padding; top: $gl-padding;
right: $gl-padding; right: $gl-padding;
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management design presentation component renders empty state when no image provided 1`] = `
<div
class="d-flex flex-column h-100 mh-100 position-relative"
>
<!---->
<!---->
</div>
`;
exports[`Design management design presentation component renders image and overlay when image provided 1`] = `
<div
class="d-flex flex-column h-100 mh-100 position-relative"
>
<design-image-stub
image="test.jpg"
name="test"
/>
<design-overlay-stub
dimensions="[object Object]"
notes=""
position="[object Object]"
/>
</div>
`;
...@@ -31,10 +31,14 @@ describe('Design overlay component', () => { ...@@ -31,10 +31,14 @@ describe('Design overlay component', () => {
function createComponent(props = {}) { function createComponent(props = {}) {
wrapper = shallowMount(DesignOverlay, { wrapper = shallowMount(DesignOverlay, {
propsData: { propsData: {
position: { dimensions: {
width: 100, width: 100,
height: 100, height: 100,
}, },
position: {
top: '0',
left: '0',
},
...props, ...props,
}, },
}); });
...@@ -44,7 +48,7 @@ describe('Design overlay component', () => { ...@@ -44,7 +48,7 @@ describe('Design overlay component', () => {
createComponent(); createComponent();
expect(wrapper.find('.image-diff-overlay').attributes().style).toBe( expect(wrapper.find('.image-diff-overlay').attributes().style).toBe(
'width: 100px; height: 100px;', 'width: 100px; height: 100px; top: 0px; left: 0px;',
); );
}); });
...@@ -92,7 +96,7 @@ describe('Design overlay component', () => { ...@@ -92,7 +96,7 @@ describe('Design overlay component', () => {
it('should recalculate badges positions on window resize', () => { it('should recalculate badges positions on window resize', () => {
createComponent({ createComponent({
notes, notes,
position: { dimensions: {
width: 400, width: 400,
height: 400, height: 400,
}, },
...@@ -101,7 +105,7 @@ describe('Design overlay component', () => { ...@@ -101,7 +105,7 @@ describe('Design overlay component', () => {
expect(findFirstBadge().attributes().style).toBe('left: 40px; top: 60px;'); expect(findFirstBadge().attributes().style).toBe('left: 40px; top: 60px;');
wrapper.setProps({ wrapper.setProps({
position: { dimensions: {
width: 200, width: 200,
height: 200, height: 200,
}, },
......
import { shallowMount } from '@vue/test-utils';
import DesignPresentation from 'ee/design_management/components/design_presentation.vue';
const mockOverlayData = {
overlayDimensions: {
width: 100,
height: 100,
},
overlayPosition: {
top: '0',
left: '0',
},
};
describe('Design management design presentation component', () => {
let wrapper;
function createComponent(
{ image, imageName, discussions = [], isAnnotating = false } = {},
data = {},
) {
wrapper = shallowMount(DesignPresentation, {
propsData: {
image,
imageName,
discussions,
isAnnotating,
},
});
wrapper.setData(data);
}
afterEach(() => {
wrapper.destroy();
});
it('renders image and overlay when image provided', () => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
},
mockOverlayData,
);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
});
it('renders empty state when no image provided', () => {
createComponent();
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
});
it('openCommentForm event emits correct data', () => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
},
mockOverlayData,
);
wrapper.vm.openCommentForm({ x: 1, y: 1 });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('openCommentForm')).toEqual([
[{ ...mockOverlayData.overlayDimensions, x: 1, y: 1 }],
]);
});
});
it('currentCommentForm is null when isAnnotating is false', () => {
createComponent({
image: 'test.jpg',
imageName: 'test',
});
expect(wrapper.vm.currentCommentForm).toBeNull();
});
it('currentCommentForm is null when isAnnotating is true but annotation coordinates are falsey', () => {
createComponent({
image: 'test.jpg',
imageName: 'test',
isAnnotating: true,
});
expect(wrapper.vm.currentCommentForm).toBeNull();
});
it('currentCommentForm is equal to current annotation coordinates when isAnnotating is true', () => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
isAnnotating: true,
},
{
currentAnnotationCoordinates: {
x: 1,
y: 1,
width: 100,
height: 100,
},
},
);
expect(wrapper.vm.currentCommentForm).toEqual({
x: 1,
y: 1,
width: 100,
height: 100,
});
});
describe('setOverlayPosition', () => {
beforeEach(() => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
},
mockOverlayData,
);
});
afterEach(() => {
jest.clearAllMocks();
});
it('sets overlay position correctly when overlay is smaller than viewport', () => {
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);
wrapper.vm.setOverlayPosition();
expect(wrapper.vm.overlayPosition).toEqual({
left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`,
top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`,
});
});
it('sets overlay position correctly when overlay width is larger than viewports', () => {
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(50);
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);
wrapper.vm.setOverlayPosition();
expect(wrapper.vm.overlayPosition).toEqual({
left: '0',
top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`,
});
});
it('sets overlay position correctly when overlay height is larger than viewports', () => {
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(50);
wrapper.vm.setOverlayPosition();
expect(wrapper.vm.overlayPosition).toEqual({
left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`,
top: '0',
});
});
});
});
...@@ -15,19 +15,11 @@ exports[`Design management design index page renders design index 1`] = ` ...@@ -15,19 +15,11 @@ exports[`Design management design index page renders design index 1`] = `
<!----> <!---->
<div <design-presentation-stub
class="d-flex flex-column h-100 mh-100 position-relative" discussions="[object Object]"
> image="test.jpg"
<design-image-stub imagename="test.jpg"
image="test.jpg" />
name="test.jpg"
/>
<design-overlay-stub
notes="[object Object]"
position="[object Object]"
/>
</div>
</div> </div>
<div <div
...@@ -110,19 +102,11 @@ exports[`Design management design index page with error GlAlert is rendered in c ...@@ -110,19 +102,11 @@ exports[`Design management design index page with error GlAlert is rendered in c
</gl-alert-stub> </gl-alert-stub>
</div> </div>
<div <design-presentation-stub
class="d-flex flex-column h-100 mh-100 position-relative" discussions=""
> image="test.jpg"
<design-image-stub imagename="test.jpg"
image="test.jpg" />
name="test.jpg"
/>
<design-overlay-stub
notes=""
position="[object Object]"
/>
</div>
</div> </div>
<div <div
......
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