Commit 43ad159c authored by Phil Hughes's avatar Phil Hughes

Merge branch 'jdb/refactor-diff-html-tables-take3' into 'master'

Jdb/refactor diff html tables to use css grid

See merge request gitlab-org/gitlab!44974
parents a5f66310 ab6b1633
<script>
import DraftNote from './draft_note.vue';
export default {
components: {
DraftNote,
},
props: {
draft: {
type: Object,
required: true,
},
diffFile: {
type: Object,
required: true,
},
line: {
type: Object,
required: false,
default: null,
},
},
};
</script>
<template>
<tr class="notes_holder js-temp-notes-holder">
<td class="notes-content" colspan="4">
<div class="content"><draft-note :draft="draft" :diff-file="diffFile" :line="line" /></div>
</td>
</tr>
</template>
<script>
import { mapGetters } from 'vuex';
import DraftNote from './draft_note.vue';
export default {
components: {
DraftNote,
},
props: {
line: {
type: Object,
required: true,
},
diffFileContentSha: {
type: String,
required: true,
},
},
computed: {
...mapGetters('batchComments', ['draftForLine']),
className() {
return this.leftDraft > 0 || this.rightDraft > 0 ? '' : 'js-temp-notes-holder';
},
leftDraft() {
return this.draftForLine(this.diffFileContentSha, this.line, 'left');
},
rightDraft() {
return this.draftForLine(this.diffFileContentSha, this.line, 'right');
},
},
};
</script>
<template>
<tr :class="className" class="notes_holder">
<td class="notes_line old"></td>
<td class="notes-content parallel old" colspan="2">
<div v-if="leftDraft.isDraft" class="content">
<draft-note :draft="leftDraft" :line="line.left" />
</div>
</td>
<td class="notes_line new"></td>
<td class="notes-content parallel new" colspan="2">
<div v-if="rightDraft.isDraft" class="content">
<draft-note :draft="rightDraft" :line="line.right" />
</div>
</td>
</tr>
</template>
...@@ -29,18 +29,10 @@ export default { ...@@ -29,18 +29,10 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
}, linePosition: {
computed: { type: String,
className() { required: false,
return this.line.discussions.length ? '' : 'js-temp-notes-holder'; default: '',
},
shouldRender() {
if (this.line.hasForm) return true;
if (!this.line.discussions || !this.line.discussions.length) {
return false;
}
return this.line.discussionsExpanded;
}, },
}, },
methods: { methods: {
...@@ -50,22 +42,18 @@ export default { ...@@ -50,22 +42,18 @@ export default {
</script> </script>
<template> <template>
<tr v-if="shouldRender" :class="className" class="notes_holder">
<td class="notes-content" colspan="4">
<div class="content"> <div class="content">
<diff-discussions <diff-discussions
v-if="line.discussions.length" v-if="line.renderDiscussion"
:line="line" :line="line"
:discussions="line.discussions" :discussions="line.discussions"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
<diff-discussion-reply <diff-discussion-reply
v-if="!hasDraft" v-if="!hasDraft"
:has-form="line.hasForm" :has-form="line.hasCommentForm"
:render-reply-placeholder="Boolean(line.discussions.length)" :render-reply-placeholder="Boolean(line.discussions.length)"
@showNewDiscussionForm=" @showNewDiscussionForm="showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash })"
showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash })
"
> >
<template #form> <template #form>
<diff-line-note-form <diff-line-note-form
...@@ -73,10 +61,9 @@ export default { ...@@ -73,10 +61,9 @@ export default {
:line="line" :line="line"
:note-target-line="line" :note-target-line="line"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:line-position="linePosition"
/> />
</template> </template>
</diff-discussion-reply> </diff-discussion-reply>
</div> </div>
</td>
</tr>
</template> </template>
...@@ -10,6 +10,7 @@ import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_prev ...@@ -10,6 +10,7 @@ import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_prev
import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue'; import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
import InlineDiffView from './inline_diff_view.vue'; import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue'; import ParallelDiffView from './parallel_diff_view.vue';
import DiffView from './diff_view.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import NoteForm from '../../notes/components/note_form.vue'; import NoteForm from '../../notes/components/note_form.vue';
import ImageDiffOverlay from './image_diff_overlay.vue'; import ImageDiffOverlay from './image_diff_overlay.vue';
...@@ -18,12 +19,14 @@ import eventHub from '../../notes/event_hub'; ...@@ -18,12 +19,14 @@ import eventHub from '../../notes/event_hub';
import { IMAGE_DIFF_POSITION_TYPE } from '../constants'; import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
import { getDiffMode } from '../store/utils'; import { getDiffMode } from '../store/utils';
import { diffViewerModes } from '~/ide/constants'; import { diffViewerModes } from '~/ide/constants';
import { mapInline, mapParallel } from './diff_row_utils';
export default { export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
InlineDiffView, InlineDiffView,
ParallelDiffView, ParallelDiffView,
DiffView,
DiffViewer, DiffViewer,
NoteForm, NoteForm,
DiffDiscussions, DiffDiscussions,
...@@ -83,6 +86,19 @@ export default { ...@@ -83,6 +86,19 @@ export default {
author() { author() {
return this.getUserData; return this.getUserData;
}, },
mappedLines() {
if (this.glFeatures.unifiedDiffLines && this.glFeatures.unifiedDiffComponents) {
return this.diffLines(this.diffFile, true).map(mapParallel(this)) || [];
}
// TODO: Everything below this line can be deleted when unifiedDiffComponents FF is removed
if (this.isInlineView) {
return this.diffFile.highlighted_diff_lines.map(mapInline(this));
}
return this.glFeatures.unifiedDiffLines
? this.diffLines(this.diffFile).map(mapParallel(this))
: this.diffFile.parallel_diff_lines.map(mapParallel(this)) || [];
},
}, },
updated() { updated() {
this.$nextTick(() => { this.$nextTick(() => {
...@@ -113,19 +129,28 @@ export default { ...@@ -113,19 +129,28 @@ export default {
<template> <template>
<div class="diff-content"> <div class="diff-content">
<div class="diff-viewer"> <div class="diff-viewer">
<template v-if="isTextFile"> <template
v-if="isTextFile && glFeatures.unifiedDiffLines && glFeatures.unifiedDiffComponents"
>
<diff-view
:diff-file="diffFile"
:diff-lines="mappedLines"
:help-page-path="helpPagePath"
:inline="isInlineView"
/>
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
</template>
<template v-else-if="isTextFile">
<inline-diff-view <inline-diff-view
v-if="isInlineView" v-if="isInlineView"
:diff-file="diffFile" :diff-file="diffFile"
:diff-lines="diffFile.highlighted_diff_lines" :diff-lines="mappedLines"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
<parallel-diff-view <parallel-diff-view
v-else-if="isParallelView" v-else-if="isParallelView"
:diff-file="diffFile" :diff-file="diffFile"
:diff-lines=" :diff-lines="mappedLines"
glFeatures.unifiedDiffLines ? diffLines(diffFile) : diffFile.parallel_diff_lines || []
"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" /> <gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
......
...@@ -54,11 +54,6 @@ export default { ...@@ -54,11 +54,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
colspan: {
type: Number,
required: false,
default: 4,
},
}, },
computed: { computed: {
...mapState({ ...mapState({
...@@ -231,7 +226,6 @@ export default { ...@@ -231,7 +226,6 @@ export default {
</script> </script>
<template> <template>
<td :colspan="colspan" class="text-center gl-font-regular">
<div class="content js-line-expansion-content"> <div class="content js-line-expansion-content">
<a <a
v-if="canExpandDown" v-if="canExpandDown"
...@@ -254,5 +248,4 @@ export default { ...@@ -254,5 +248,4 @@ export default {
<span>{{ $options.i18n.showMore }}</span> <span>{{ $options.i18n.showMore }}</span>
</a> </a>
</div> </div>
</td>
</template> </template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default {
components: {
GlIcon,
DiffGutterAvatars,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
props: {
fileHash: {
type: String,
required: true,
},
filePath: {
type: String,
required: true,
},
line: {
type: Object,
required: true,
},
isCommented: {
type: Boolean,
required: false,
default: false,
},
inline: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapGetters('diffs', ['fileLineCoverage']),
...mapGetters(['isLoggedIn']),
...mapState({
isHighlighted(state) {
const line = this.line.left?.line_code ? this.line.left : this.line.right;
return utils.isHighlighted(state, line, this.isCommented);
},
}),
classNameMap() {
return {
[CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: true,
};
},
parallelViewLeftLineType() {
return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
},
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
},
classNameMapCellLeft() {
return utils.classNameMapCell(this.line.left, this.isHighlighted, this.isLoggedIn);
},
classNameMapCellRight() {
return utils.classNameMapCell(this.line.right, this.isHighlighted, this.isLoggedIn);
},
addCommentTooltipLeft() {
return utils.addCommentTooltip(this.line.left);
},
addCommentTooltipRight() {
return utils.addCommentTooltip(this.line.right);
},
shouldRenderCommentButton() {
return (
this.isLoggedIn &&
!this.line.isContextLineLeft &&
!this.line.isMetaLineLeft &&
!this.line.hasDiscussionsLeft &&
!this.line.hasDiscussionsRight
);
},
},
mounted() {
this.scrollToLineIfNeededParallel(this.line);
},
methods: {
...mapActions('diffs', [
'scrollToLineIfNeededParallel',
'showCommentForm',
'setHighlightedRow',
'toggleLineDiscussions',
]),
// Prevent text selecting on both sides of parallel diff view
// Backport of the same code from legacy diff notes.
handleParallelLineMouseDown(e) {
const line = e.currentTarget;
const table = line.closest('.diff-table');
table.classList.remove('left-side-selected', 'right-side-selected');
const [lineClass] = ['left-side', 'right-side'].filter(name => line.classList.contains(name));
if (lineClass) {
table.classList.add(`${lineClass}-selected`);
}
},
handleCommentButton(line) {
this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash });
},
},
};
</script>
<template>
<div :class="classNameMap" class="diff-grid-row diff-tr line_holder">
<div class="diff-grid-left left-side">
<template v-if="line.left">
<div
:class="classNameMapCellLeft"
data-testid="leftLineNumber"
class="diff-td diff-line-num old_line"
>
<span
v-if="shouldRenderCommentButton"
v-gl-tooltip
data-testid="leftCommentButton"
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltipLeft"
>
<button
type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
:disabled="line.left.commentsDisabled"
@click="handleCommentButton(line.left)"
>
<gl-icon :size="12" name="comment" />
</button>
</span>
<a
v-if="line.left.old_line"
:data-linenumber="line.left.old_line"
:href="line.lineHrefOld"
@click="setHighlightedRow(line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="line.hasDiscussionsLeft"
:discussions="line.left.discussions"
:discussions-expanded="line.left.discussionsExpanded"
data-testid="leftDiscussions"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.left.line_code,
fileHash,
expanded: !line.left.discussionsExpanded,
})
"
/>
</div>
<div :class="classNameMapCellLeft" class="diff-td diff-line-num old_line">
<a
v-if="line.left.old_line"
:data-linenumber="line.left.old_line"
:href="line.lineHrefOld"
@click="setHighlightedRow(line.lineCode)"
>
</a>
</div>
<div :class="parallelViewLeftLineType" class="diff-td line-coverage left-side"></div>
<div
:id="line.left.line_code"
:key="line.left.line_code"
v-safe-html="line.left.rich_text"
:class="parallelViewLeftLineType"
class="diff-td line_content with-coverage parallel left-side"
data-testid="leftContent"
@mousedown="handleParallelLineMouseDown"
></div>
</template>
<template v-else>
<div data-testid="leftEmptyCell" class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td line-coverage left-side empty-cell"></div>
<div class="diff-td line_content with-coverage parallel left-side empty-cell"></div>
</template>
</div>
<div
v-if="!inline || (line.right && Boolean(line.right.type))"
class="diff-grid-right right-side"
>
<template v-if="line.right">
<div
:class="classNameMapCellRight"
data-testid="rightLineNumber"
class="diff-td diff-line-num new_line"
>
<span
v-if="shouldRenderCommentButton"
v-gl-tooltip
data-testid="rightCommentButton"
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltipRight"
>
<button
type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
:disabled="line.right.commentsDisabled"
@click="handleCommentButton(line.right)"
>
<gl-icon :size="12" name="comment" />
</button>
</span>
<a
v-if="line.right.new_line"
:data-linenumber="line.right.new_line"
:href="line.lineHrefNew"
@click="setHighlightedRow(line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="line.hasDiscussionsRight"
:discussions="line.right.discussions"
:discussions-expanded="line.right.discussionsExpanded"
data-testid="rightDiscussions"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.right.line_code,
fileHash,
expanded: !line.right.discussionsExpanded,
})
"
/>
</div>
<div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
<a
v-if="line.right.new_line"
:data-linenumber="line.right.new_line"
:href="line.lineHrefNew"
@click="setHighlightedRow(line.lineCode)"
>
</a>
</div>
<div
v-gl-tooltip.hover
:title="coverageState.text"
:class="[line.right.type, coverageState.class, { hll: isHighlighted }]"
class="diff-td line-coverage right-side"
></div>
<div
:id="line.right.line_code"
:key="line.right.rich_text"
v-safe-html="line.right.rich_text"
:class="[
line.right.type,
{
hll: isHighlighted,
},
]"
class="diff-td line_content with-coverage parallel right-side"
@mousedown="handleParallelLineMouseDown"
></div>
</template>
<template v-else>
<div data-testid="rightEmptyCell" class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td line-coverage right-side empty-cell"></div>
<div class="diff-td line_content with-coverage parallel right-side empty-cell"></div>
</template>
</div>
</div>
</template>
...@@ -83,3 +83,76 @@ export const parallelViewLeftLineType = (line, hll) => { ...@@ -83,3 +83,76 @@ export const parallelViewLeftLineType = (line, hll) => {
export const shouldShowCommentButton = (hover, context, meta, discussions) => { export const shouldShowCommentButton = (hover, context, meta, discussions) => {
return hover && !context && !meta && !discussions; return hover && !context && !meta && !discussions;
}; };
export const mapParallel = content => line => {
let { left, right } = line;
// Dicussions/Comments
const hasExpandedDiscussionOnLeft =
left?.discussions?.length > 0 ? left?.discussionsExpanded : false;
const hasExpandedDiscussionOnRight =
right?.discussions?.length > 0 ? right?.discussionsExpanded : false;
const renderCommentRow =
hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight || left?.hasForm || right?.hasForm;
if (left) {
left = {
...left,
renderDiscussion: hasExpandedDiscussionOnLeft,
hasDraft: content.hasParallelDraftLeft(content.diffFile.file_hash, line),
lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'left'),
hasCommentForm: left.hasForm,
};
}
if (right) {
right = {
...right,
renderDiscussion: Boolean(hasExpandedDiscussionOnRight && right.type),
hasDraft: content.hasParallelDraftRight(content.diffFile.file_hash, line),
lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'right'),
hasCommentForm: Boolean(right.hasForm && right.type),
};
}
return {
...line,
left,
right,
isMatchLineLeft: isMatchLine(left?.type),
isMatchLineRight: isMatchLine(right?.type),
isContextLineLeft: isContextLine(left?.type),
isContextLineRight: isContextLine(right?.type),
hasDiscussionsLeft: hasDiscussions(left),
hasDiscussionsRight: hasDiscussions(right),
lineHrefOld: lineHref(left),
lineHrefNew: lineHref(right),
lineCode: lineCode(line),
isMetaLineLeft: isMetaLine(left?.type),
isMetaLineRight: isMetaLine(right?.type),
draftRowClasses: left?.lineDraft > 0 || right?.lineDraft > 0 ? '' : 'js-temp-notes-holder',
renderCommentRow,
commentRowClasses: hasDiscussions(left) || hasDiscussions(right) ? '' : 'js-temp-notes-holder',
};
};
// TODO: Delete this function when unifiedDiffComponents FF is removed
export const mapInline = content => line => {
// Discussions/Comments
const renderCommentRow = line.hasForm || (line.discussions?.length && line.discussionsExpanded);
return {
...line,
renderDiscussion: Boolean(line.discussions?.length),
isMatchLine: isMatchLine(line.type),
commentRowClasses: line.discussions?.length ? '' : 'js-temp-notes-holder',
renderCommentRow,
hasDraft: content.shouldRenderDraftRow(content.diffFile.file_hash, line),
hasCommentForm: line.hasForm,
isMetaLine: isMetaLine(line.type),
isContextLine: isContextLine(line.type),
hasDiscussions: hasDiscussions(line),
lineHref: lineHref(line),
lineCode: lineCode(line),
};
};
<script>
import { mapGetters, mapState } from 'vuex';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import DiffRow from './diff_row.vue';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
export default {
components: {
DiffExpansionCell,
DiffRow,
DiffCommentCell,
DraftNote,
},
mixins: [draftCommentsMixin],
props: {
diffFile: {
type: Object,
required: true,
},
diffLines: {
type: Array,
required: true,
},
helpPagePath: {
type: String,
required: false,
default: '',
},
inline: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapGetters('diffs', ['commitId']),
...mapState({
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
}),
diffLinesLength() {
return this.diffLines.length;
},
commentedLines() {
return getCommentedLines(
this.selectedCommentPosition || this.selectedCommentPositionHover,
this.diffLines,
);
},
},
userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
<div
:class="[$options.userColorScheme, { inline }]"
:data-commit-id="commitId"
class="diff-grid diff-table code diff-wrap-lines js-syntax-highlight text-file"
>
<template v-for="(line, index) in diffLines">
<div
v-if="line.isMatchLineLeft || line.isMatchLineRight"
:key="`expand-${index}`"
class="diff-tr line_expansion match"
>
<div class="diff-td text-center gl-font-regular">
<diff-expansion-cell
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line.left"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
</div>
</div>
<diff-row
v-if="!line.isMatchLineLeft && !line.isMatchLineRight"
:key="line.line_code"
:file-hash="diffFile.file_hash"
:file-path="diffFile.file_path"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
:inline="inline"
/>
<div
v-if="line.renderCommentRow"
:key="`dcr-${line.line_code || index}`"
:class="line.commentRowClasses"
class="diff-grid-comments diff-tr notes_holder"
>
<div
v-if="!inline || (line.left && line.left.discussions.length)"
class="diff-td notes-content parallel old"
>
<diff-comment-cell
v-if="line.left"
:line="line.left"
:diff-file-hash="diffFile.file_hash"
:help-page-path="helpPagePath"
:has-draft="line.left.hasDraft"
line-position="left"
/>
</div>
<div
v-if="!inline || (line.right && line.right.discussions.length)"
class="diff-td notes-content parallel new"
>
<diff-comment-cell
v-if="line.right"
:line="line.right"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
:help-page-path="helpPagePath"
:has-draft="line.right.hasDraft"
line-position="right"
/>
</div>
</div>
<div
v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)"
:key="`drafts-${index}`"
:class="line.draftRowClasses"
class="diff-grid-drafts diff-tr notes_holder"
>
<div
v-if="!inline || (line.left && line.left.lineDraft.isDraft)"
class="diff-td notes-content parallel old"
>
<div v-if="line.left && line.left.lineDraft.isDraft" class="content">
<draft-note :draft="line.left.lineDraft" :line="line.left" />
</div>
</div>
<div
v-if="!inline || (line.right && line.right.lineDraft.isDraft)"
class="diff-td notes-content parallel new"
>
<div v-if="line.right && line.right.lineDraft.isDraft" class="content">
<draft-note :draft="line.right.lineDraft" :line="line.right" />
</div>
</div>
</div>
</template>
</div>
</template>
<script>
import DiffExpansionCell from './diff_expansion_cell.vue';
import { MATCH_LINE_TYPE } from '../constants';
export default {
components: {
DiffExpansionCell,
},
props: {
fileHash: {
type: String,
required: true,
},
contextLinesPath: {
type: String,
required: true,
},
line: {
type: Object,
required: true,
},
isTop: {
type: Boolean,
required: false,
default: false,
},
isBottom: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isMatchLine() {
return this.line.type === MATCH_LINE_TYPE;
},
},
};
</script>
<template>
<tr v-if="isMatchLine" class="line_expansion match">
<diff-expansion-cell
:file-hash="fileHash"
:context-lines-path="contextLinesPath"
:line="line"
:is-top="isTop"
:is-bottom="isBottom"
/>
</tr>
</template>
...@@ -3,7 +3,13 @@ import { mapActions, mapGetters, mapState } from 'vuex'; ...@@ -3,7 +3,13 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { CONTEXT_LINE_CLASS_NAME } from '../constants'; import { CONTEXT_LINE_CLASS_NAME } from '../constants';
import DiffGutterAvatars from './diff_gutter_avatars.vue'; import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils'; import {
isHighlighted,
shouldShowCommentButton,
shouldRenderCommentButton,
classNameMapCell,
addCommentTooltip,
} from './diff_row_utils';
export default { export default {
components: { components: {
...@@ -48,60 +54,42 @@ export default { ...@@ -48,60 +54,42 @@ export default {
...mapGetters('diffs', ['fileLineCoverage']), ...mapGetters('diffs', ['fileLineCoverage']),
...mapState({ ...mapState({
isHighlighted(state) { isHighlighted(state) {
return utils.isHighlighted(state, this.line, this.isCommented); return isHighlighted(state, this.line, this.isCommented);
}, },
}), }),
isContextLine() {
return utils.isContextLine(this.line.type);
},
classNameMap() { classNameMap() {
return [ return [
this.line.type, this.line.type,
{ {
[CONTEXT_LINE_CLASS_NAME]: this.isContextLine, [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLine,
}, },
]; ];
}, },
inlineRowId() { inlineRowId() {
return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`; return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`;
}, },
isMatchLine() {
return utils.isMatchLine(this.line.type);
},
coverageState() { coverageState() {
return this.fileLineCoverage(this.filePath, this.line.new_line); return this.fileLineCoverage(this.filePath, this.line.new_line);
}, },
isMetaLine() {
return utils.isMetaLine(this.line.type);
},
classNameMapCell() { classNameMapCell() {
return utils.classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover); return classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover);
}, },
addCommentTooltip() { addCommentTooltip() {
return utils.addCommentTooltip(this.line); return addCommentTooltip(this.line);
}, },
shouldRenderCommentButton() { shouldRenderCommentButton() {
return utils.shouldRenderCommentButton(this.isLoggedIn, true); return shouldRenderCommentButton(this.isLoggedIn, true);
}, },
shouldShowCommentButton() { shouldShowCommentButton() {
return utils.shouldShowCommentButton( return shouldShowCommentButton(
this.isHover, this.isHover,
this.isContextLine, this.line.isContextLine,
this.isMetaLine, this.line.isMetaLine,
this.hasDiscussions, this.line.hasDiscussions,
); );
}, },
hasDiscussions() {
return utils.hasDiscussions(this.line);
},
lineHref() {
return utils.lineHref(this.line);
},
lineCode() {
return utils.lineCode(this.line);
},
shouldShowAvatarsOnGutter() { shouldShowAvatarsOnGutter() {
return this.hasDiscussions; return this.line.hasDiscussions;
}, },
}, },
mounted() { mounted() {
...@@ -128,7 +116,6 @@ export default { ...@@ -128,7 +116,6 @@ export default {
<template> <template>
<tr <tr
v-if="!isMatchLine"
:id="inlineRowId" :id="inlineRowId"
:class="classNameMap" :class="classNameMap"
class="line_holder" class="line_holder"
...@@ -158,8 +145,8 @@ export default { ...@@ -158,8 +145,8 @@ export default {
v-if="line.old_line" v-if="line.old_line"
ref="lineNumberRefOld" ref="lineNumberRefOld"
:data-linenumber="line.old_line" :data-linenumber="line.old_line"
:href="lineHref" :href="line.lineHref"
@click="setHighlightedRow(lineCode)" @click="setHighlightedRow(line.lineCode)"
> >
</a> </a>
<diff-gutter-avatars <diff-gutter-avatars
...@@ -167,7 +154,11 @@ export default { ...@@ -167,7 +154,11 @@ export default {
:discussions="line.discussions" :discussions="line.discussions"
:discussions-expanded="line.discussionsExpanded" :discussions-expanded="line.discussionsExpanded"
@toggleLineDiscussions=" @toggleLineDiscussions="
toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded }) toggleLineDiscussions({
lineCode: line.lineCode,
fileHash,
expanded: !line.discussionsExpanded,
})
" "
/> />
</td> </td>
...@@ -176,8 +167,8 @@ export default { ...@@ -176,8 +167,8 @@ export default {
v-if="line.new_line" v-if="line.new_line"
ref="lineNumberRefNew" ref="lineNumberRefNew"
:data-linenumber="line.new_line" :data-linenumber="line.new_line"
:href="lineHref" :href="line.lineHref"
@click="setHighlightedRow(lineCode)" @click="setHighlightedRow(line.lineCode)"
> >
</a> </a>
</td> </td>
......
...@@ -2,18 +2,18 @@ ...@@ -2,18 +2,18 @@
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import draftCommentsMixin from '~/diffs/mixins/draft_comments'; import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import InlineDraftCommentRow from '~/batch_comments/components/inline_draft_comment_row.vue'; import DraftNote from '~/batch_comments/components/draft_note.vue';
import inlineDiffTableRow from './inline_diff_table_row.vue'; import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue'; import DiffCommentCell from './diff_comment_cell.vue';
import inlineDiffExpansionRow from './inline_diff_expansion_row.vue'; import DiffExpansionCell from './diff_expansion_cell.vue';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils'; import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
export default { export default {
components: { components: {
inlineDiffCommentRow, DiffCommentCell,
inlineDiffTableRow, inlineDiffTableRow,
InlineDraftCommentRow, DraftNote,
inlineDiffExpansionRow, DiffExpansionCell,
}, },
mixins: [draftCommentsMixin, glFeatureFlagsMixin()], mixins: [draftCommentsMixin, glFeatureFlagsMixin()],
props: { props: {
...@@ -65,15 +65,19 @@ export default { ...@@ -65,15 +65,19 @@ export default {
</colgroup> </colgroup>
<tbody> <tbody>
<template v-for="(line, index) in diffLines"> <template v-for="(line, index) in diffLines">
<inline-diff-expansion-row <tr v-if="line.isMatchLine" :key="`expand-${index}`" class="line_expansion match">
:key="`expand-${index}`" <td colspan="4" class="text-center gl-font-regular">
<diff-expansion-cell
:file-hash="diffFile.file_hash" :file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path" :context-lines-path="diffFile.context_lines_path"
:line="line" :line="line"
:is-top="index === 0" :is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
/> />
</td>
</tr>
<inline-diff-table-row <inline-diff-table-row
v-if="!line.isMatchLine"
:key="`${line.line_code || index}`" :key="`${line.line_code || index}`"
:file-hash="diffFile.file_hash" :file-hash="diffFile.file_hash"
:file-path="diffFile.file_path" :file-path="diffFile.file_path"
...@@ -81,20 +85,32 @@ export default { ...@@ -81,20 +85,32 @@ export default {
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine" :is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
/> />
<inline-diff-comment-row <tr
v-if="line.renderCommentRow"
:key="`icr-${line.line_code || index}`" :key="`icr-${line.line_code || index}`"
:class="line.commentRowClasses"
class="notes_holder"
>
<td class="notes-content" colspan="4">
<diff-comment-cell
:diff-file-hash="diffFile.file_hash" :diff-file-hash="diffFile.file_hash"
:line="line" :line="line"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:has-draft="shouldRenderDraftRow(diffFile.file_hash, line) || false" :has-draft="line.hasDraft"
/> />
<inline-draft-comment-row </td>
v-if="shouldRenderDraftRow(diffFile.file_hash, line)" </tr>
:key="`draft_${index}`" <tr v-if="line.hasDraft" :key="`draft_${index}`" class="notes_holder js-temp-notes-holder">
<td class="notes-content" colspan="4">
<div class="content">
<draft-note
:draft="draftForLine(diffFile.file_hash, line)" :draft="draftForLine(diffFile.file_hash, line)"
:diff-file="diffFile" :diff-file="diffFile"
:line="line" :line="line"
/> />
</div>
</td>
</tr>
</template> </template>
</tbody> </tbody>
</table> </table>
......
<script>
import { mapActions } from 'vuex';
import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
DiffDiscussions,
DiffLineNoteForm,
DiffDiscussionReply,
},
props: {
line: {
type: Object,
required: true,
},
diffFileHash: {
type: String,
required: true,
},
lineIndex: {
type: Number,
required: true,
},
helpPagePath: {
type: String,
required: false,
default: '',
},
hasDraftLeft: {
type: Boolean,
required: false,
default: false,
},
hasDraftRight: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
hasExpandedDiscussionOnLeft() {
return this.line.left && this.line.left.discussions.length
? this.line.left.discussionsExpanded
: false;
},
hasExpandedDiscussionOnRight() {
return this.line.right && this.line.right.discussions.length
? this.line.right.discussionsExpanded
: false;
},
hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
shouldRenderDiscussionsOnLeft() {
return (
this.line.left &&
this.line.left.discussions &&
this.line.left.discussions.length &&
this.hasExpandedDiscussionOnLeft
);
},
shouldRenderDiscussionsOnRight() {
return (
this.line.right &&
this.line.right.discussions &&
this.line.right.discussions.length &&
this.hasExpandedDiscussionOnRight &&
this.line.right.type
);
},
showRightSideCommentForm() {
return this.line.right && this.line.right.type && this.line.right.hasForm;
},
showLeftSideCommentForm() {
return this.line.left && this.line.left.hasForm;
},
className() {
return (this.left && this.line.left.discussions.length > 0) ||
(this.right && this.line.right.discussions.length > 0)
? ''
: 'js-temp-notes-holder';
},
shouldRender() {
const { line } = this;
const hasDiscussion =
(line.left && line.left.discussions && line.left.discussions.length) ||
(line.right && line.right.discussions && line.right.discussions.length);
if (
hasDiscussion &&
(this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight)
) {
return true;
}
const hasCommentFormOnLeft = line.left && line.left.hasForm;
const hasCommentFormOnRight = line.right && line.right.hasForm;
return hasCommentFormOnLeft || hasCommentFormOnRight;
},
shouldRenderReplyPlaceholderOnLeft() {
return Boolean(
this.line.left && this.line.left.discussions && this.line.left.discussions.length,
);
},
shouldRenderReplyPlaceholderOnRight() {
return Boolean(
this.line.right && this.line.right.discussions && this.line.right.discussions.length,
);
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
showNewDiscussionForm(lineCode) {
this.showCommentForm({ lineCode, fileHash: this.diffFileHash });
},
},
};
</script>
<template>
<tr v-if="shouldRender" :class="className" class="notes_holder">
<td class="notes-content parallel old" colspan="3">
<div v-if="shouldRenderDiscussionsOnLeft" class="content">
<diff-discussions
:discussions="line.left.discussions"
:line="line.left"
:help-page-path="helpPagePath"
/>
</div>
<diff-discussion-reply
v-if="!hasDraftLeft"
:has-form="showLeftSideCommentForm"
:render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft"
@showNewDiscussionForm="showNewDiscussionForm(line.left.line_code)"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
:help-page-path="helpPagePath"
line-position="left"
/>
</template>
</diff-discussion-reply>
</td>
<td class="notes-content parallel new" colspan="3">
<div v-if="shouldRenderDiscussionsOnRight" class="content">
<diff-discussions
:discussions="line.right.discussions"
:line="line.right"
:help-page-path="helpPagePath"
/>
</div>
<diff-discussion-reply
v-if="!hasDraftRight"
:has-form="showRightSideCommentForm"
:render-reply-placeholder="shouldRenderReplyPlaceholderOnRight"
@showNewDiscussionForm="showNewDiscussionForm(line.right.line_code)"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
line-position="right"
/>
</template>
</diff-discussion-reply>
</td>
</tr>
</template>
<script>
import { MATCH_LINE_TYPE } from '../constants';
import DiffExpansionCell from './diff_expansion_cell.vue';
export default {
components: {
DiffExpansionCell,
},
props: {
fileHash: {
type: String,
required: true,
},
contextLinesPath: {
type: String,
required: true,
},
line: {
type: Object,
required: true,
},
isTop: {
type: Boolean,
required: false,
default: false,
},
isBottom: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isMatchLineLeft() {
return this.line.left && this.line.left.type === MATCH_LINE_TYPE;
},
isMatchLineRight() {
return this.line.right && this.line.right.type === MATCH_LINE_TYPE;
},
},
};
</script>
<template>
<tr class="line_expansion match">
<template v-if="isMatchLineLeft || isMatchLineRight">
<diff-expansion-cell
:file-hash="fileHash"
:context-lines-path="contextLinesPath"
:line="line.left"
:is-top="isTop"
:is-bottom="isBottom"
:colspan="6"
/>
</template>
</tr>
</template>
...@@ -55,27 +55,15 @@ export default { ...@@ -55,27 +55,15 @@ export default {
return utils.isHighlighted(state, line, this.isCommented); return utils.isHighlighted(state, line, this.isCommented);
}, },
}), }),
isContextLineLeft() {
return utils.isContextLine(this.line.left?.type);
},
isContextLineRight() {
return utils.isContextLine(this.line.right?.type);
},
classNameMap() { classNameMap() {
return { return {
[CONTEXT_LINE_CLASS_NAME]: this.isContextLineLeft, [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: true, [PARALLEL_DIFF_VIEW_TYPE]: true,
}; };
}, },
parallelViewLeftLineType() { parallelViewLeftLineType() {
return utils.parallelViewLeftLineType(this.line, this.isHighlighted); return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
}, },
isMatchLineLeft() {
return utils.isMatchLine(this.line.left?.type);
},
isMatchLineRight() {
return utils.isMatchLine(this.line.right?.type);
},
coverageState() { coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line); return this.fileLineCoverage(this.filePath, this.line.right.new_line);
}, },
...@@ -107,40 +95,19 @@ export default { ...@@ -107,40 +95,19 @@ export default {
shouldShowCommentButtonLeft() { shouldShowCommentButtonLeft() {
return utils.shouldShowCommentButton( return utils.shouldShowCommentButton(
this.isLeftHover, this.isLeftHover,
this.isContextLineLeft, this.line.isContextLineLeft,
this.isMetaLineLeft, this.line.isMetaLineLeft,
this.hasDiscussionsLeft, this.line.hasDiscussionsLeft,
); );
}, },
shouldShowCommentButtonRight() { shouldShowCommentButtonRight() {
return utils.shouldShowCommentButton( return utils.shouldShowCommentButton(
this.isRightHover, this.isRightHover,
this.isContextLineRight, this.line.isContextLineRight,
this.isMetaLineRight, this.line.isMetaLineRight,
this.hasDiscussionsRight, this.line.hasDiscussionsRight,
); );
}, },
hasDiscussionsLeft() {
return utils.hasDiscussions(this.line.left);
},
hasDiscussionsRight() {
return utils.hasDiscussions(this.line.right);
},
lineHrefOld() {
return utils.lineHref(this.line.left);
},
lineHrefNew() {
return utils.lineHref(this.line.right);
},
lineCode() {
return utils.lineCode(this.line);
},
isMetaLineLeft() {
return utils.isMetaLine(this.line.left?.type);
},
isMetaLineRight() {
return utils.isMetaLine(this.line.right?.type);
},
}, },
mounted() { mounted() {
this.scrollToLineIfNeededParallel(this.line); this.scrollToLineIfNeededParallel(this.line);
...@@ -203,7 +170,7 @@ export default { ...@@ -203,7 +170,7 @@ export default {
@mouseover="handleMouseMove" @mouseover="handleMouseMove"
@mouseout="handleMouseMove" @mouseout="handleMouseMove"
> >
<template v-if="line.left && !isMatchLineLeft"> <template v-if="line.left && !line.isMatchLineLeft">
<td ref="oldTd" :class="classNameMapCellLeft" class="diff-line-num old_line"> <td ref="oldTd" :class="classNameMapCellLeft" class="diff-line-num old_line">
<span <span
v-if="shouldRenderCommentButton" v-if="shouldRenderCommentButton"
...@@ -227,12 +194,12 @@ export default { ...@@ -227,12 +194,12 @@ export default {
v-if="line.left.old_line" v-if="line.left.old_line"
ref="lineNumberRefOld" ref="lineNumberRefOld"
:data-linenumber="line.left.old_line" :data-linenumber="line.left.old_line"
:href="lineHrefOld" :href="line.lineHrefOld"
@click="setHighlightedRow(lineCode)" @click="setHighlightedRow(line.lineCode)"
> >
</a> </a>
<diff-gutter-avatars <diff-gutter-avatars
v-if="hasDiscussionsLeft" v-if="line.hasDiscussionsLeft"
:discussions="line.left.discussions" :discussions="line.left.discussions"
:discussions-expanded="line.left.discussionsExpanded" :discussions-expanded="line.left.discussionsExpanded"
@toggleLineDiscussions=" @toggleLineDiscussions="
...@@ -259,7 +226,7 @@ export default { ...@@ -259,7 +226,7 @@ export default {
<td class="line-coverage left-side empty-cell"></td> <td class="line-coverage left-side empty-cell"></td>
<td class="line_content with-coverage parallel left-side empty-cell"></td> <td class="line_content with-coverage parallel left-side empty-cell"></td>
</template> </template>
<template v-if="line.right && !isMatchLineRight"> <template v-if="line.right && !line.isMatchLineRight">
<td ref="newTd" :class="classNameMapCellRight" class="diff-line-num new_line"> <td ref="newTd" :class="classNameMapCellRight" class="diff-line-num new_line">
<span <span
v-if="shouldRenderCommentButton" v-if="shouldRenderCommentButton"
...@@ -283,12 +250,12 @@ export default { ...@@ -283,12 +250,12 @@ export default {
v-if="line.right.new_line" v-if="line.right.new_line"
ref="lineNumberRefNew" ref="lineNumberRefNew"
:data-linenumber="line.right.new_line" :data-linenumber="line.right.new_line"
:href="lineHrefNew" :href="line.lineHrefNew"
@click="setHighlightedRow(lineCode)" @click="setHighlightedRow(line.lineCode)"
> >
</a> </a>
<diff-gutter-avatars <diff-gutter-avatars
v-if="hasDiscussionsRight" v-if="line.hasDiscussionsRight"
:discussions="line.right.discussions" :discussions="line.right.discussions"
:discussions-expanded="line.right.discussionsExpanded" :discussions-expanded="line.right.discussionsExpanded"
@toggleLineDiscussions=" @toggleLineDiscussions="
......
<script> <script>
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import draftCommentsMixin from '~/diffs/mixins/draft_comments'; import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import ParallelDraftCommentRow from '~/batch_comments/components/parallel_draft_comment_row.vue'; import DraftNote from '~/batch_comments/components/draft_note.vue';
import parallelDiffTableRow from './parallel_diff_table_row.vue'; import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue'; import DiffCommentCell from './diff_comment_cell.vue';
import parallelDiffExpansionRow from './parallel_diff_expansion_row.vue'; import DiffExpansionCell from './diff_expansion_cell.vue';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils'; import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
export default { export default {
components: { components: {
parallelDiffExpansionRow, DiffExpansionCell,
parallelDiffTableRow, parallelDiffTableRow,
parallelDiffCommentRow, DiffCommentCell,
ParallelDraftCommentRow, DraftNote,
}, },
mixins: [draftCommentsMixin], mixins: [draftCommentsMixin],
props: { props: {
...@@ -66,14 +66,21 @@ export default { ...@@ -66,14 +66,21 @@ export default {
</colgroup> </colgroup>
<tbody> <tbody>
<template v-for="(line, index) in diffLines"> <template v-for="(line, index) in diffLines">
<parallel-diff-expansion-row <tr
v-if="line.isMatchLineLeft || line.isMatchLineRight"
:key="`expand-${index}`" :key="`expand-${index}`"
class="line_expansion match"
>
<td colspan="6" class="text-center gl-font-regular">
<diff-expansion-cell
:file-hash="diffFile.file_hash" :file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path" :context-lines-path="diffFile.context_lines_path"
:line="line" :line="line.left"
:is-top="index === 0" :is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
/> />
</td>
</tr>
<parallel-diff-table-row <parallel-diff-table-row
:key="line.line_code" :key="line.line_code"
:file-hash="diffFile.file_hash" :file-hash="diffFile.file_hash"
...@@ -82,21 +89,53 @@ export default { ...@@ -82,21 +89,53 @@ export default {
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine" :is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
/> />
<parallel-diff-comment-row <tr
v-if="line.renderCommentRow"
:key="`dcr-${line.line_code || index}`" :key="`dcr-${line.line_code || index}`"
:line="line" :class="line.commentRowClasses"
class="notes_holder"
>
<td class="notes-content parallel old" colspan="3">
<diff-comment-cell
v-if="line.left"
:line="line.left"
:diff-file-hash="diffFile.file_hash"
:help-page-path="helpPagePath"
:has-draft="line.left.hasDraft"
line-position="left"
/>
</td>
<td class="notes-content parallel new" colspan="3">
<diff-comment-cell
v-if="line.right"
:line="line.right"
:diff-file-hash="diffFile.file_hash" :diff-file-hash="diffFile.file_hash"
:line-index="index" :line-index="index"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:has-draft-left="hasParallelDraftLeft(diffFile.file_hash, line) || false" :has-draft="line.right.hasDraft"
:has-draft-right="hasParallelDraftRight(diffFile.file_hash, line) || false" line-position="right"
/> />
<parallel-draft-comment-row </td>
</tr>
<tr
v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)" v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)"
:key="`drafts-${index}`" :key="`drafts-${index}`"
:line="line" :class="line.draftRowClasses"
:diff-file-content-sha="diffFile.file_hash" class="notes_holder"
/> >
<td class="notes_line old"></td>
<td class="notes-content parallel old" colspan="2">
<div v-if="line.left && line.left.lineDraft.isDraft" class="content">
<draft-note :draft="line.left.lineDraft" :line="line.left" />
</div>
</td>
<td class="notes_line new"></td>
<td class="notes-content parallel new" colspan="2">
<div v-if="line.right && line.right.lineDraft.isDraft" class="content">
<draft-note :draft="line.right.lineDraft" :line="line.right" />
</div>
</td>
</tr>
</template> </template>
</tbody> </tbody>
</table> </table>
......
...@@ -165,8 +165,8 @@ export const fileLineCoverage = state => (file, line) => { ...@@ -165,8 +165,8 @@ export const fileLineCoverage = state => (file, line) => {
export const currentDiffIndex = state => export const currentDiffIndex = state =>
Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId)); Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId));
export const diffLines = state => file => { export const diffLines = state => (file, unifiedDiffComponents) => {
if (state.diffViewType === INLINE_DIFF_VIEW_TYPE) { if (!unifiedDiffComponents && state.diffViewType === INLINE_DIFF_VIEW_TYPE) {
return null; return null;
} }
......
...@@ -449,6 +449,7 @@ ...@@ -449,6 +449,7 @@
} }
} }
.diff-table.code,
table.code { table.code {
width: 100%; width: 100%;
font-family: $monospace-font; font-family: $monospace-font;
...@@ -459,10 +460,12 @@ table.code { ...@@ -459,10 +460,12 @@ table.code {
table-layout: fixed; table-layout: fixed;
border-radius: 0 0 $border-radius-default $border-radius-default; border-radius: 0 0 $border-radius-default $border-radius-default;
.diff-tr:first-of-type.line_expansion > .diff-td,
tr:first-of-type.line_expansion > td { tr:first-of-type.line_expansion > td {
border-top: 0; border-top: 0;
} }
.diff-tr:nth-last-of-type(2).line_expansion > .diff-td,
tr:nth-last-of-type(2).line_expansion, tr:nth-last-of-type(2).line_expansion,
tr:last-of-type.line_expansion { tr:last-of-type.line_expansion {
> td { > td {
...@@ -470,6 +473,7 @@ table.code { ...@@ -470,6 +473,7 @@ table.code {
} }
} }
.diff-tr.line_holder .diff-td,
tr.line_holder td { tr.line_holder td {
line-height: $code-line-height; line-height: $code-line-height;
font-size: $code-font-size; font-size: $code-font-size;
...@@ -565,24 +569,95 @@ table.code { ...@@ -565,24 +569,95 @@ table.code {
} }
.line_holder:last-of-type { .line_holder:last-of-type {
.diff-td:first-child,
td:first-child { td:first-child {
border-bottom-left-radius: $border-radius-default; border-bottom-left-radius: $border-radius-default;
} }
} }
&.left-side-selected { &.left-side-selected {
.diff-td.line_content.parallel.right-side,
td.line_content.parallel.right-side { td.line_content.parallel.right-side {
user-select: none; user-select: none;
} }
} }
&.right-side-selected { &.right-side-selected {
.diff-td.line_content.parallel.left-side,
td.line_content.parallel.left-side { td.line_content.parallel.left-side {
user-select: none; user-select: none;
} }
} }
} }
// Merge request diff grid layout
.diff-grid {
.diff-grid-row {
display: grid;
grid-template-columns: 1fr 1fr;
}
.diff-grid-left,
.diff-grid-right {
display: grid;
grid-template-columns: 50px 8px 1fr;
.diff-td:nth-child(2) {
display: none;
}
}
.diff-grid-comments {
display: grid;
grid-template-columns: 1fr 1fr;
}
.diff-grid-drafts {
display: grid;
grid-template-columns: 1fr 1fr;
}
&.inline {
.diff-grid-comments {
display: grid;
grid-template-columns: 1fr;
}
.diff-grid-drafts {
display: grid;
grid-template-columns: 1fr;
}
.diff-grid-row {
grid-template-columns: 1fr;
}
.diff-grid-left,
.diff-grid-right {
grid-template-columns: 50px 50px 8px 1fr;
.diff-td:nth-child(2) {
display: block;
}
}
.diff-grid-left .old:nth-child(2) [data-linenumber],
.diff-grid-right .new:nth-child(2) [data-linenumber] {
display: inline;
}
.diff-grid-left .old:nth-child(3) [data-linenumber],
.diff-grid-right .new:nth-child(1) [data-linenumber] {
display: none;
}
}
}
// Merge request diff grid layout overrides
.diff-table.code .diff-tr.line_holder .diff-td.line_content.parallel {
width: unset;
}
.diff-stats { .diff-stats {
align-items: center; align-items: center;
padding: 0 1rem; padding: 0 1rem;
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
@mixin diff-expansion($background, $border, $link) { @mixin diff-expansion($background, $border, $link) {
background-color: $background; background-color: $background;
.diff-td,
td { td {
border-top: 1px solid $border; border-top: 1px solid $border;
border-bottom: 1px solid $border; border-bottom: 1px solid $border;
...@@ -41,3 +42,12 @@ ...@@ -41,3 +42,12 @@
border-left: 3px solid $no-coverage; border-left: 3px solid $no-coverage;
} }
} }
@mixin line-number-hover($color) {
background-color: $color;
border-color: darken($color, 5%);
a {
color: darken($color, 15%);
}
}
...@@ -125,6 +125,9 @@ $dark-il: #de935f; ...@@ -125,6 +125,9 @@ $dark-il: #de935f;
@include dark-diff-match-line; @include dark-diff-match-line;
} }
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line_content.hll:not(.empty-cell),
td.diff-line-num.hll:not(.empty-cell), td.diff-line-num.hll:not(.empty-cell),
td.line-coverage.hll:not(.empty-cell), td.line-coverage.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) { td.line_content.hll:not(.empty-cell) {
...@@ -158,15 +161,17 @@ $dark-il: #de935f; ...@@ -158,15 +161,17 @@ $dark-il: #de935f;
} }
} }
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($dark-over-bg);
}
}
.diff-line-num { .diff-line-num {
&.is-over, &.is-over,
&.hll:not(.empty-cell).is-over { &.hll:not(.empty-cell).is-over {
background-color: $dark-over-bg; @include line-number-hover($dark-over-bg);
border-color: darken($dark-over-bg, 5%);
a {
color: darken($dark-over-bg, 15%);
}
} }
} }
......
...@@ -125,6 +125,9 @@ $monokai-gi: #a6e22e; ...@@ -125,6 +125,9 @@ $monokai-gi: #a6e22e;
@include dark-diff-match-line; @include dark-diff-match-line;
} }
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line_content.hll:not(.empty-cell),
td.diff-line-num.hll:not(.empty-cell), td.diff-line-num.hll:not(.empty-cell),
td.line-coverage.hll:not(.empty-cell), td.line-coverage.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) { td.line_content.hll:not(.empty-cell) {
...@@ -158,15 +161,17 @@ $monokai-gi: #a6e22e; ...@@ -158,15 +161,17 @@ $monokai-gi: #a6e22e;
} }
} }
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($monokai-over-bg);
}
}
.diff-line-num { .diff-line-num {
&.is-over, &.is-over,
&.hll:not(.empty-cell).is-over { &.hll:not(.empty-cell).is-over {
background-color: $monokai-over-bg; @include line-number-hover($monokai-over-bg);
border-color: darken($monokai-over-bg, 5%);
a {
color: darken($monokai-over-bg, 15%);
}
} }
} }
......
...@@ -59,6 +59,13 @@ ...@@ -59,6 +59,13 @@
} }
} }
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($none-over-bg);
}
}
.diff-line-num { .diff-line-num {
&.old { &.old {
a { a {
...@@ -74,12 +81,7 @@ ...@@ -74,12 +81,7 @@
&.is-over, &.is-over,
&.hll:not(.empty-cell).is-over { &.hll:not(.empty-cell).is-over {
background-color: $none-over-bg; @include line-number-hover($none-over-bg);
border-color: darken($none-over-bg, 5%);
a {
color: darken($none-over-bg, 15%);
}
} }
&.hll:not(.empty-cell) { &.hll:not(.empty-cell) {
......
...@@ -129,6 +129,9 @@ $solarized-dark-il: #2aa198; ...@@ -129,6 +129,9 @@ $solarized-dark-il: #2aa198;
@include dark-diff-match-line; @include dark-diff-match-line;
} }
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line_content.hll:not(.empty-cell),
td.diff-line-num.hll:not(.empty-cell), td.diff-line-num.hll:not(.empty-cell),
td.line-coverage.hll:not(.empty-cell), td.line-coverage.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) { td.line_content.hll:not(.empty-cell) {
...@@ -140,6 +143,13 @@ $solarized-dark-il: #2aa198; ...@@ -140,6 +143,13 @@ $solarized-dark-il: #2aa198;
@include line-coverage-border-color($solarized-dark-coverage, $solarized-dark-no-coverage); @include line-coverage-border-color($solarized-dark-coverage, $solarized-dark-no-coverage);
} }
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($solarized-dark-over-bg);
}
}
.diff-line-num.new, .diff-line-num.new,
.line-coverage.new, .line-coverage.new,
.line_content.new { .line_content.new {
...@@ -165,12 +175,7 @@ $solarized-dark-il: #2aa198; ...@@ -165,12 +175,7 @@ $solarized-dark-il: #2aa198;
.diff-line-num { .diff-line-num {
&.is-over, &.is-over,
&.hll:not(.empty-cell).is-over { &.hll:not(.empty-cell).is-over {
background-color: $solarized-dark-over-bg; @include line-number-hover($solarized-dark-over-bg);
border-color: darken($solarized-dark-over-bg, 5%);
a {
color: darken($solarized-dark-over-bg, 15%);
}
} }
} }
......
...@@ -136,6 +136,9 @@ $solarized-light-il: #2aa198; ...@@ -136,6 +136,9 @@ $solarized-light-il: #2aa198;
@include match-line; @include match-line;
} }
.diff-td.diff-line-num.hll:not(.empty-cell),
.diff-td.line-coverage.hll:not(.empty-cell),
.diff-td.line_content.hll:not(.empty-cell),
td.diff-line-num.hll:not(.empty-cell), td.diff-line-num.hll:not(.empty-cell),
td.line-coverage.hll:not(.empty-cell), td.line-coverage.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) { td.line_content.hll:not(.empty-cell) {
...@@ -159,6 +162,13 @@ $solarized-light-il: #2aa198; ...@@ -159,6 +162,13 @@ $solarized-light-il: #2aa198;
} }
} }
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($solarized-light-over-bg);
}
}
.diff-line-num.old, .diff-line-num.old,
.line-coverage.old, .line-coverage.old,
.line_content.old { .line_content.old {
...@@ -173,12 +183,7 @@ $solarized-light-il: #2aa198; ...@@ -173,12 +183,7 @@ $solarized-light-il: #2aa198;
.diff-line-num { .diff-line-num {
&.is-over, &.is-over,
&.hll:not(.empty-cell).is-over { &.hll:not(.empty-cell).is-over {
background-color: $solarized-light-over-bg; @include line-number-hover($solarized-light-over-bg);
border-color: darken($solarized-light-over-bg, 5%);
a {
color: darken($solarized-light-over-bg, 15%);
}
} }
} }
......
...@@ -113,6 +113,13 @@ pre.code, ...@@ -113,6 +113,13 @@ pre.code,
@include match-line; @include match-line;
} }
.diff-grid-left:hover,
.diff-grid-right:hover {
.diff-line-num:not(.empty-cell) {
@include line-number-hover($white-over-bg);
}
}
.diff-line-num { .diff-line-num {
&.old { &.old {
background-color: $line-number-old; background-color: $line-number-old;
...@@ -134,12 +141,7 @@ pre.code, ...@@ -134,12 +141,7 @@ pre.code,
&.is-over, &.is-over,
&.hll:not(.empty-cell).is-over { &.hll:not(.empty-cell).is-over {
background-color: $white-over-bg; @include line-number-hover($white-over-bg);
border-color: darken($white-over-bg, 5%);
a {
color: darken($white-over-bg, 15%);
}
} }
&.hll:not(.empty-cell) { &.hll:not(.empty-cell) {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
* Note Form * Note Form
*/ */
.diff-file .diff-content { .diff-file .diff-content {
.diff-tr.line_holder:hover > .diff-td .line_note_link,
tr.line_holder:hover > td .line_note_link { tr.line_holder:hover > td .line_note_link {
opacity: 1; opacity: 1;
filter: alpha(opacity = 100); filter: alpha(opacity = 100);
......
...@@ -453,6 +453,8 @@ $note-form-margin-left: 72px; ...@@ -453,6 +453,8 @@ $note-form-margin-left: 72px;
} }
.diff-file { .diff-file {
.diff-grid-left:hover,
.diff-grid-right:hover,
.is-over { .is-over {
.add-diff-note { .add-diff-note {
display: inline-flex; display: inline-flex;
...@@ -490,6 +492,7 @@ $note-form-margin-left: 72px; ...@@ -490,6 +492,7 @@ $note-form-margin-left: 72px;
.notes_holder { .notes_holder {
font-family: $regular-font; font-family: $regular-font;
.diff-td,
td { td {
border: 1px solid $border-color; border: 1px solid $border-color;
border-left: 0; border-left: 0;
...@@ -805,6 +808,8 @@ $note-form-margin-left: 72px; ...@@ -805,6 +808,8 @@ $note-form-margin-left: 72px;
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
.diff-grid-left:hover,
.diff-grid-right:hover,
.line_holder .is-over:not(.no-comment-btn) { .line_holder .is-over:not(.no-comment-btn) {
.add-diff-note { .add-diff-note {
opacity: 1; opacity: 1;
......
...@@ -37,6 +37,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -37,6 +37,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true) push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project) push_frontend_feature_flag(:merge_request_widget_graphql, @project)
push_frontend_feature_flag(:unified_diff_lines, @project, default_enabled: true) push_frontend_feature_flag(:unified_diff_lines, @project, default_enabled: true)
push_frontend_feature_flag(:unified_diff_components, @project)
push_frontend_feature_flag(:highlight_current_diff_row, @project) push_frontend_feature_flag(:highlight_current_diff_row, @project)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project) push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true) push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
......
---
name: unified_diff_components
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44974
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/268039
type: development
group: group::source code
default_enabled: false
import { shallowMount } from '@vue/test-utils';
import DiffCommentCell from '~/diffs/components/diff_comment_cell.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import DiffDiscussionReply from '~/diffs/components/diff_discussion_reply.vue';
describe('DiffCommentCell', () => {
const createWrapper = (props = {}) => {
const { renderDiscussion, ...otherProps } = props;
const line = {
discussions: [],
renderDiscussion,
};
const diffFileHash = 'abc';
return shallowMount(DiffCommentCell, {
propsData: { line, diffFileHash, ...otherProps },
});
};
it('renders discussions if line has discussions', () => {
const wrapper = createWrapper({ renderDiscussion: true });
expect(wrapper.find(DiffDiscussions).exists()).toBe(true);
});
it('does not render discussions if line has no discussions', () => {
const wrapper = createWrapper();
expect(wrapper.find(DiffDiscussions).exists()).toBe(false);
});
it('renders discussion reply if line has no draft', () => {
const wrapper = createWrapper();
expect(wrapper.find(DiffDiscussionReply).exists()).toBe(true);
});
it('does not render discussion reply if line has draft', () => {
const wrapper = createWrapper({ hasDraft: true });
expect(wrapper.find(DiffDiscussionReply).exists()).toBe(false);
});
});
...@@ -12,6 +12,8 @@ import DiffDiscussions from '~/diffs/components/diff_discussions.vue'; ...@@ -12,6 +12,8 @@ import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants'; import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import diffFileMockData from '../mock_data/diff_file'; import diffFileMockData from '../mock_data/diff_file';
import { diffViewerModes } from '~/ide/constants'; import { diffViewerModes } from '~/ide/constants';
import { diffLines } from '~/diffs/store/getters';
import DiffView from '~/diffs/components/diff_view.vue';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -33,7 +35,7 @@ describe('DiffContent', () => { ...@@ -33,7 +35,7 @@ describe('DiffContent', () => {
diffFile: JSON.parse(JSON.stringify(diffFileMockData)), diffFile: JSON.parse(JSON.stringify(diffFileMockData)),
}; };
const createComponent = ({ props, state } = {}) => { const createComponent = ({ props, state, provide } = {}) => {
const fakeStore = new Vuex.Store({ const fakeStore = new Vuex.Store({
getters: { getters: {
getNoteableData() { getNoteableData() {
...@@ -55,6 +57,10 @@ describe('DiffContent', () => { ...@@ -55,6 +57,10 @@ describe('DiffContent', () => {
namespaced: true, namespaced: true,
getters: { getters: {
draftsForFile: () => () => true, draftsForFile: () => () => true,
draftForLine: () => () => true,
shouldRenderDraftRow: () => () => true,
hasParallelDraftLeft: () => () => true,
hasParallelDraftRight: () => () => true,
}, },
}, },
diffs: { diffs: {
...@@ -68,6 +74,7 @@ describe('DiffContent', () => { ...@@ -68,6 +74,7 @@ describe('DiffContent', () => {
isInlineView: isInlineViewGetterMock, isInlineView: isInlineViewGetterMock,
isParallelView: isParallelViewGetterMock, isParallelView: isParallelViewGetterMock,
getCommentFormForDiffFile: getCommentFormForDiffFileGetterMock, getCommentFormForDiffFile: getCommentFormForDiffFileGetterMock,
diffLines,
}, },
actions: { actions: {
saveDiffDiscussion: saveDiffDiscussionMock, saveDiffDiscussion: saveDiffDiscussionMock,
...@@ -77,6 +84,8 @@ describe('DiffContent', () => { ...@@ -77,6 +84,8 @@ describe('DiffContent', () => {
}, },
}); });
const glFeatures = provide ? { ...provide.glFeatures } : {};
wrapper = shallowMount(DiffContentComponent, { wrapper = shallowMount(DiffContentComponent, {
propsData: { propsData: {
...defaultProps, ...defaultProps,
...@@ -84,6 +93,7 @@ describe('DiffContent', () => { ...@@ -84,6 +93,7 @@ describe('DiffContent', () => {
}, },
localVue, localVue,
store: fakeStore, store: fakeStore,
provide: { glFeatures },
}); });
}; };
...@@ -112,6 +122,16 @@ describe('DiffContent', () => { ...@@ -112,6 +122,16 @@ describe('DiffContent', () => {
expect(wrapper.find(ParallelDiffView).exists()).toBe(true); expect(wrapper.find(ParallelDiffView).exists()).toBe(true);
}); });
it('should render diff view if `unifiedDiffLines` & `unifiedDiffComponents` are true', () => {
isParallelViewGetterMock.mockReturnValue(true);
createComponent({
props: { diffFile: textDiffFile },
provide: { glFeatures: { unifiedDiffLines: true, unifiedDiffComponents: true } },
});
expect(wrapper.find(DiffView).exists()).toBe(true);
});
it('renders rendering more lines loading icon', () => { it('renders rendering more lines loading icon', () => {
createComponent({ props: { diffFile: { ...textDiffFile, renderingLines: true } } }); createComponent({ props: { diffFile: { ...textDiffFile, renderingLines: true } } });
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import diffsModule from '~/diffs/store/modules';
import DiffRow from '~/diffs/components/diff_row.vue';
describe('DiffRow', () => {
const testLines = [
{
left: { old_line: 1, discussions: [] },
right: { new_line: 1, discussions: [] },
hasDiscussionsLeft: true,
hasDiscussionsRight: true,
},
{
left: {},
right: {},
isMatchLineLeft: true,
isMatchLineRight: true,
},
{},
{
left: { old_line: 1, discussions: [] },
right: { new_line: 1, discussions: [] },
},
];
const createWrapper = ({ props, state, isLoggedIn = true }) => {
const localVue = createLocalVue();
localVue.use(Vuex);
const diffs = diffsModule();
diffs.state = { ...diffs.state, ...state };
const getters = { isLoggedIn: () => isLoggedIn };
const store = new Vuex.Store({
modules: { diffs },
getters,
});
const propsData = {
fileHash: 'abc',
filePath: 'abc',
line: {},
...props,
};
return shallowMount(DiffRow, { propsData, localVue, store });
};
it('isHighlighted returns true if isCommented is true', () => {
const props = { isCommented: true };
const wrapper = createWrapper({ props });
expect(wrapper.vm.isHighlighted).toBe(true);
});
it('isHighlighted returns true given line.left', () => {
const props = {
line: {
left: {
line_code: 'abc',
},
},
};
const state = { highlightedRow: 'abc' };
const wrapper = createWrapper({ props, state });
expect(wrapper.vm.isHighlighted).toBe(true);
});
it('isHighlighted returns true given line.right', () => {
const props = {
line: {
right: {
line_code: 'abc',
},
},
};
const state = { highlightedRow: 'abc' };
const wrapper = createWrapper({ props, state });
expect(wrapper.vm.isHighlighted).toBe(true);
});
it('isHighlighted returns false given line.left', () => {
const props = {
line: {
left: {
line_code: 'abc',
},
},
};
const wrapper = createWrapper({ props });
expect(wrapper.vm.isHighlighted).toBe(false);
});
describe.each`
side
${'left'}
${'right'}
`('$side side', ({ side }) => {
it(`renders empty cells if ${side} is unavailable`, () => {
const wrapper = createWrapper({ props: { line: testLines[2] } });
expect(wrapper.find(`[data-testid="${side}LineNumber"]`).exists()).toBe(false);
expect(wrapper.find(`[data-testid="${side}EmptyCell"]`).exists()).toBe(true);
});
it('renders comment button', () => {
const wrapper = createWrapper({ props: { line: testLines[3] } });
expect(wrapper.find(`[data-testid="${side}CommentButton"]`).exists()).toBe(true);
});
it('renders avatars', () => {
const wrapper = createWrapper({ props: { line: testLines[0] } });
expect(wrapper.find(`[data-testid="${side}Discussions"]`).exists()).toBe(true);
});
});
it('renders left line numbers', () => {
const wrapper = createWrapper({ props: { line: testLines[0] } });
const lineNumber = testLines[0].left.old_line;
expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true);
});
it('renders right line numbers', () => {
const wrapper = createWrapper({ props: { line: testLines[0] } });
const lineNumber = testLines[0].right.new_line;
expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true);
});
});
...@@ -201,3 +201,76 @@ describe('shouldShowCommentButton', () => { ...@@ -201,3 +201,76 @@ describe('shouldShowCommentButton', () => {
}, },
); );
}); });
describe('mapParallel', () => {
it('should assign computed properties to the line object', () => {
const side = {
discussions: [{}],
discussionsExpanded: true,
hasForm: true,
};
const content = {
diffFile: {},
hasParallelDraftLeft: () => false,
hasParallelDraftRight: () => false,
draftForLine: () => ({}),
};
const line = { left: side, right: side };
const expectation = {
commentRowClasses: '',
draftRowClasses: 'js-temp-notes-holder',
hasDiscussionsLeft: true,
hasDiscussionsRight: true,
isContextLineLeft: false,
isContextLineRight: false,
isMatchLineLeft: false,
isMatchLineRight: false,
isMetaLineLeft: false,
isMetaLineRight: false,
};
const leftExpectation = {
renderDiscussion: true,
hasDraft: false,
lineDraft: {},
hasCommentForm: true,
};
const rightExpectation = {
renderDiscussion: false,
hasDraft: false,
lineDraft: {},
hasCommentForm: false,
};
const mapped = utils.mapParallel(content)(line);
expect(mapped).toMatchObject(expectation);
expect(mapped.left).toMatchObject(leftExpectation);
expect(mapped.right).toMatchObject(rightExpectation);
});
});
describe('mapInline', () => {
it('should assign computed properties to the line object', () => {
const content = {
diffFile: {},
shouldRenderDraftRow: () => false,
};
const line = {
discussions: [{}],
discussionsExpanded: true,
hasForm: true,
};
const expectation = {
commentRowClasses: '',
hasDiscussions: true,
isContextLine: false,
isMatchLine: false,
isMetaLine: false,
renderDiscussion: true,
hasDraft: false,
hasCommentForm: true,
};
const mapped = utils.mapInline(content)(line);
expect(mapped).toMatchObject(expectation);
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import DiffView from '~/diffs/components/diff_view.vue';
// import DraftNote from '~/batch_comments/components/draft_note.vue';
// import DiffRow from '~/diffs/components/diff_row.vue';
// import DiffCommentCell from '~/diffs/components/diff_comment_cell.vue';
// import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue';
describe('DiffView', () => {
const DiffExpansionCell = { template: `<div/>` };
const DiffRow = { template: `<div/>` };
const DiffCommentCell = { template: `<div/>` };
const DraftNote = { template: `<div/>` };
const createWrapper = props => {
const localVue = createLocalVue();
localVue.use(Vuex);
const batchComments = {
getters: {
shouldRenderDraftRow: () => false,
shouldRenderParallelDraftRow: () => () => true,
draftForLine: () => false,
draftsForFile: () => false,
hasParallelDraftLeft: () => false,
hasParallelDraftRight: () => false,
},
namespaced: true,
};
const diffs = { getters: { commitId: () => 'abc123' }, namespaced: true };
const notes = {
state: { selectedCommentPosition: null, selectedCommentPositionHover: null },
};
const store = new Vuex.Store({
modules: { diffs, notes, batchComments },
});
const propsData = {
diffFile: {},
diffLines: [],
...props,
};
const stubs = { DiffExpansionCell, DiffRow, DiffCommentCell, DraftNote };
return shallowMount(DiffView, { propsData, store, localVue, stubs });
};
it('renders a match line', () => {
const wrapper = createWrapper({ diffLines: [{ isMatchLineLeft: true }] });
expect(wrapper.find(DiffExpansionCell).exists()).toBe(true);
});
it('renders a comment row', () => {
const wrapper = createWrapper({
diffLines: [{ renderCommentRow: true, left: { lineDraft: {} } }],
});
expect(wrapper.find(DiffCommentCell).exists()).toBe(true);
});
it('renders a draft row', () => {
const wrapper = createWrapper({
diffLines: [{ renderCommentRow: true, left: { lineDraft: { isDraft: true } } }],
});
expect(wrapper.find(DraftNote).exists()).toBe(true);
});
});
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/mr_notes/stores';
import InlineDiffExpansionRow from '~/diffs/components/inline_diff_expansion_row.vue';
import diffFileMockData from '../mock_data/diff_file';
describe('InlineDiffExpansionRow', () => {
const mockData = { ...diffFileMockData };
const matchLine = mockData.highlighted_diff_lines.pop();
const createComponent = (options = {}) => {
const cmp = Vue.extend(InlineDiffExpansionRow);
const defaults = {
fileHash: mockData.file_hash,
contextLinesPath: 'contextLinesPath',
line: matchLine,
isTop: false,
isBottom: false,
};
const props = { ...defaults, ...options };
return createComponentWithStore(cmp, createStore(), props).$mount();
};
describe('template', () => {
it('should render expansion row for match lines', () => {
const vm = createComponent();
expect(vm.$el.classList.contains('line_expansion')).toBe(true);
});
});
});
...@@ -4,6 +4,7 @@ import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue'; ...@@ -4,6 +4,7 @@ import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue'; import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import diffFileMockData from '../mock_data/diff_file'; import diffFileMockData from '../mock_data/diff_file';
import discussionsMockData from '../mock_data/diff_discussions'; import discussionsMockData from '../mock_data/diff_discussions';
import { mapInline } from '~/diffs/components/diff_row_utils';
const TEST_USER_ID = 'abc123'; const TEST_USER_ID = 'abc123';
const TEST_USER = { id: TEST_USER_ID }; const TEST_USER = { id: TEST_USER_ID };
...@@ -11,7 +12,16 @@ const TEST_USER = { id: TEST_USER_ID }; ...@@ -11,7 +12,16 @@ const TEST_USER = { id: TEST_USER_ID };
describe('InlineDiffTableRow', () => { describe('InlineDiffTableRow', () => {
let wrapper; let wrapper;
let store; let store;
const thisLine = diffFileMockData.highlighted_diff_lines[0]; const mockDiffContent = {
diffFile: diffFileMockData,
shouldRenderDraftRow: jest.fn(),
hasParallelDraftLeft: jest.fn(),
hasParallelDraftRight: jest.fn(),
draftForLine: jest.fn(),
};
const applyMap = mapInline(mockDiffContent);
const thisLine = applyMap(diffFileMockData.highlighted_diff_lines[0]);
const createComponent = (props = {}, propsStore = store) => { const createComponent = (props = {}, propsStore = store) => {
wrapper = shallowMount(InlineDiffTableRow, { wrapper = shallowMount(InlineDiffTableRow, {
...@@ -132,7 +142,7 @@ describe('InlineDiffTableRow', () => { ...@@ -132,7 +142,7 @@ describe('InlineDiffTableRow', () => {
${true} | ${{ ...thisLine, type: 'old-nonewline', discussions: [] }} | ${false} ${true} | ${{ ...thisLine, type: 'old-nonewline', discussions: [] }} | ${false}
${true} | ${{ ...thisLine, discussions: [{}] }} | ${false} ${true} | ${{ ...thisLine, discussions: [{}] }} | ${false}
`('visible is $expectation - line ($line)', ({ isHover, line, expectation }) => { `('visible is $expectation - line ($line)', ({ isHover, line, expectation }) => {
createComponent({ line }); createComponent({ line: applyMap(line) });
wrapper.setData({ isHover }); wrapper.setData({ isHover });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
...@@ -148,7 +158,7 @@ describe('InlineDiffTableRow', () => { ...@@ -148,7 +158,7 @@ describe('InlineDiffTableRow', () => {
'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled', 'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled',
({ disabled, commentsDisabled }) => { ({ disabled, commentsDisabled }) => {
createComponent({ createComponent({
line: { ...thisLine, commentsDisabled }, line: applyMap({ ...thisLine, commentsDisabled }),
}); });
wrapper.setData({ isHover: true }); wrapper.setData({ isHover: true });
...@@ -177,7 +187,7 @@ describe('InlineDiffTableRow', () => { ...@@ -177,7 +187,7 @@ describe('InlineDiffTableRow', () => {
'has the correct tooltip when commentsDisabled=$commentsDisabled', 'has the correct tooltip when commentsDisabled=$commentsDisabled',
({ tooltip, commentsDisabled }) => { ({ tooltip, commentsDisabled }) => {
createComponent({ createComponent({
line: { ...thisLine, commentsDisabled }, line: applyMap({ ...thisLine, commentsDisabled }),
}); });
wrapper.setData({ isHover: true }); wrapper.setData({ isHover: true });
...@@ -216,7 +226,7 @@ describe('InlineDiffTableRow', () => { ...@@ -216,7 +226,7 @@ describe('InlineDiffTableRow', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(store, 'dispatch').mockImplementation(); jest.spyOn(store, 'dispatch').mockImplementation();
createComponent({ createComponent({
line: { ...thisLine, ...lineProps }, line: applyMap({ ...thisLine, ...lineProps }),
}); });
}); });
...@@ -268,7 +278,7 @@ describe('InlineDiffTableRow', () => { ...@@ -268,7 +278,7 @@ describe('InlineDiffTableRow', () => {
describe('with showCommentButton', () => { describe('with showCommentButton', () => {
it('renders if line has discussions', () => { it('renders if line has discussions', () => {
createComponent({ line }); createComponent({ line: applyMap(line) });
expect(findAvatars().props()).toEqual({ expect(findAvatars().props()).toEqual({
discussions: line.discussions, discussions: line.discussions,
...@@ -278,13 +288,13 @@ describe('InlineDiffTableRow', () => { ...@@ -278,13 +288,13 @@ describe('InlineDiffTableRow', () => {
it('does notrender if line has no discussions', () => { it('does notrender if line has no discussions', () => {
line.discussions = []; line.discussions = [];
createComponent({ line }); createComponent({ line: applyMap(line) });
expect(findAvatars().exists()).toEqual(false); expect(findAvatars().exists()).toEqual(false);
}); });
it('toggles line discussion', () => { it('toggles line discussion', () => {
createComponent({ line }); createComponent({ line: applyMap(line) });
expect(store.dispatch).toHaveBeenCalledTimes(1); expect(store.dispatch).toHaveBeenCalledTimes(1);
......
import Vue from 'vue';
import '~/behaviors/markdown/render_gfm'; import '~/behaviors/markdown/render_gfm';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import { mount } from '@vue/test-utils';
import { getByText } from '@testing-library/dom';
import { createStore } from '~/mr_notes/stores'; import { createStore } from '~/mr_notes/stores';
import InlineDiffView from '~/diffs/components/inline_diff_view.vue'; import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
import { mapInline } from '~/diffs/components/diff_row_utils';
import diffFileMockData from '../mock_data/diff_file'; import diffFileMockData from '../mock_data/diff_file';
import discussionsMockData from '../mock_data/diff_discussions'; import discussionsMockData from '../mock_data/diff_discussions';
describe('InlineDiffView', () => { describe('InlineDiffView', () => {
let component; let wrapper;
const getDiffFileMock = () => ({ ...diffFileMockData }); const getDiffFileMock = () => ({ ...diffFileMockData });
const getDiscussionsMockData = () => [{ ...discussionsMockData }]; const getDiscussionsMockData = () => [{ ...discussionsMockData }];
const notesLength = getDiscussionsMockData()[0].notes.length; const notesLength = getDiscussionsMockData()[0].notes.length;
beforeEach(done => { const setup = (diffFile, diffLines) => {
const diffFile = getDiffFileMock(); const mockDiffContent = {
diffFile,
shouldRenderDraftRow: jest.fn(),
};
const store = createStore(); const store = createStore();
store.dispatch('diffs/setInlineDiffViewType'); store.dispatch('diffs/setInlineDiffViewType');
component = createComponentWithStore(Vue.extend(InlineDiffView), store, { wrapper = mount(InlineDiffView, {
store,
propsData: {
diffFile, diffFile,
diffLines: diffFile.highlighted_diff_lines, diffLines: diffLines.map(mapInline(mockDiffContent)),
}).$mount(); },
Vue.nextTick(done);
}); });
};
describe('template', () => { describe('template', () => {
it('should have rendered diff lines', () => { it('should have rendered diff lines', () => {
const el = component.$el; const diffFile = getDiffFileMock();
setup(diffFile, diffFile.highlighted_diff_lines);
expect(el.querySelectorAll('tr.line_holder').length).toEqual(8); expect(wrapper.findAll('tr.line_holder').length).toEqual(8);
expect(el.querySelectorAll('tr.line_holder.new').length).toEqual(4); expect(wrapper.findAll('tr.line_holder.new').length).toEqual(4);
expect(el.querySelectorAll('tr.line_expansion.match').length).toEqual(1); expect(wrapper.findAll('tr.line_expansion.match').length).toEqual(1);
expect(el.textContent.indexOf('Bad dates')).toBeGreaterThan(-1); getByText(wrapper.element, /Bad dates/i);
}); });
it('should render discussions', done => { it('should render discussions', () => {
const el = component.$el; const diffFile = getDiffFileMock();
component.diffLines[1].discussions = getDiscussionsMockData(); diffFile.highlighted_diff_lines[1].discussions = getDiscussionsMockData();
component.diffLines[1].discussionsExpanded = true; diffFile.highlighted_diff_lines[1].discussionsExpanded = true;
setup(diffFile, diffFile.highlighted_diff_lines);
Vue.nextTick(() => {
expect(el.querySelectorAll('.notes_holder').length).toEqual(1); expect(wrapper.findAll('.notes_holder').length).toEqual(1);
expect(el.querySelectorAll('.notes_holder .note').length).toEqual(notesLength + 1); expect(wrapper.findAll('.notes_holder .note').length).toEqual(notesLength + 1);
expect(el.innerText.indexOf('comment 5')).toBeGreaterThan(-1); getByText(wrapper.element, 'comment 5');
component.$store.dispatch('setInitialNotes', []); wrapper.vm.$store.dispatch('setInitialNotes', []);
done();
});
}); });
}); });
}); });
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/mr_notes/stores';
import ParallelDiffExpansionRow from '~/diffs/components/parallel_diff_expansion_row.vue';
import diffFileMockData from '../mock_data/diff_file';
describe('ParallelDiffExpansionRow', () => {
const matchLine = diffFileMockData.highlighted_diff_lines[5];
const createComponent = (options = {}) => {
const cmp = Vue.extend(ParallelDiffExpansionRow);
const defaults = {
fileHash: diffFileMockData.file_hash,
contextLinesPath: 'contextLinesPath',
line: matchLine,
isTop: false,
isBottom: false,
};
const props = { ...defaults, ...options };
return createComponentWithStore(cmp, createStore(), props).$mount();
};
describe('template', () => {
it('should render expansion row for match lines', () => {
const vm = createComponent();
expect(vm.$el.classList.contains('line_expansion')).toBe(true);
});
});
});
...@@ -3,11 +3,22 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -3,11 +3,22 @@ import { shallowMount } from '@vue/test-utils';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/mr_notes/stores'; import { createStore } from '~/mr_notes/stores';
import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue'; import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
import { mapParallel } from '~/diffs/components/diff_row_utils';
import diffFileMockData from '../mock_data/diff_file'; import diffFileMockData from '../mock_data/diff_file';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue'; import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import discussionsMockData from '../mock_data/diff_discussions'; import discussionsMockData from '../mock_data/diff_discussions';
describe('ParallelDiffTableRow', () => { describe('ParallelDiffTableRow', () => {
const mockDiffContent = {
diffFile: diffFileMockData,
shouldRenderDraftRow: jest.fn(),
hasParallelDraftLeft: jest.fn(),
hasParallelDraftRight: jest.fn(),
draftForLine: jest.fn(),
};
const applyMap = mapParallel(mockDiffContent);
describe('when one side is empty', () => { describe('when one side is empty', () => {
let wrapper; let wrapper;
let vm; let vm;
...@@ -18,7 +29,7 @@ describe('ParallelDiffTableRow', () => { ...@@ -18,7 +29,7 @@ describe('ParallelDiffTableRow', () => {
wrapper = shallowMount(ParallelDiffTableRow, { wrapper = shallowMount(ParallelDiffTableRow, {
store: createStore(), store: createStore(),
propsData: { propsData: {
line: thisLine, line: applyMap(thisLine),
fileHash: diffFileMockData.file_hash, fileHash: diffFileMockData.file_hash,
filePath: diffFileMockData.file_path, filePath: diffFileMockData.file_path,
contextLinesPath: 'contextLinesPath', contextLinesPath: 'contextLinesPath',
...@@ -67,7 +78,7 @@ describe('ParallelDiffTableRow', () => { ...@@ -67,7 +78,7 @@ describe('ParallelDiffTableRow', () => {
beforeEach(() => { beforeEach(() => {
vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), { vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), {
line: thisLine, line: applyMap(thisLine),
fileHash: diffFileMockData.file_hash, fileHash: diffFileMockData.file_hash,
filePath: diffFileMockData.file_path, filePath: diffFileMockData.file_path,
contextLinesPath: 'contextLinesPath', contextLinesPath: 'contextLinesPath',
...@@ -243,7 +254,10 @@ describe('ParallelDiffTableRow', () => { ...@@ -243,7 +254,10 @@ describe('ParallelDiffTableRow', () => {
${{ ...thisLine, left: { type: 'old-nonewline', discussions: [] } }} | ${false} ${{ ...thisLine, left: { type: 'old-nonewline', discussions: [] } }} | ${false}
${{ ...thisLine, left: { discussions: [{}] } }} | ${false} ${{ ...thisLine, left: { discussions: [{}] } }} | ${false}
`('visible is $expectation - line ($line)', async ({ line, expectation }) => { `('visible is $expectation - line ($line)', async ({ line, expectation }) => {
createComponent({ line }, store, { isLeftHover: true, isCommentButtonRendered: true }); createComponent({ line: applyMap(line) }, store, {
isLeftHover: true,
isCommentButtonRendered: true,
});
expect(findNoteButton().isVisible()).toBe(expectation); expect(findNoteButton().isVisible()).toBe(expectation);
}); });
...@@ -320,7 +334,7 @@ describe('ParallelDiffTableRow', () => { ...@@ -320,7 +334,7 @@ describe('ParallelDiffTableRow', () => {
Object.assign(thisLine.left, lineProps); Object.assign(thisLine.left, lineProps);
Object.assign(thisLine.right, lineProps); Object.assign(thisLine.right, lineProps);
createComponent({ createComponent({
line: { ...thisLine }, line: applyMap({ ...thisLine }),
}); });
}); });
...@@ -357,7 +371,7 @@ describe('ParallelDiffTableRow', () => { ...@@ -357,7 +371,7 @@ describe('ParallelDiffTableRow', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(store, 'dispatch').mockImplementation(); jest.spyOn(store, 'dispatch').mockImplementation();
line = { line = applyMap({
left: { left: {
line_code: TEST_LINE_CODE, line_code: TEST_LINE_CODE,
type: 'new', type: 'new',
...@@ -369,7 +383,7 @@ describe('ParallelDiffTableRow', () => { ...@@ -369,7 +383,7 @@ describe('ParallelDiffTableRow', () => {
rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
meta_data: null, meta_data: null,
}, },
}; });
}); });
describe('with showCommentButton', () => { describe('with showCommentButton', () => {
...@@ -384,7 +398,7 @@ describe('ParallelDiffTableRow', () => { ...@@ -384,7 +398,7 @@ describe('ParallelDiffTableRow', () => {
it('does notrender if line has no discussions', () => { it('does notrender if line has no discussions', () => {
line.left.discussions = []; line.left.discussions = [];
createComponent({ line }); createComponent({ line: applyMap(line) });
expect(findAvatars().exists()).toEqual(false); expect(findAvatars().exists()).toEqual(false);
}); });
......
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