Commit c8e2aba1 authored by Phil Hughes's avatar Phil Hughes

Changes image diff comments to use percents

This changes the positioning of the image diff comments
from absolute x & y positions to instead use percents.
These percents then mean that the comment bubble can
correctly adjust depending on the screen size.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/225961
parent 26b1c5d1
...@@ -173,12 +173,16 @@ export default { ...@@ -173,12 +173,16 @@ export default {
:a-mode="diffFile.a_mode" :a-mode="diffFile.a_mode"
:b-mode="diffFile.b_mode" :b-mode="diffFile.b_mode"
> >
<template #image-overlay="{ renderedWidth, renderedHeight }">
<image-diff-overlay <image-diff-overlay
slot="image-overlay" v-if="renderedWidth"
:rendered-width="renderedWidth"
:rendered-height="renderedHeight"
:discussions="imageDiscussions" :discussions="imageDiscussions"
:file-hash="diffFileHash" :file-hash="diffFileHash"
:can-comment="getNoteableData.current_user.can_create_note && !diffFile.brokenSymlink" :can-comment="getNoteableData.current_user.can_create_note && !diffFile.brokenSymlink"
/> />
</template>
<div v-if="showNotesContainer" class="note-container"> <div v-if="showNotesContainer" class="note-container">
<user-avatar-link <user-avatar-link
v-if="diffFileCommentForm && author" v-if="diffFileCommentForm && author"
......
...@@ -4,6 +4,10 @@ import { isArray } from 'lodash'; ...@@ -4,6 +4,10 @@ import { isArray } from 'lodash';
import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff'; import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff';
import { GlIcon } from '@gitlab/ui'; import { GlIcon } from '@gitlab/ui';
function calcPercent(pos, size, renderedSize) {
return (((pos / size) * 100) / ((renderedSize / size) * 100)) * 100;
}
export default { export default {
name: 'ImageDiffOverlay', name: 'ImageDiffOverlay',
components: { components: {
...@@ -39,6 +43,14 @@ export default { ...@@ -39,6 +43,14 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
renderedWidth: {
type: Number,
required: true,
},
renderedHeight: {
type: Number,
required: true,
},
}, },
computed: { computed: {
...mapGetters('diffs', ['getDiffFileByHash', 'getCommentFormForDiffFile']), ...mapGetters('diffs', ['getDiffFileByHash', 'getCommentFormForDiffFile']),
...@@ -59,33 +71,33 @@ export default { ...@@ -59,33 +71,33 @@ export default {
}, },
getPositionForObject(meta) { getPositionForObject(meta) {
const { x, y, width, height } = meta; const { x, y, width, height } = meta;
const imageWidth = this.getImageDimensions().width;
const imageHeight = this.getImageDimensions().height;
const widthRatio = imageWidth / width;
const heightRatio = imageHeight / height;
return { return {
x: Math.round(x * widthRatio), x: (x / width) * 100,
y: Math.round(y * heightRatio), y: (y / height) * 100,
}; };
}, },
getPosition(discussion) { getPosition(discussion) {
const { x, y } = this.getPositionForObject(discussion.position); const { x, y } = this.getPositionForObject(discussion.position);
return { return {
left: `${x}px`, left: `${x}%`,
top: `${y}px`, top: `${y}%`,
}; };
}, },
clickedImage(x, y) { clickedImage(x, y) {
const { width, height } = this.getImageDimensions(); const { width, height } = this.getImageDimensions();
const xPercent = calcPercent(x, width, this.renderedWidth);
const yPercent = calcPercent(y, height, this.renderedHeight);
this.openDiffFileCommentForm({ this.openDiffFileCommentForm({
fileHash: this.fileHash, fileHash: this.fileHash,
width, width,
height, height,
x, x: width * (xPercent / 100),
y, y: height * (yPercent / 100),
xPercent,
yPercent,
}); });
}, },
}, },
...@@ -112,22 +124,19 @@ export default { ...@@ -112,22 +124,19 @@ export default {
type="button" type="button"
@click="clickedToggle(discussion)" @click="clickedToggle(discussion)"
> >
<gl-icon v-if="showCommentIcon" name="image-comment-dark" /> <gl-icon v-if="showCommentIcon" name="image-comment-dark" :size="24" />
<template v-else> <template v-else>
{{ toggleText(discussion, index) }} {{ toggleText(discussion, index) }}
</template> </template>
</button> </button>
<button <button
v-if="currentCommentForm" v-if="canComment && currentCommentForm"
:style="{ :style="{ left: `${currentCommentForm.xPercent}%`, top: `${currentCommentForm.yPercent}%` }"
left: `${currentCommentForm.x}px`,
top: `${currentCommentForm.y}px`,
}"
:aria-label="__('Comment form position')" :aria-label="__('Comment form position')"
class="btn-transparent comment-indicator" class="btn-transparent comment-indicator position-absolute"
type="button" type="button"
> >
<gl-icon name="image-comment-dark" /> <gl-icon name="image-comment-dark" :size="24" />
</button> </button>
</div> </div>
</template> </template>
...@@ -131,14 +131,18 @@ export default { ...@@ -131,14 +131,18 @@ export default {
:file-hash="discussion.diff_file.file_hash" :file-hash="discussion.diff_file.file_hash"
:project-path="projectPath" :project-path="projectPath"
> >
<template #image-overlay="{ renderedWidth, renderedHeight }">
<image-diff-overlay <image-diff-overlay
slot="image-overlay" v-if="renderedWidth"
:rendered-width="renderedWidth"
:rendered-height="renderedHeight"
:discussions="discussion" :discussions="discussion"
:file-hash="discussion.diff_file.file_hash" :file-hash="discussion.diff_file.file_hash"
:show-comment-icon="true" :show-comment-icon="true"
:should-toggle-discussion="false" :should-toggle-discussion="false"
badge-class="image-comment-badge" badge-class="image-comment-badge gl-text-gray-500"
/> />
</template>
</diff-viewer> </diff-viewer>
<slot></slot> <slot></slot>
</div> </div>
......
...@@ -28,6 +28,8 @@ export default { ...@@ -28,6 +28,8 @@ export default {
return { return {
width: 0, width: 0,
height: 0, height: 0,
renderedWidth: 0,
renderedHeight: 0,
}; };
}, },
computed: { computed: {
...@@ -63,11 +65,14 @@ export default { ...@@ -63,11 +65,14 @@ export default {
this.height = contentImg.naturalHeight; this.height = contentImg.naturalHeight;
this.$nextTick(() => { this.$nextTick(() => {
this.renderedWidth = contentImg.clientWidth;
this.renderedHeight = contentImg.clientHeight;
this.$emit('imgLoaded', { this.$emit('imgLoaded', {
width: this.width, width: this.width,
height: this.height, height: this.height,
renderedWidth: contentImg.clientWidth, renderedWidth: this.renderedWidth,
renderedHeight: contentImg.clientHeight, renderedHeight: this.renderedHeight,
}); });
}); });
} }
...@@ -79,7 +84,12 @@ export default { ...@@ -79,7 +84,12 @@ export default {
<template> <template>
<div data-testid="image-viewer"> <div data-testid="image-viewer">
<div :class="innerCssClasses" class="position-relative"> <div :class="innerCssClasses" class="position-relative">
<img ref="contentImg" :src="path" @load="onImgLoad" /> <slot name="image-overlay"></slot> <img ref="contentImg" :src="path" @load="onImgLoad" />
<slot
name="image-overlay"
:rendered-width="renderedWidth"
:rendered-height="renderedHeight"
></slot>
</div> </div>
<p v-if="renderInfo" class="image-info"> <p v-if="renderInfo" class="image-info">
<template v-if="hasFileSize"> <template v-if="hasFileSize">
......
...@@ -106,7 +106,13 @@ export default { ...@@ -106,7 +106,13 @@ export default {
:a-mode="aMode" :a-mode="aMode"
:b-mode="bMode" :b-mode="bMode"
> >
<slot slot="image-overlay" name="image-overlay"></slot> <template #image-overlay="{ renderedWidth, renderedHeight }">
<slot
:rendered-width="renderedWidth"
:rendered-height="renderedHeight"
name="image-overlay"
></slot>
</template>
</component> </component>
<slot></slot> <slot></slot>
</div> </div>
......
...@@ -141,7 +141,13 @@ export default { ...@@ -141,7 +141,13 @@ export default {
:path="newPath" :path="newPath"
@imgLoaded="onionNewImgLoaded" @imgLoaded="onionNewImgLoaded"
> >
<slot slot="image-overlay" name="image-overlay"> </slot> <template #image-overlay="{ renderedWidth, renderedHeight }">
<slot
:rendered-width="renderedWidth"
:rendered-height="renderedHeight"
name="image-overlay"
></slot>
</template>
</image-viewer> </image-viewer>
</div> </div>
<div class="controls"> <div class="controls">
......
...@@ -143,7 +143,13 @@ export default { ...@@ -143,7 +143,13 @@ export default {
class="frame added" class="frame added"
@imgLoaded="swipeNewImgLoaded" @imgLoaded="swipeNewImgLoaded"
> >
<slot slot="image-overlay" name="image-overlay"> </slot> <template #image-overlay="{ renderedWidth, renderedHeight }">
<slot
:rendered-width="renderedWidth"
:rendered-height="renderedHeight"
name="image-overlay"
></slot>
</template>
</image-viewer> </image-viewer>
</div> </div>
<span <span
......
...@@ -44,7 +44,13 @@ export default { ...@@ -44,7 +44,13 @@ export default {
:inner-css-classes="['frame', 'added']" :inner-css-classes="['frame', 'added']"
class="wrap w-50" class="wrap w-50"
> >
<slot slot="image-overlay" name="image-overlay"> </slot> <template #image-overlay="{ renderedWidth, renderedHeight }">
<slot
:rendered-width="renderedWidth"
:rendered-height="renderedHeight"
name="image-overlay"
></slot>
</template>
</image-viewer> </image-viewer>
</div> </div>
</template> </template>
...@@ -76,7 +76,13 @@ export default { ...@@ -76,7 +76,13 @@ export default {
<div v-if="diffMode === $options.diffModes.replaced" class="diff-viewer"> <div v-if="diffMode === $options.diffModes.replaced" class="diff-viewer">
<div class="image js-replaced-image"> <div class="image js-replaced-image">
<component :is="imageViewComponent" v-bind="$props"> <component :is="imageViewComponent" v-bind="$props">
<slot slot="image-overlay" name="image-overlay"> </slot> <template #image-overlay="{ renderedWidth, renderedHeight }">
<slot
:rendered-width="renderedWidth"
:rendered-height="renderedHeight"
name="image-overlay"
></slot>
</template>
</component> </component>
</div> </div>
<div class="view-modes"> <div class="view-modes">
...@@ -121,7 +127,13 @@ export default { ...@@ -121,7 +127,13 @@ export default {
}, },
]" ]"
> >
<slot v-if="isNew || isRenamed" slot="image-overlay" name="image-overlay"> </slot> <template v-if="isNew || isRenamed" #image-overlay="{ renderedWidth, renderedHeight }">
<slot
:rendered-width="renderedWidth"
:rendered-height="renderedHeight"
name="image-overlay"
></slot>
</template>
</image-viewer> </image-viewer>
</div> </div>
</div> </div>
......
---
title: Fixed image diff comments positioning
merge_request:
author:
type: fixed
...@@ -6,7 +6,6 @@ import InlineDiffView from '~/diffs/components/inline_diff_view.vue'; ...@@ -6,7 +6,6 @@ import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue'; import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue'; import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue'; import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import NoteForm from '~/notes/components/note_form.vue'; import NoteForm from '~/notes/components/note_form.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue'; import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants'; import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
...@@ -167,14 +166,6 @@ describe('DiffContent', () => { ...@@ -167,14 +166,6 @@ describe('DiffContent', () => {
describe('with image files', () => { describe('with image files', () => {
const imageDiffFile = { ...defaultProps.diffFile, viewer: { name: diffViewerModes.image } }; const imageDiffFile = { ...defaultProps.diffFile, viewer: { name: diffViewerModes.image } };
it('should have image diff view in place', () => {
getCommentFormForDiffFileGetterMock.mockReturnValue(() => true);
createComponent({ props: { diffFile: imageDiffFile } });
expect(wrapper.find(InlineDiffView).exists()).toBe(false);
expect(wrapper.find(ImageDiffOverlay).exists()).toBe(true);
});
it('renders diff file discussions', () => { it('renders diff file discussions', () => {
getCommentFormForDiffFileGetterMock.mockReturnValue(() => true); getCommentFormForDiffFileGetterMock.mockReturnValue(() => true);
createComponent({ createComponent({
......
...@@ -24,6 +24,8 @@ describe('Diffs image diff overlay component', () => { ...@@ -24,6 +24,8 @@ describe('Diffs image diff overlay component', () => {
propsData: { propsData: {
discussions: [...imageDiffDiscussions], discussions: [...imageDiffDiscussions],
fileHash: 'ABC', fileHash: 'ABC',
renderedWidth: 200,
renderedHeight: 200,
...props, ...props,
}, },
methods: { methods: {
...@@ -71,8 +73,8 @@ describe('Diffs image diff overlay component', () => { ...@@ -71,8 +73,8 @@ describe('Diffs image diff overlay component', () => {
createComponent(); createComponent();
const imageBadges = getAllImageBadges(); const imageBadges = getAllImageBadges();
expect(imageBadges.at(0).attributes('style')).toBe('left: 10px; top: 10px;'); expect(imageBadges.at(0).attributes('style')).toBe('left: 10%; top: 5%;');
expect(imageBadges.at(1).attributes('style')).toBe('left: 5px; top: 5px;'); expect(imageBadges.at(1).attributes('style')).toBe('left: 5%; top: 2.5%;');
}); });
it('renders single badge for discussion object', () => { it('renders single badge for discussion object', () => {
...@@ -95,6 +97,8 @@ describe('Diffs image diff overlay component', () => { ...@@ -95,6 +97,8 @@ describe('Diffs image diff overlay component', () => {
y: 0, y: 0,
width: 100, width: 100,
height: 200, height: 200,
xPercent: 0,
yPercent: 0,
}); });
}); });
...@@ -120,11 +124,13 @@ describe('Diffs image diff overlay component', () => { ...@@ -120,11 +124,13 @@ describe('Diffs image diff overlay component', () => {
describe('comment form', () => { describe('comment form', () => {
const getCommentIndicator = () => wrapper.find('.comment-indicator'); const getCommentIndicator = () => wrapper.find('.comment-indicator');
beforeEach(() => { beforeEach(() => {
createComponent({}, store => { createComponent({ canComment: true }, store => {
store.state.diffs.commentForms.push({ store.state.diffs.commentForms.push({
fileHash: 'ABC', fileHash: 'ABC',
x: 20, x: 20,
y: 10, y: 10,
xPercent: 10,
yPercent: 10,
}); });
}); });
}); });
...@@ -134,7 +140,7 @@ describe('Diffs image diff overlay component', () => { ...@@ -134,7 +140,7 @@ describe('Diffs image diff overlay component', () => {
}); });
it('sets comment form badge position', () => { it('sets comment form badge position', () => {
expect(getCommentIndicator().attributes('style')).toBe('left: 20px; top: 10px;'); expect(getCommentIndicator().attributes('style')).toBe('left: 10%; top: 10%;');
}); });
}); });
}); });
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