Merge branch 'mr-image-commenting' into 'master'
Re-implemented image commenting on diffs Closes #48956 See merge request gitlab-org/gitlab-ce!22443
This commit is contained in:
commit
e1e315ee28
|
@ -223,7 +223,10 @@ export default {
|
|||
:commit="commit"
|
||||
/>
|
||||
|
||||
<div class="files d-flex prepend-top-default">
|
||||
<div
|
||||
:data-can-create-note="getNoteableData.current_user.can_create_note"
|
||||
class="files d-flex prepend-top-default"
|
||||
>
|
||||
<div
|
||||
v-show="showTreeList"
|
||||
class="diff-tree-list"
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
<script>
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
|
||||
import { diffModes } from '~/ide/constants';
|
||||
import InlineDiffView from './inline_diff_view.vue';
|
||||
import ParallelDiffView from './parallel_diff_view.vue';
|
||||
import NoteForm from '../../notes/components/note_form.vue';
|
||||
import ImageDiffOverlay from './image_diff_overlay.vue';
|
||||
import DiffDiscussions from './diff_discussions.vue';
|
||||
import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
|
||||
import { getDiffMode } from '../store/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InlineDiffView,
|
||||
ParallelDiffView,
|
||||
DiffViewer,
|
||||
NoteForm,
|
||||
DiffDiscussions,
|
||||
ImageDiffOverlay,
|
||||
},
|
||||
props: {
|
||||
diffFile: {
|
||||
|
@ -23,13 +30,38 @@ export default {
|
|||
endpoint: state => state.diffs.endpoint,
|
||||
}),
|
||||
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
|
||||
...mapGetters('diffs', ['getCommentFormForDiffFile']),
|
||||
...mapGetters(['getNoteableData', 'noteableType']),
|
||||
diffMode() {
|
||||
const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]);
|
||||
return diffModes[diffModeKey] || diffModes.replaced;
|
||||
return getDiffMode(this.diffFile);
|
||||
},
|
||||
isTextFile() {
|
||||
return this.diffFile.viewer.name === 'text';
|
||||
},
|
||||
diffFileCommentForm() {
|
||||
return this.getCommentFormForDiffFile(this.diffFile.fileHash);
|
||||
},
|
||||
showNotesContainer() {
|
||||
return this.diffFile.discussions.length || this.diffFileCommentForm;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['saveDiffDiscussion', 'closeDiffFileCommentForm']),
|
||||
handleSaveNote(note) {
|
||||
this.saveDiffDiscussion({
|
||||
note,
|
||||
formData: {
|
||||
noteableData: this.getNoteableData,
|
||||
noteableType: this.noteableType,
|
||||
diffFile: this.diffFile,
|
||||
positionType: IMAGE_DIFF_POSITION_TYPE,
|
||||
x: this.diffFileCommentForm.x,
|
||||
y: this.diffFileCommentForm.y,
|
||||
width: this.diffFileCommentForm.width,
|
||||
height: this.diffFileCommentForm.height,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -56,7 +88,37 @@ export default {
|
|||
:new-sha="diffFile.diffRefs.headSha"
|
||||
:old-path="diffFile.oldPath"
|
||||
:old-sha="diffFile.diffRefs.baseSha"
|
||||
:project-path="projectPath"/>
|
||||
:file-hash="diffFile.fileHash"
|
||||
:project-path="projectPath"
|
||||
>
|
||||
<image-diff-overlay
|
||||
slot="image-overlay"
|
||||
:discussions="diffFile.discussions"
|
||||
:file-hash="diffFile.fileHash"
|
||||
:can-comment="getNoteableData.current_user.can_create_note"
|
||||
/>
|
||||
<div
|
||||
v-if="showNotesContainer"
|
||||
class="note-container"
|
||||
>
|
||||
<diff-discussions
|
||||
v-if="diffFile.discussions.length"
|
||||
class="diff-file-discussions"
|
||||
:discussions="diffFile.discussions"
|
||||
:should-collapse-discussions="true"
|
||||
:render-avatar-badge="true"
|
||||
/>
|
||||
<note-form
|
||||
v-if="diffFileCommentForm"
|
||||
ref="noteForm"
|
||||
:is-editing="false"
|
||||
:save-button-title="__('Comment')"
|
||||
class="diff-comment-form new-note discussion-form discussion-form-container"
|
||||
@handleFormUpdate="handleSaveNote"
|
||||
@cancelForm="closeDiffFileCommentForm(diffFile.fileHash)"
|
||||
/>
|
||||
</div>
|
||||
</diff-viewer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,24 +1,40 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
noteableDiscussion,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
discussions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
shouldCollapseDiscussions: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
renderAvatarBadge: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleDiscussion']),
|
||||
...mapActions('diffs', ['removeDiscussionsFromDiff']),
|
||||
deleteNoteHandler(discussion) {
|
||||
if (discussion.notes.length <= 1) {
|
||||
this.removeDiscussionsFromDiff(discussion);
|
||||
}
|
||||
},
|
||||
isExpanded(discussion) {
|
||||
return this.shouldCollapseDiscussions ? discussion.expanded : true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -26,22 +42,54 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="discussion in discussions"
|
||||
v-for="(discussion, index) in discussions"
|
||||
:key="discussion.id"
|
||||
class="discussion-notes diff-discussions"
|
||||
:class="{
|
||||
collapsed: !isExpanded(discussion)
|
||||
}"
|
||||
class="discussion-notes diff-discussions position-relative"
|
||||
>
|
||||
<ul
|
||||
:data-discussion-id="discussion.id"
|
||||
class="notes"
|
||||
>
|
||||
<template v-if="shouldCollapseDiscussions">
|
||||
<button
|
||||
:class="{
|
||||
'diff-notes-collapse': discussion.expanded,
|
||||
'btn-transparent badge badge-pill': !discussion.expanded
|
||||
}"
|
||||
type="button"
|
||||
class="js-diff-notes-toggle"
|
||||
@click="toggleDiscussion({ discussionId: discussion.id })"
|
||||
>
|
||||
<icon
|
||||
v-if="discussion.expanded"
|
||||
name="collapse"
|
||||
class="collapse-icon"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
<noteable-discussion
|
||||
v-show="isExpanded(discussion)"
|
||||
:discussion="discussion"
|
||||
:render-header="false"
|
||||
:render-diff-file="false"
|
||||
:always-expanded="true"
|
||||
:discussions-by-diff-order="true"
|
||||
@noteDeleted="deleteNoteHandler"
|
||||
/>
|
||||
>
|
||||
<span
|
||||
v-if="renderAvatarBadge"
|
||||
slot="avatar-badge"
|
||||
class="badge badge-pill"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
</noteable-discussion>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import _ from 'underscore';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'ImageDiffOverlay',
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
discussions: {
|
||||
type: [Array, Object],
|
||||
required: true,
|
||||
},
|
||||
fileHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
canComment: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showCommentIcon: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
badgeClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'badge badge-pill',
|
||||
},
|
||||
shouldToggleDiscussion: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('diffs', ['getDiffFileByHash', 'getCommentFormForDiffFile']),
|
||||
currentCommentForm() {
|
||||
return this.getCommentFormForDiffFile(this.fileHash);
|
||||
},
|
||||
allDiscussions() {
|
||||
return _.isArray(this.discussions) ? this.discussions : [this.discussions];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleDiscussion']),
|
||||
...mapActions('diffs', ['openDiffFileCommentForm']),
|
||||
getImageDimensions() {
|
||||
return {
|
||||
width: this.$parent.width,
|
||||
height: this.$parent.height,
|
||||
};
|
||||
},
|
||||
getPositionForObject(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 {
|
||||
x: Math.round(x * widthRatio),
|
||||
y: Math.round(y * heightRatio),
|
||||
};
|
||||
},
|
||||
getPosition(discussion) {
|
||||
const { x, y } = this.getPositionForObject(discussion.position);
|
||||
|
||||
return {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
};
|
||||
},
|
||||
clickedImage(x, y) {
|
||||
const { width, height } = this.getImageDimensions();
|
||||
|
||||
this.openDiffFileCommentForm({
|
||||
fileHash: this.fileHash,
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="position-absolute w-100 h-100 image-diff-overlay">
|
||||
<button
|
||||
v-if="canComment"
|
||||
type="button"
|
||||
class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
|
||||
@click="clickedImage($event.offsetX, $event.offsetY)"
|
||||
>
|
||||
<span class="sr-only">
|
||||
{{ __('Add image comment') }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-for="(discussion, index) in allDiscussions"
|
||||
:key="discussion.id"
|
||||
:style="getPosition(discussion)"
|
||||
:class="badgeClass"
|
||||
:disabled="!shouldToggleDiscussion"
|
||||
class="js-image-badge"
|
||||
type="button"
|
||||
@click="toggleDiscussion({ discussionId: discussion.id })"
|
||||
>
|
||||
<icon
|
||||
v-if="showCommentIcon"
|
||||
name="image-comment-dark"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
</button>
|
||||
<button
|
||||
v-if="currentCommentForm"
|
||||
:style="{
|
||||
left: `${currentCommentForm.x}px`,
|
||||
top: `${currentCommentForm.y}px`
|
||||
}"
|
||||
:aria-label="__('Comment form position')"
|
||||
class="btn-transparent comment-indicator"
|
||||
type="button"
|
||||
>
|
||||
<icon
|
||||
name="image-comment-dark"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -12,6 +12,7 @@ export const NOTE_TYPE = 'Note';
|
|||
export const NEW_LINE_TYPE = 'new';
|
||||
export const OLD_LINE_TYPE = 'old';
|
||||
export const TEXT_DIFF_POSITION_TYPE = 'text';
|
||||
export const IMAGE_DIFF_POSITION_TYPE = 'image';
|
||||
|
||||
export const LINE_POSITION_LEFT = 'left';
|
||||
export const LINE_POSITION_RIGHT = 'right';
|
||||
|
|
|
@ -50,8 +50,8 @@ export const assignDiscussionsToDiff = (
|
|||
};
|
||||
|
||||
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
|
||||
const { fileHash, line_code } = removeDiscussion;
|
||||
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code });
|
||||
const { fileHash, line_code, id } = removeDiscussion;
|
||||
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code, id });
|
||||
};
|
||||
|
||||
export const startRenderDiffsQueue = ({ state, commit }) => {
|
||||
|
@ -189,6 +189,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
|
|||
return dispatch('saveNote', postData, { root: true })
|
||||
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
|
||||
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
|
||||
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.fileHash))
|
||||
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
|
||||
};
|
||||
|
||||
|
@ -210,5 +211,19 @@ export const toggleShowTreeList = ({ commit, state }) => {
|
|||
localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
|
||||
};
|
||||
|
||||
export const openDiffFileCommentForm = ({ commit, getters }, formData) => {
|
||||
const form = getters.getCommentFormForDiffFile(formData.fileHash);
|
||||
|
||||
if (form) {
|
||||
commit(types.UPDATE_DIFF_FILE_COMMENT_FORM, formData);
|
||||
} else {
|
||||
commit(types.OPEN_DIFF_FILE_COMMENT_FORM, formData);
|
||||
}
|
||||
};
|
||||
|
||||
export const closeDiffFileCommentForm = ({ commit }, fileHash) => {
|
||||
commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash);
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -114,5 +114,8 @@ export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.
|
|||
|
||||
export const diffFilesLength = state => state.diffFiles.length;
|
||||
|
||||
export const getCommentFormForDiffFile = state => fileHash =>
|
||||
state.commentForms.find(form => form.fileHash === fileHash);
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -24,4 +24,6 @@ export default () => ({
|
|||
showTreeList:
|
||||
storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : storedTreeShow === 'true',
|
||||
currentDiffFileId: '',
|
||||
projectPath: '',
|
||||
commentForms: [],
|
||||
});
|
||||
|
|
|
@ -14,3 +14,7 @@ export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FIL
|
|||
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
|
||||
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
|
||||
export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
|
||||
|
||||
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
|
||||
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
|
||||
export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM';
|
||||
|
|
|
@ -153,20 +153,22 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
|
||||
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode, id }) {
|
||||
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
|
||||
if (selectedFile) {
|
||||
const targetLine = selectedFile.parallelDiffLines.find(
|
||||
line =>
|
||||
(line.left && line.left.lineCode === lineCode) ||
|
||||
(line.right && line.right.lineCode === lineCode),
|
||||
);
|
||||
if (targetLine) {
|
||||
const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
|
||||
if (selectedFile.parallelDiffLines) {
|
||||
const targetLine = selectedFile.parallelDiffLines.find(
|
||||
line =>
|
||||
(line.left && line.left.lineCode === lineCode) ||
|
||||
(line.right && line.right.lineCode === lineCode),
|
||||
);
|
||||
if (targetLine) {
|
||||
const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
|
||||
|
||||
Object.assign(targetLine[side], {
|
||||
discussions: [],
|
||||
});
|
||||
Object.assign(targetLine[side], {
|
||||
discussions: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFile.highlightedDiffLines) {
|
||||
|
@ -180,6 +182,12 @@ export default {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFile.discussions && selectedFile.discussions.length) {
|
||||
selectedFile.discussions = selectedFile.discussions.filter(
|
||||
discussion => discussion.id !== id,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[types.TOGGLE_FOLDER_OPEN](state, path) {
|
||||
|
@ -191,4 +199,25 @@ export default {
|
|||
[types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
|
||||
state.currentDiffFileId = fileId;
|
||||
},
|
||||
[types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
|
||||
state.commentForms.push({
|
||||
...formData,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_DIFF_FILE_COMMENT_FORM](state, formData) {
|
||||
const { fileHash } = formData;
|
||||
|
||||
state.commentForms = state.commentForms.map(form => {
|
||||
if (form.fileHash === fileHash) {
|
||||
return {
|
||||
...formData,
|
||||
};
|
||||
}
|
||||
|
||||
return form;
|
||||
});
|
||||
},
|
||||
[types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) {
|
||||
state.commentForms = state.commentForms.filter(form => form.fileHash !== fileHash);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'underscore';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { diffModes } from '~/ide/constants';
|
||||
import {
|
||||
LINE_POSITION_LEFT,
|
||||
LINE_POSITION_RIGHT,
|
||||
|
@ -34,6 +35,7 @@ export function getFormData(params) {
|
|||
noteTargetLine,
|
||||
diffViewType,
|
||||
linePosition,
|
||||
positionType,
|
||||
} = params;
|
||||
|
||||
const position = JSON.stringify({
|
||||
|
@ -42,9 +44,13 @@ export function getFormData(params) {
|
|||
head_sha: diffFile.diffRefs.headSha,
|
||||
old_path: diffFile.oldPath,
|
||||
new_path: diffFile.newPath,
|
||||
position_type: TEXT_DIFF_POSITION_TYPE,
|
||||
old_line: noteTargetLine.oldLine,
|
||||
new_line: noteTargetLine.newLine,
|
||||
position_type: positionType || TEXT_DIFF_POSITION_TYPE,
|
||||
old_line: noteTargetLine ? noteTargetLine.oldLine : null,
|
||||
new_line: noteTargetLine ? noteTargetLine.newLine : null,
|
||||
x: params.x,
|
||||
y: params.y,
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
});
|
||||
|
||||
const postData = {
|
||||
|
@ -66,7 +72,7 @@ export function getFormData(params) {
|
|||
diffFile.diffRefs.startSha && diffFile.diffRefs.headSha
|
||||
? DIFF_NOTE_TYPE
|
||||
: LEGACY_DIFF_NOTE_TYPE,
|
||||
line_code: noteTargetLine.lineCode,
|
||||
line_code: noteTargetLine ? noteTargetLine.lineCode : null,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -225,6 +231,7 @@ export function prepareDiffData(diffData) {
|
|||
Object.assign(file, {
|
||||
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
|
||||
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
|
||||
discussions: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -320,3 +327,8 @@ export const generateTreeList = files =>
|
|||
},
|
||||
{ treeEntries: {}, tree: [] },
|
||||
);
|
||||
|
||||
export const getDiffMode = diffFile => {
|
||||
const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}File`]);
|
||||
return diffModes[diffModeKey] || diffModes.replaced;
|
||||
};
|
||||
|
|
|
@ -11,7 +11,6 @@ import bp from './breakpoints';
|
|||
import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
|
||||
import { isInVueNoteablePage } from './lib/utils/dom_utils';
|
||||
import { getLocationHash } from './lib/utils/url_utility';
|
||||
import initDiscussionTab from './image_diff/init_discussion_tab';
|
||||
import Diff from './diff';
|
||||
import { localTimeAgo } from './lib/utils/datetime_utility';
|
||||
import syntaxHighlight from './syntax_highlight';
|
||||
|
@ -207,8 +206,6 @@ export default class MergeRequestTabs {
|
|||
}
|
||||
this.resetViewContainer();
|
||||
this.destroyPipelinesView();
|
||||
|
||||
initDiscussionTab();
|
||||
}
|
||||
if (this.setUrl) {
|
||||
this.setCurrentAction(action);
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import imageDiffHelper from '~/image_diff/helpers/index';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
|
||||
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
|
||||
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
|
||||
import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
|
||||
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
|
||||
import { trimFirstCharOfLineContent, getDiffMode } from '~/diffs/store/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DiffFileHeader,
|
||||
GlSkeletonLoading,
|
||||
DiffViewer,
|
||||
ImageDiffOverlay,
|
||||
},
|
||||
props: {
|
||||
discussion: {
|
||||
|
@ -25,7 +28,11 @@ export default {
|
|||
computed: {
|
||||
...mapState({
|
||||
noteableData: state => state.notes.noteableData,
|
||||
projectPath: state => state.diffs.projectPath,
|
||||
}),
|
||||
diffMode() {
|
||||
return getDiffMode(this.diffFile);
|
||||
},
|
||||
hasTruncatedDiffLines() {
|
||||
return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
|
||||
},
|
||||
|
@ -62,11 +69,7 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.isImageDiff) {
|
||||
const canCreateNote = false;
|
||||
const renderCommentBadge = true;
|
||||
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
|
||||
} else if (!this.hasTruncatedDiffLines) {
|
||||
if (!this.hasTruncatedDiffLines) {
|
||||
this.fetchDiff();
|
||||
}
|
||||
},
|
||||
|
@ -160,7 +163,24 @@ export default {
|
|||
<div
|
||||
v-else
|
||||
>
|
||||
<div v-html="imageDiffHtml"></div>
|
||||
<diff-viewer
|
||||
:diff-mode="diffMode"
|
||||
:new-path="diffFile.newPath"
|
||||
:new-sha="diffFile.diffRefs.headSha"
|
||||
:old-path="diffFile.oldPath"
|
||||
:old-sha="diffFile.diffRefs.baseSha"
|
||||
:file-hash="diffFile.fileHash"
|
||||
:project-path="projectPath"
|
||||
>
|
||||
<image-diff-overlay
|
||||
slot="image-overlay"
|
||||
:discussions="discussion"
|
||||
:file-hash="diffFile.fileHash"
|
||||
:show-comment-icon="true"
|
||||
:should-toggle-discussion="false"
|
||||
badge-class="image-comment-badge"
|
||||
/>
|
||||
</diff-viewer>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -350,11 +350,18 @@ Please check your network connection and try again.`;
|
|||
<ul class="notes">
|
||||
<component
|
||||
:is="componentName(note)"
|
||||
v-for="note in discussion.notes"
|
||||
v-for="(note, index) in discussion.notes"
|
||||
:key="note.id"
|
||||
:note="componentData(note)"
|
||||
@handleDeleteNote="deleteNoteHandler"
|
||||
/>
|
||||
>
|
||||
<slot
|
||||
v-if="index === 0"
|
||||
slot="avatar-badge"
|
||||
name="avatar-badge"
|
||||
>
|
||||
</slot>
|
||||
</component>
|
||||
</ul>
|
||||
<div
|
||||
:class="{ 'is-replying': isReplying }"
|
||||
|
|
|
@ -182,7 +182,13 @@ export default {
|
|||
:img-src="author.avatar_url"
|
||||
:img-alt="author.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
>
|
||||
<slot
|
||||
slot="avatar-badge"
|
||||
name="avatar-badge"
|
||||
>
|
||||
</slot>
|
||||
</user-avatar-link>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="note-header">
|
||||
|
|
|
@ -17,19 +17,37 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
innerCssClasses: {
|
||||
type: [Array, Object, String],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
isZoomable: false,
|
||||
isZoomed: false,
|
||||
isLoaded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
fileSizeReadable() {
|
||||
return numberToHumanSize(this.fileSize);
|
||||
},
|
||||
dimensionStyles() {
|
||||
if (!this.isLoaded) return {};
|
||||
|
||||
return {
|
||||
width: `${this.width}px`,
|
||||
height: `${this.height}px`,
|
||||
};
|
||||
},
|
||||
hasFileSize() {
|
||||
return this.fileSize > 0;
|
||||
},
|
||||
hasDimensions() {
|
||||
return this.width && this.height;
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resizeThrottled, false);
|
||||
|
@ -48,51 +66,52 @@ export default {
|
|||
const { contentImg } = this.$refs;
|
||||
|
||||
if (contentImg) {
|
||||
this.isZoomable =
|
||||
contentImg.naturalWidth > contentImg.width ||
|
||||
contentImg.naturalHeight > contentImg.height;
|
||||
|
||||
this.width = contentImg.naturalWidth;
|
||||
this.height = contentImg.naturalHeight;
|
||||
|
||||
this.$emit('imgLoaded', {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
renderedWidth: contentImg.clientWidth,
|
||||
renderedHeight: contentImg.clientHeight,
|
||||
this.$nextTick(() => {
|
||||
this.isLoaded = true;
|
||||
|
||||
this.$emit('imgLoaded', {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
renderedWidth: contentImg.clientWidth,
|
||||
renderedHeight: contentImg.clientHeight,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
onImgClick() {
|
||||
if (this.isZoomable) this.isZoomed = !this.isZoomed;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="file-container">
|
||||
<div class="file-content image_file">
|
||||
<div>
|
||||
<div
|
||||
:class="innerCssClasses"
|
||||
:style="dimensionStyles"
|
||||
class="position-relative"
|
||||
>
|
||||
<img
|
||||
ref="contentImg"
|
||||
:class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
|
||||
:src="path"
|
||||
:alt="path"
|
||||
@load="onImgLoad"
|
||||
@click="onImgClick"/>
|
||||
<p
|
||||
v-if="renderInfo"
|
||||
class="file-info prepend-top-10">
|
||||
<template v-if="fileSize>0">
|
||||
{{ fileSizeReadable }}
|
||||
</template>
|
||||
<template v-if="fileSize>0 && width && height">
|
||||
|
|
||||
</template>
|
||||
<template v-if="width && height">
|
||||
W: {{ width }} | H: {{ height }}
|
||||
</template>
|
||||
</p>
|
||||
/>
|
||||
<slot name="image-overlay"></slot>
|
||||
</div>
|
||||
<p
|
||||
v-if="renderInfo"
|
||||
class="image-info"
|
||||
>
|
||||
<template v-if="hasFileSize">
|
||||
{{ fileSizeReadable }}
|
||||
</template>
|
||||
<template v-if="hasFileSize && hasDimensions">
|
||||
|
|
||||
</template>
|
||||
<template v-if="hasDimensions">
|
||||
<strong>W</strong>: {{ width }} | <strong>H</strong>: {{ height }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -69,6 +69,13 @@ export default {
|
|||
:new-path="fullNewPath"
|
||||
:old-path="fullOldPath"
|
||||
:project-path="projectPath"
|
||||
/>
|
||||
>
|
||||
<slot
|
||||
slot="image-overlay"
|
||||
name="image-overlay"
|
||||
>
|
||||
</slot>
|
||||
</component>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -15,11 +15,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -120,7 +115,6 @@ export default {
|
|||
key="onionOldImg"
|
||||
:render-info="false"
|
||||
:path="oldPath"
|
||||
:project-path="projectPath"
|
||||
@imgLoaded="onionOldImgLoaded"
|
||||
/>
|
||||
</div>
|
||||
|
@ -136,9 +130,14 @@ export default {
|
|||
key="onionNewImg"
|
||||
:render-info="false"
|
||||
:path="newPath"
|
||||
:project-path="projectPath"
|
||||
@imgLoaded="onionNewImgLoaded"
|
||||
/>
|
||||
>
|
||||
<slot
|
||||
slot="image-overlay"
|
||||
name="image-overlay"
|
||||
>
|
||||
</slot>
|
||||
</image-viewer>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="transparent"></div>
|
||||
|
|
|
@ -16,11 +16,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -117,16 +112,14 @@ export default {
|
|||
'height': swipeMaxPixelHeight,
|
||||
}"
|
||||
class="swipe-frame">
|
||||
<div class="frame deleted">
|
||||
<image-viewer
|
||||
key="swipeOldImg"
|
||||
ref="swipeOldImg"
|
||||
:render-info="false"
|
||||
:path="oldPath"
|
||||
:project-path="projectPath"
|
||||
@imgLoaded="swipeOldImgLoaded"
|
||||
/>
|
||||
</div>
|
||||
<image-viewer
|
||||
key="swipeOldImg"
|
||||
ref="swipeOldImg"
|
||||
:render-info="false"
|
||||
:path="oldPath"
|
||||
class="frame deleted"
|
||||
@imgLoaded="swipeOldImgLoaded"
|
||||
/>
|
||||
<div
|
||||
ref="swipeWrap"
|
||||
:style="{
|
||||
|
@ -134,15 +127,19 @@ export default {
|
|||
'height': swipeMaxPixelHeight,
|
||||
}"
|
||||
class="swipe-wrap">
|
||||
<div class="frame added">
|
||||
<image-viewer
|
||||
key="swipeNewImg"
|
||||
:render-info="false"
|
||||
:path="newPath"
|
||||
:project-path="projectPath"
|
||||
@imgLoaded="swipeNewImgLoaded"
|
||||
/>
|
||||
</div>
|
||||
<image-viewer
|
||||
key="swipeNewImg"
|
||||
:render-info="false"
|
||||
:path="newPath"
|
||||
class="frame added"
|
||||
@imgLoaded="swipeNewImgLoaded"
|
||||
>
|
||||
<slot
|
||||
slot="image-overlay"
|
||||
name="image-overlay"
|
||||
>
|
||||
</slot>
|
||||
</image-viewer>
|
||||
</div>
|
||||
<span
|
||||
ref="swipeBar"
|
||||
|
|
|
@ -14,28 +14,29 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="two-up view row">
|
||||
<div class="col-sm-6 frame deleted">
|
||||
<image-viewer
|
||||
:path="oldPath"
|
||||
:project-path="projectPath"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-6 frame added">
|
||||
<image-viewer
|
||||
:path="newPath"
|
||||
:project-path="projectPath"
|
||||
/>
|
||||
</div>
|
||||
<div class="two-up view">
|
||||
<image-viewer
|
||||
:path="oldPath"
|
||||
:render-info="true"
|
||||
inner-css-classes="frame deleted"
|
||||
class="wrap"
|
||||
/>
|
||||
<image-viewer
|
||||
:path="newPath"
|
||||
:render-info="true"
|
||||
:inner-css-classes="['frame', 'added']"
|
||||
class="wrap"
|
||||
>
|
||||
<slot
|
||||
slot="image-overlay"
|
||||
name="image-overlay"
|
||||
>
|
||||
</slot>
|
||||
</image-viewer>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -8,9 +8,6 @@ import { diffModes, imageViewMode } from '../constants';
|
|||
export default {
|
||||
components: {
|
||||
ImageViewer,
|
||||
TwoUpViewer,
|
||||
SwipeViewer,
|
||||
OnionSkinViewer,
|
||||
},
|
||||
props: {
|
||||
diffMode: {
|
||||
|
@ -25,17 +22,32 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mode: imageViewMode.twoup,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageViewComponent() {
|
||||
switch (this.mode) {
|
||||
case imageViewMode.twoup:
|
||||
return TwoUpViewer;
|
||||
case imageViewMode.swipe:
|
||||
return SwipeViewer;
|
||||
case imageViewMode.onion:
|
||||
return OnionSkinViewer;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
isNew() {
|
||||
return this.diffMode === diffModes.new;
|
||||
},
|
||||
imagePath() {
|
||||
return this.isNew ? this.newPath : this.oldPath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeMode(newMode) {
|
||||
this.mode = newMode;
|
||||
|
@ -52,15 +64,16 @@ export default {
|
|||
v-if="diffMode === $options.diffModes.replaced"
|
||||
class="diff-viewer">
|
||||
<div class="image js-replaced-image">
|
||||
<two-up-viewer
|
||||
v-if="mode === $options.imageViewMode.twoup"
|
||||
v-bind="$props"/>
|
||||
<swipe-viewer
|
||||
v-else-if="mode === $options.imageViewMode.swipe"
|
||||
v-bind="$props"/>
|
||||
<onion-skin-viewer
|
||||
v-else-if="mode === $options.imageViewMode.onion"
|
||||
v-bind="$props"/>
|
||||
<component
|
||||
:is="imageViewComponent"
|
||||
v-bind="$props"
|
||||
>
|
||||
<slot
|
||||
slot="image-overlay"
|
||||
name="image-overlay"
|
||||
>
|
||||
</slot>
|
||||
</component>
|
||||
</div>
|
||||
<div class="view-modes">
|
||||
<ul class="view-modes-menu">
|
||||
|
@ -87,23 +100,27 @@ export default {
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="note-container"></div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="diffMode === $options.diffModes.new"
|
||||
class="diff-viewer added">
|
||||
<image-viewer
|
||||
:path="newPath"
|
||||
:project-path="projectPath"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="diff-viewer deleted">
|
||||
<image-viewer
|
||||
:path="oldPath"
|
||||
:project-path="projectPath"
|
||||
/>
|
||||
class="diff-viewer"
|
||||
>
|
||||
<div class="image">
|
||||
<image-viewer
|
||||
:path="imagePath"
|
||||
:inner-css-classes="['frame', {
|
||||
'added': isNew,
|
||||
'deleted': diffMode === $options.diffModes.deleted
|
||||
}]"
|
||||
>
|
||||
<slot
|
||||
v-if="isNew"
|
||||
slot="image-overlay"
|
||||
name="image-overlay"
|
||||
>
|
||||
</slot>
|
||||
</image-viewer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -99,6 +99,6 @@ export default {
|
|||
v-tooltip
|
||||
:title="tooltipText"
|
||||
:tooltip-placement="tooltipPlacement"
|
||||
>{{ username }}</span>
|
||||
>{{ username }}</span><slot name="avatar-badge"></slot>
|
||||
</gl-link>
|
||||
</template>
|
||||
|
|
|
@ -421,21 +421,13 @@
|
|||
|
||||
.diff-file-container {
|
||||
.frame.deleted {
|
||||
border: 0;
|
||||
border: 1px solid $deleted;
|
||||
background-color: inherit;
|
||||
|
||||
.image_file img {
|
||||
border: 1px solid $deleted;
|
||||
}
|
||||
}
|
||||
|
||||
.frame.added {
|
||||
border: 0;
|
||||
border: 1px solid $added;
|
||||
background-color: inherit;
|
||||
|
||||
.image_file img {
|
||||
border: 1px solid $added;
|
||||
}
|
||||
}
|
||||
|
||||
.swipe.view,
|
||||
|
@ -481,6 +473,11 @@
|
|||
bottom: -25px;
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-notes .discussion-notes {
|
||||
margin-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-content .diff-file {
|
||||
|
@ -804,7 +801,7 @@
|
|||
|
||||
// double jagged line divider
|
||||
.discussion-notes + .discussion-notes::before,
|
||||
.discussion-notes + .discussion-form::before {
|
||||
.diff-file-discussions + .discussion-form::before {
|
||||
content: '';
|
||||
position: relative;
|
||||
display: block;
|
||||
|
@ -844,6 +841,13 @@
|
|||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
.diff-file-discussions + .discussion-form::before {
|
||||
width: auto;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.notes {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -870,11 +874,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.files:not([data-can-create-note]) .frame {
|
||||
.files:not([data-can-create-note="true"]) .frame {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.frame.click-to-comment {
|
||||
.frame,
|
||||
.frame.click-to-comment,
|
||||
.btn-transparent.image-diff-overlay-add-comment {
|
||||
position: relative;
|
||||
cursor: image-url('illustrations/image_comment_light_cursor.svg')
|
||||
$image-comment-cursor-left-offset $image-comment-cursor-top-offset,
|
||||
|
@ -910,6 +916,7 @@
|
|||
|
||||
.frame .badge.badge-pill,
|
||||
.image-diff-avatar-link .badge.badge-pill,
|
||||
.user-avatar-link .badge.badge-pill,
|
||||
.notes > .badge.badge-pill {
|
||||
position: absolute;
|
||||
background-color: $blue-400;
|
||||
|
@ -944,7 +951,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.image-diff-avatar-link {
|
||||
.image-diff-avatar-link,
|
||||
.user-avatar-link {
|
||||
position: relative;
|
||||
|
||||
.badge.badge-pill,
|
||||
|
@ -1073,3 +1081,14 @@
|
|||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.image-diff-overlay,
|
||||
.image-diff-overlay-add-comment {
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Reimplemented image commenting in merge request diffs
|
||||
merge_request:
|
||||
author:
|
||||
type: added
|
|
@ -336,6 +336,9 @@ msgstr ""
|
|||
msgid "Add a table"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add image comment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add license"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1718,12 +1721,18 @@ msgstr ""
|
|||
msgid "Collapse sidebar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Comment & resolve discussion"
|
||||
msgstr ""
|
||||
|
||||
msgid "Comment & unresolve discussion"
|
||||
msgstr ""
|
||||
|
||||
msgid "Comment form position"
|
||||
msgstr ""
|
||||
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -114,10 +114,9 @@ describe 'Merge request > User creates image diff notes', :js do
|
|||
create_image_diff_note
|
||||
end
|
||||
|
||||
# TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
|
||||
xit 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
|
||||
it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
|
||||
indicator = find('.js-image-badge', match: :first)
|
||||
badge = find('.image-diff-avatar-link .badge', match: :first)
|
||||
badge = find('.user-avatar-link .badge', match: :first)
|
||||
|
||||
expect(indicator).to have_content('1')
|
||||
expect(badge).to have_content('1')
|
||||
|
@ -157,8 +156,7 @@ describe 'Merge request > User creates image diff notes', :js do
|
|||
visit project_merge_request_path(project, merge_request)
|
||||
end
|
||||
|
||||
# TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
|
||||
xit 'render diff indicators within the image frame' do
|
||||
it 'render diff indicators within the image frame' do
|
||||
diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
|
||||
|
||||
wait_for_requests
|
||||
|
@ -200,7 +198,6 @@ describe 'Merge request > User creates image diff notes', :js do
|
|||
|
||||
def create_image_diff_note
|
||||
find('.js-add-image-diff-note-button', match: :first).click
|
||||
page.all('.js-add-image-diff-note-button')[0].click
|
||||
find('.diff-content .note-textarea').native.send_keys('image diff test comment')
|
||||
click_button 'Comment'
|
||||
wait_for_requests
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
import Vue from 'vue';
|
||||
import DiffContentComponent from '~/diffs/components/diff_content.vue';
|
||||
import store from '~/mr_notes/stores';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
|
||||
import '~/behaviors/markdown/render_gfm';
|
||||
import diffFileMockData from '../mock_data/diff_file';
|
||||
import discussionsMockData from '../mock_data/diff_discussions';
|
||||
|
||||
describe('DiffContent', () => {
|
||||
const Component = Vue.extend(DiffContentComponent);
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
const store = createStore();
|
||||
store.state.notes.noteableData = {
|
||||
current_user: {
|
||||
can_create_note: false,
|
||||
},
|
||||
};
|
||||
|
||||
vm = mountComponentWithStore(Component, {
|
||||
store,
|
||||
props: {
|
||||
|
@ -46,21 +55,57 @@ describe('DiffContent', () => {
|
|||
});
|
||||
|
||||
describe('image diff', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(done => {
|
||||
vm.diffFile.newPath = GREEN_BOX_IMAGE_URL;
|
||||
vm.diffFile.newSha = 'DEF';
|
||||
vm.diffFile.oldPath = RED_BOX_IMAGE_URL;
|
||||
vm.diffFile.oldSha = 'ABC';
|
||||
vm.diffFile.viewPath = '';
|
||||
vm.diffFile.discussions = [{ ...discussionsMockData }];
|
||||
vm.$store.state.diffs.commentForms.push({
|
||||
fileHash: vm.diffFile.fileHash,
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 100,
|
||||
height: 200,
|
||||
});
|
||||
|
||||
vm.$nextTick(done);
|
||||
});
|
||||
|
||||
it('should have image diff view in place', done => {
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
|
||||
it('should have image diff view in place', () => {
|
||||
expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
|
||||
|
||||
expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1);
|
||||
expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1);
|
||||
});
|
||||
|
||||
done();
|
||||
it('renders image diff overlay', () => {
|
||||
expect(vm.$el.querySelector('.image-diff-overlay')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('renders diff file discussions', () => {
|
||||
expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
|
||||
});
|
||||
|
||||
describe('handleSaveNote', () => {
|
||||
it('dispatches handleSaveNote', () => {
|
||||
spyOn(vm.$store, 'dispatch').and.stub();
|
||||
|
||||
vm.handleSaveNote('test');
|
||||
|
||||
expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/saveDiffDiscussion', {
|
||||
note: 'test',
|
||||
formData: {
|
||||
noteableData: jasmine.anything(),
|
||||
noteableType: jasmine.anything(),
|
||||
diffFile: vm.diffFile,
|
||||
positionType: 'image',
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 100,
|
||||
height: 200,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,24 +1,90 @@
|
|||
import Vue from 'vue';
|
||||
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
|
||||
import store from '~/mr_notes/stores';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import '~/behaviors/markdown/render_gfm';
|
||||
import discussionsMockData from '../mock_data/diff_discussions';
|
||||
|
||||
describe('DiffDiscussions', () => {
|
||||
let component;
|
||||
let vm;
|
||||
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
|
||||
|
||||
beforeEach(() => {
|
||||
component = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
|
||||
function createComponent(props = {}) {
|
||||
const store = createStore();
|
||||
|
||||
vm = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
|
||||
discussions: getDiscussionsMockData(),
|
||||
...props,
|
||||
}).$mount();
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('should have notes list', () => {
|
||||
const { $el } = component;
|
||||
createComponent();
|
||||
|
||||
expect($el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
|
||||
expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('image commenting', () => {
|
||||
it('renders collapsible discussion button', () => {
|
||||
createComponent({ shouldCollapseDiscussions: true });
|
||||
|
||||
expect(vm.$el.querySelector('.js-diff-notes-toggle')).not.toBe(null);
|
||||
expect(vm.$el.querySelector('.js-diff-notes-toggle svg')).not.toBe(null);
|
||||
expect(vm.$el.querySelector('.js-diff-notes-toggle').classList).toContain(
|
||||
'diff-notes-collapse',
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches toggleDiscussion when clicking collapse button', () => {
|
||||
createComponent({ shouldCollapseDiscussions: true });
|
||||
|
||||
spyOn(vm.$store, 'dispatch').and.stub();
|
||||
|
||||
vm.$el.querySelector('.js-diff-notes-toggle').click();
|
||||
|
||||
expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
|
||||
discussionId: vm.discussions[0].id,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders expand button when discussion is collapsed', done => {
|
||||
createComponent({ shouldCollapseDiscussions: true });
|
||||
|
||||
vm.discussions[0].expanded = false;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.js-diff-notes-toggle').textContent.trim()).toBe('1');
|
||||
expect(vm.$el.querySelector('.js-diff-notes-toggle').className).toContain(
|
||||
'btn-transparent badge badge-pill',
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('hides discussion when collapsed', done => {
|
||||
createComponent({ shouldCollapseDiscussions: true });
|
||||
|
||||
vm.discussions[0].expanded = false;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.note-discussion').style.display).toBe('none');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders badge on avatar', () => {
|
||||
createComponent({ renderAvatarBadge: true, discussions: [{ ...discussionsMockData }] });
|
||||
|
||||
expect(vm.$el.querySelector('.user-avatar-link .badge-pill')).not.toBe(null);
|
||||
expect(vm.$el.querySelector('.user-avatar-link .badge-pill').textContent.trim()).toBe('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
import Vue from 'vue';
|
||||
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import { imageDiffDiscussions } from '../mock_data/diff_discussions';
|
||||
|
||||
describe('Diffs image diff overlay component', () => {
|
||||
const dimensions = {
|
||||
width: 100,
|
||||
height: 200,
|
||||
};
|
||||
let Component;
|
||||
let vm;
|
||||
|
||||
function createComponent(props = {}, extendStore = () => {}) {
|
||||
const store = createStore();
|
||||
|
||||
extendStore(store);
|
||||
|
||||
vm = createComponentWithStore(Component, store, {
|
||||
discussions: [...imageDiffDiscussions],
|
||||
fileHash: 'ABC',
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
Component = Vue.extend(ImageDiffOverlay);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders comment badges', () => {
|
||||
createComponent();
|
||||
spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
|
||||
vm.$mount();
|
||||
|
||||
expect(vm.$el.querySelectorAll('.js-image-badge').length).toBe(2);
|
||||
});
|
||||
|
||||
it('renders index of discussion in badge', () => {
|
||||
createComponent();
|
||||
spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
|
||||
vm.$mount();
|
||||
|
||||
expect(vm.$el.querySelectorAll('.js-image-badge')[0].textContent.trim()).toBe('1');
|
||||
expect(vm.$el.querySelectorAll('.js-image-badge')[1].textContent.trim()).toBe('2');
|
||||
});
|
||||
|
||||
it('renders icon when showCommentIcon is true', () => {
|
||||
createComponent({ showCommentIcon: true });
|
||||
spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
|
||||
vm.$mount();
|
||||
|
||||
expect(vm.$el.querySelector('.js-image-badge svg')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('sets badge comment positions', () => {
|
||||
createComponent();
|
||||
spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
|
||||
vm.$mount();
|
||||
|
||||
expect(vm.$el.querySelectorAll('.js-image-badge')[0].style.left).toBe('10px');
|
||||
expect(vm.$el.querySelectorAll('.js-image-badge')[0].style.top).toBe('10px');
|
||||
|
||||
expect(vm.$el.querySelectorAll('.js-image-badge')[1].style.left).toBe('5px');
|
||||
expect(vm.$el.querySelectorAll('.js-image-badge')[1].style.top).toBe('5px');
|
||||
});
|
||||
|
||||
it('renders single badge for discussion object', () => {
|
||||
createComponent({
|
||||
discussions: {
|
||||
...imageDiffDiscussions[0],
|
||||
},
|
||||
});
|
||||
spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
|
||||
vm.$mount();
|
||||
|
||||
expect(vm.$el.querySelectorAll('.js-image-badge').length).toBe(1);
|
||||
});
|
||||
|
||||
it('dispatches openDiffFileCommentForm when clicking overlay', () => {
|
||||
createComponent({ canComment: true });
|
||||
spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
|
||||
vm.$mount();
|
||||
|
||||
spyOn(vm.$store, 'dispatch').and.stub();
|
||||
|
||||
vm.$el.querySelector('.js-add-image-diff-note-button').click();
|
||||
|
||||
expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/openDiffFileCommentForm', {
|
||||
fileHash: 'ABC',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 200,
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle discussion', () => {
|
||||
it('disables buttons when shouldToggleDiscussion is false', () => {
|
||||
createComponent({ shouldToggleDiscussion: false });
|
||||
spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
|
||||
vm.$mount();
|
||||
|
||||
expect(vm.$el.querySelector('.js-image-badge').hasAttribute('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('dispatches toggleDiscussion when clicking image badge', () => {
|
||||
createComponent();
|
||||
spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
|
||||
vm.$mount();
|
||||
|
||||
spyOn(vm.$store, 'dispatch').and.stub();
|
||||
|
||||
vm.$el.querySelector('.js-image-badge').click();
|
||||
|
||||
expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', { discussionId: '1' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('comment form', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({}, store => {
|
||||
store.state.diffs.commentForms.push({
|
||||
fileHash: 'ABC',
|
||||
x: 20,
|
||||
y: 10,
|
||||
});
|
||||
});
|
||||
spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
|
||||
vm.$mount();
|
||||
});
|
||||
|
||||
it('renders comment form badge', () => {
|
||||
expect(vm.$el.querySelector('.comment-indicator')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('sets comment form badge position', () => {
|
||||
expect(vm.$el.querySelector('.comment-indicator').style.left).toBe('20px');
|
||||
expect(vm.$el.querySelector('.comment-indicator').style.top).toBe('10px');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -492,3 +492,24 @@ export default {
|
|||
image_diff_html:
|
||||
'<div class="image js-replaced-image" data="">\n<div class="two-up view">\n<div class="wrap">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n<div class="wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n</div>\n<div class="swipe view hide">\n<div class="swipe-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="swipe-wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n</div>\n<span class="swipe-bar">\n<span class="top-handle"></span>\n<span class="bottom-handle"></span>\n</span>\n</div>\n</div>\n<div class="onion-skin view hide">\n<div class="onion-skin-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<div class="controls">\n<div class="transparent"></div>\n<div class="drag-track">\n<div class="dragger" style="left: 0px;"></div>\n</div>\n<div class="opaque"></div>\n</div>\n</div>\n</div>\n</div>\n<div class="view-modes hide">\n<ul class="view-modes-menu">\n<li class="two-up" data-mode="two-up">2-up</li>\n<li class="swipe" data-mode="swipe">Swipe</li>\n<li class="onion-skin" data-mode="onion-skin">Onion skin</li>\n</ul>\n</div>\n',
|
||||
};
|
||||
|
||||
export const imageDiffDiscussions = [
|
||||
{
|
||||
id: '1',
|
||||
position: {
|
||||
x: 10,
|
||||
y: 10,
|
||||
width: 100,
|
||||
height: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
position: {
|
||||
x: 5,
|
||||
y: 5,
|
||||
width: 100,
|
||||
height: 200,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -237,4 +237,5 @@ export default {
|
|||
},
|
||||
},
|
||||
],
|
||||
discussions: [],
|
||||
};
|
||||
|
|
|
@ -218,6 +218,7 @@ describe('DiffsStoreActions', () => {
|
|||
],
|
||||
};
|
||||
const singleDiscussion = {
|
||||
id: '1',
|
||||
fileHash: 'ABC',
|
||||
line_code: 'ABC_1_1',
|
||||
};
|
||||
|
@ -230,6 +231,7 @@ describe('DiffsStoreActions', () => {
|
|||
{
|
||||
type: types.REMOVE_LINE_DISCUSSIONS_FOR_FILE,
|
||||
payload: {
|
||||
id: '1',
|
||||
fileHash: 'ABC',
|
||||
lineCode: 'ABC_1_1',
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import DiffWithNote from '~/notes/components/diff_with_note.vue';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import createStore from '~/notes/stores';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import { mountComponentWithStore } from 'spec/helpers';
|
||||
|
||||
const discussionFixture = 'merge_requests/diff_discussion.json';
|
||||
|
|
|
@ -47,7 +47,7 @@ describe('ContentViewer', () => {
|
|||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
|
||||
expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
|
||||
|
||||
done();
|
||||
});
|
||||
|
|
|
@ -30,11 +30,11 @@ describe('DiffViewer', () => {
|
|||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
|
||||
expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
|
||||
`//raw/DEF/${RED_BOX_IMAGE_URL}`,
|
||||
);
|
||||
|
||||
expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
|
||||
expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
|
||||
`//raw/ABC/${GREEN_BOX_IMAGE_URL}`,
|
||||
);
|
||||
|
||||
|
|
|
@ -52,13 +52,9 @@ describe('ImageDiffViewer', () => {
|
|||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
|
||||
GREEN_BOX_IMAGE_URL,
|
||||
);
|
||||
expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
|
||||
|
||||
expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
|
||||
RED_BOX_IMAGE_URL,
|
||||
);
|
||||
expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
|
||||
|
||||
expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up');
|
||||
expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe(
|
||||
|
@ -81,9 +77,7 @@ describe('ImageDiffViewer', () => {
|
|||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
|
||||
GREEN_BOX_IMAGE_URL,
|
||||
);
|
||||
expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -97,9 +91,7 @@ describe('ImageDiffViewer', () => {
|
|||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
|
||||
RED_BOX_IMAGE_URL,
|
||||
);
|
||||
expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
|
||||
|
||||
done();
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue