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 {
Icon,
},
props: {
dimensions: {
type: Object,
required: true,
},
position: {
type: Object,
required: true,
......@@ -23,12 +27,11 @@ export default {
},
},
computed: {
overlayDimensions() {
overlayStyle() {
return {
width: `${this.position.width}px`,
height: `${this.position.height}px`,
left: `calc(50% - ${this.position.width / 2}px)`,
top: `calc(50% - ${this.position.height / 2}px)`,
width: `${this.dimensions.width}px`,
height: `${this.dimensions.height}px`,
...this.position,
};
},
},
......@@ -38,8 +41,8 @@ export default {
},
getNotePosition(data) {
const { x, y, width, height } = data;
const widthRatio = this.position.width / width;
const heightRatio = this.position.height / height;
const widthRatio = this.dimensions.width / width;
const heightRatio = this.dimensions.height / height;
return {
left: `${Math.round(x * widthRatio)}px`,
top: `${Math.round(y * heightRatio)}px`,
......@@ -50,7 +53,7 @@ export default {
</script>
<template>
<div class="position-absolute image-diff-overlay frame" :style="overlayDimensions">
<div class="position-absolute image-diff-overlay frame" :style="overlayStyle">
<button
type="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 {
height: contentImg.clientHeight,
};
this.$emit('setOverlayDimensions', position);
this.$emit('resize', position);
});
},
},
......
......@@ -5,12 +5,11 @@ import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import createFlash from '~/flash';
import allVersionsMixin from '../../mixins/all_versions';
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 DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignDestroyer from '../../components/design_destroyer.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 appDataQuery from '../../graphql/queries/appData.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql';
......@@ -30,8 +29,7 @@ import {
export default {
components: {
ApolloMutation,
DesignImage,
DesignOverlay,
DesignPresentation,
DesignDiscussion,
DesignDestroyer,
Toolbar,
......@@ -52,10 +50,6 @@ export default {
design: {},
comment: '',
annotationCoordinates: null,
overlayDimensions: {
width: 0,
height: 0,
},
projectPath: '',
errorMessage: '',
issueIid: '',
......@@ -97,9 +91,6 @@ export default {
discussions() {
return extractDiscussions(this.design.discussions);
},
discussionStartingNotes() {
return this.discussions.map(discussion => discussion.notes[0]);
},
discussionParticipants() {
return extractParticipants(this.design.issue.participants);
},
......@@ -145,6 +136,9 @@ export default {
webPath: this.design.issue.webPath.substr(1),
};
},
isAnnotating() {
return Boolean(this.annotationCoordinates);
},
},
mounted() {
Mousetrap.bind('esc', this.closeDesign);
......@@ -180,25 +174,13 @@ export default {
this.errorMessage = designDeletionError({ singular: true });
throw e;
},
openCommentForm(position) {
const { x, y } = position;
const { width, height } = this.overlayDimensions;
this.annotationCoordinates = {
...this.annotationCoordinates,
x,
y,
width,
height,
};
openCommentForm(annotationCoordinates) {
this.annotationCoordinates = annotationCoordinates;
},
closeCommentForm() {
this.comment = '';
this.annotationCoordinates = null;
},
setOverlayDimensions(position) {
this.overlayDimensions.width = position.width;
this.overlayDimensions.height = position.height;
},
closeDesign() {
this.$router.push({
name: 'designs',
......@@ -245,20 +227,14 @@ export default {
</gl-alert>
</div>
<div class="d-flex flex-column h-100 mh-100 position-relative">
<design-image
<design-presentation
:image="design.image"
:name="design.filename"
@setOverlayDimensions="setOverlayDimensions"
/>
<design-overlay
:position="overlayDimensions"
:notes="discussionStartingNotes"
:current-comment-form="annotationCoordinates"
:image-name="design.filename"
:discussions="discussions"
:is-annotating="isAnnotating"
@openCommentForm="openCommentForm"
/>
</div>
</div>
<div class="image-notes">
<h2 class="gl-font-size-20 font-weight-bold mt-0">{{ issue.title }}</h2>
<a class="text-tertiary text-decoration-none mb-3 d-block" :href="issue.webUrl">{{
......
......@@ -2,6 +2,11 @@
object-fit: contain;
}
.design-presentation-wrapper {
top: 0;
left: 0;
}
.design-list-item .design-event {
top: $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', () => {
function createComponent(props = {}) {
wrapper = shallowMount(DesignOverlay, {
propsData: {
position: {
dimensions: {
width: 100,
height: 100,
},
position: {
top: '0',
left: '0',
},
...props,
},
});
......@@ -44,7 +48,7 @@ describe('Design overlay component', () => {
createComponent();
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', () => {
it('should recalculate badges positions on window resize', () => {
createComponent({
notes,
position: {
dimensions: {
width: 400,
height: 400,
},
......@@ -101,7 +105,7 @@ describe('Design overlay component', () => {
expect(findFirstBadge().attributes().style).toBe('left: 40px; top: 60px;');
wrapper.setProps({
position: {
dimensions: {
width: 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`] = `
<!---->
<div
class="d-flex flex-column h-100 mh-100 position-relative"
>
<design-image-stub
<design-presentation-stub
discussions="[object Object]"
image="test.jpg"
name="test.jpg"
imagename="test.jpg"
/>
<design-overlay-stub
notes="[object Object]"
position="[object Object]"
/>
</div>
</div>
<div
......@@ -110,19 +102,11 @@ exports[`Design management design index page with error GlAlert is rendered in c
</gl-alert-stub>
</div>
<div
class="d-flex flex-column h-100 mh-100 position-relative"
>
<design-image-stub
<design-presentation-stub
discussions=""
image="test.jpg"
name="test.jpg"
imagename="test.jpg"
/>
<design-overlay-stub
notes=""
position="[object Object]"
/>
</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