Commit 169a242d authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'ph/diffRowFunctionalComponent' into 'master'

Converts the diff row component into a functional component [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!63854
parents d1fbc9e1 81b9b785
<script>
/* eslint-disable vue/no-v-html */
import { GlTooltipDirective } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { memoize } from 'lodash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import {
CONTEXT_LINE_CLASS_NAME,
PARALLEL_DIFF_VIEW_TYPE,
CONFLICT_MARKER_OUR,
CONFLICT_MARKER_THEIR,
CONFLICT_OUR,
CONFLICT_THEIR,
......@@ -22,15 +18,8 @@ import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default {
components: {
DiffGutterAvatars,
CodeQualityGutterIcon: () =>
import('ee_component/diffs/components/code_quality_gutter_icon.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
DiffGutterAvatars,
CodeQualityGutterIcon: () => import('ee_component/diffs/components/code_quality_gutter_icon.vue'),
props: {
fileHash: {
type: String,
......@@ -58,148 +47,107 @@ export default {
type: Number,
required: true,
},
isHighlighted: {
type: Boolean,
required: true,
},
fileLineCoverage: {
type: Function,
required: true,
},
},
data() {
return {
dragging: 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, false);
},
}),
classNameMap() {
classNameMap: memoize(
(props) => {
return {
[CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: !this.inline,
commented: this.isCommented,
[PARALLEL_DIFF_VIEW_TYPE]: !props.inline,
commented: props.isCommented,
};
},
parallelViewLeftLineType() {
return utils.parallelViewLeftLineType(this.line, this.isHighlighted || this.isCommented);
(props) => [!props.inline, props.isCommented].join(':'),
),
parallelViewLeftLineType: memoize(
(props) => {
return utils.parallelViewLeftLineType(props.line, props.isHighlighted || props.isCommented);
},
coverageStateLeft() {
if (!this.inline || !this.line.left) return {};
return this.fileLineCoverage(this.filePath, this.line.left.new_line);
(props) =>
[props.line.left?.type, props.line.right?.type, props.isHighlighted, props.isCommented].join(
':',
),
),
coverageStateLeft: memoize(
(props) => {
if (!props.inline || !props.line.left) return {};
return props.fileLineCoverage(props.filePath, props.line.left.new_line);
},
coverageStateRight() {
if (!this.line.right) return {};
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
(props) => [props.inline, props.filePath, props.line.left?.new_line].join(':'),
),
coverageStateRight: memoize(
(props) => {
if (!props.line.right) return {};
return props.fileLineCoverage(props.filePath, props.line.right.new_line);
},
showCodequalityLeft() {
(props) => [props.line.right?.new_line, props.filePath].join(':'),
),
showCodequalityLeft: memoize(
(props) => {
return (
this.glFeatures.codequalityMrDiffAnnotations &&
this.inline &&
this.line.left?.codequality?.length > 0
window.gon?.features?.codequalityMrDiffAnnotations &&
props.inline &&
props.line.left?.codequality?.length > 0
);
},
showCodequalityRight() {
(props) => [props.inline, props.line.left?.codequality?.length].join(':'),
),
showCodequalityRight: memoize(
(props) => {
return (
this.glFeatures.codequalityMrDiffAnnotations &&
!this.inline &&
this.line.right?.codequality?.length > 0
window.gon?.features?.codequalityMrDiffAnnotations &&
!props.inline &&
props.line.right?.codequality?.length > 0
);
},
classNameMapCellLeft() {
(props) => [props.inline, props.line.right?.codequality?.length].join(':'),
),
classNameMapCellLeft: memoize(
(props) => {
return utils.classNameMapCell({
line: this.line.left,
hll: this.isHighlighted || this.isCommented,
isLoggedIn: this.isLoggedIn,
line: props.line.left,
hll: props.isHighlighted || props.isCommented,
});
},
classNameMapCellRight() {
(props) => [props.line.left.type, props.isHighlighted, props.isCommented].join(':'),
),
classNameMapCellRight: memoize(
(props) => {
return utils.classNameMapCell({
line: this.line.right,
hll: this.isHighlighted || this.isCommented,
isLoggedIn: this.isLoggedIn,
line: props.line.right,
hll: props.isHighlighted || props.isCommented,
});
},
addCommentTooltipLeft() {
return utils.addCommentTooltip(this.line.left, this.glFeatures.dragCommentSelection);
(props) => [props.line.right.type, props.isHighlighted, props.isCommented].join(':'),
),
shouldRenderCommentButton: memoize(
(props) => {
return isLoggedIn() && !props.line.isContextLineLeft && !props.line.isMetaLineLeft;
},
addCommentTooltipRight() {
return utils.addCommentTooltip(this.line.right, this.glFeatures.dragCommentSelection);
},
emptyCellRightClassMap() {
return { conflict_their: this.line.left?.type === CONFLICT_OUR };
},
emptyCellLeftClassMap() {
return { conflict_our: this.line.right?.type === CONFLICT_THEIR };
},
shouldRenderCommentButton() {
return this.isLoggedIn && !this.line.isContextLineLeft && !this.line.isMetaLineLeft;
},
isLeftConflictMarker() {
return [CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR].includes(this.line.left?.type);
},
interopLeftAttributes() {
if (this.inline) {
return getInteropInlineAttributes(this.line.left);
}
(props) => [props.line.isContextLineLeft, props.line.isMetaLineLeft].join(':'),
),
interopLeftAttributes(props) {
if (props.inline) {
return getInteropInlineAttributes(props.line.left);
}
return getInteropOldSideAttributes(this.line.left);
},
interopRightAttributes() {
return getInteropNewSideAttributes(this.line.right);
},
return getInteropOldSideAttributes(props.line.left);
},
mounted() {
this.scrollToLineIfNeededParallel(this.line);
interopRightAttributes(props) {
return getInteropNewSideAttributes(props.line.right);
},
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 });
},
conflictText(line) {
return line.type === CONFLICT_MARKER_THEIR
? this.$options.THEIR_CHANGES
: this.$options.OUR_CHANGES;
},
onDragEnd() {
this.dragging = false;
if (!this.glFeatures.dragCommentSelection) return;
this.$emit('stopdragging');
},
onDragEnter(line, index) {
if (!this.glFeatures.dragCommentSelection) return;
this.$emit('enterdragging', { ...line, index });
conflictText: memoize(
(line) => {
return line.type === CONFLICT_MARKER_THEIR ? 'HEAD//our changes' : 'origin//their changes';
},
onDragStart(line) {
this.$root.$emit(BV_HIDE_TOOLTIP);
this.dragging = true;
this.$emit('startdragging', line);
},
},
OUR_CHANGES: 'HEAD//our changes',
THEIR_CHANGES: 'origin//their changes',
(line) => line.type,
),
CONFLICT_MARKER,
CONFLICT_MARKER_THEIR,
CONFLICT_OUR,
......@@ -207,250 +155,259 @@ export default {
};
</script>
<template>
<div :class="classNameMap" class="diff-grid-row diff-tr line_holder">
<template functional>
<div :class="$options.classNameMap(props)" class="diff-grid-row diff-tr line_holder">
<div
:id="line.left && line.left.line_code"
:id="props.line.left && props.line.left.line_code"
data-testid="left-side"
class="diff-grid-left left-side"
v-bind="interopLeftAttributes"
v-bind="$options.interopLeftAttributes(props)"
@dragover.prevent
@dragenter="onDragEnter(line.left, index)"
@dragend="onDragEnd"
@dragenter="listeners.enterdragging({ ...props.line.left, index: props.index })"
@dragend="listeners.stopdragging"
>
<template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER">
<template v-if="props.line.left && props.line.left.type !== $options.CONFLICT_MARKER">
<div
:class="classNameMapCellLeft"
:class="$options.classNameMapCellLeft(props)"
data-testid="left-line-number"
class="diff-td diff-line-num"
data-qa-selector="new_diff_line_link"
>
<template v-if="!isLeftConflictMarker">
<span
v-if="shouldRenderCommentButton && !line.hasDiscussionsLeft"
v-gl-tooltip
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltipLeft"
>
<div
data-testid="left-comment-button"
role="button"
tabindex="0"
:draggable="!line.left.commentsDisabled && glFeatures.dragCommentSelection"
type="button"
class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button"
data-qa-selector="diff_comment_button"
:class="{ 'gl-cursor-grab': dragging }"
:disabled="line.left.commentsDisabled"
:aria-disabled="line.left.commentsDisabled"
@click="!line.left.commentsDisabled && handleCommentButton(line.left)"
@keydown.enter="!line.left.commentsDisabled && handleCommentButton(line.left)"
@keydown.space="!line.left.commentsDisabled && handleCommentButton(line.left)"
@dragstart="!line.left.commentsDisabled && onDragStart({ ...line.left, index })"
></div>
</span>
</template>
<span
v-if="
!props.line.left.isConflictMarker &&
$options.shouldRenderCommentButton(props) &&
!props.line.hasDiscussionsLeft
"
class="add-diff-note tooltip-wrapper has-tooltip"
:title="props.line.left.addCommentTooltip"
>
<div
data-testid="left-comment-button"
role="button"
tabindex="0"
:draggable="!props.line.left.commentsDisabled"
type="button"
class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button"
data-qa-selector="diff_comment_button"
:disabled="props.line.left.commentsDisabled"
:aria-disabled="props.line.left.commentsDisabled"
@click="
!props.line.left.commentsDisabled &&
listeners.showCommentForm(props.line.left.line_code)
"
@keydown.enter="
!props.line.left.commentsDisabled &&
listeners.showCommentForm(props.line.left.line_code)
"
@keydown.space="
!props.line.left.commentsDisabled &&
listeners.showCommentForm(props.line.left.line_code)
"
@dragstart="
!props.line.left.commentsDisabled &&
listeners.startdragging({
event: $event,
line: { ...props.line.left, index: props.index },
})
"
></div>
</span>
<a
v-if="line.left.old_line && line.left.type !== $options.CONFLICT_THEIR"
:data-linenumber="line.left.old_line"
:href="line.lineHrefOld"
@click="setHighlightedRow(line.lineCode)"
v-if="props.line.left.old_line && props.line.left.type !== $options.CONFLICT_THEIR"
:data-linenumber="props.line.left.old_line"
:href="props.line.lineHrefOld"
@click="listeners.setHighlightedRow(props.line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="line.hasDiscussionsLeft"
:discussions="line.left.discussions"
:discussions-expanded="line.left.discussionsExpanded"
<component
:is="$options.DiffGutterAvatars"
v-if="props.line.hasDiscussionsLeft"
:discussions="props.line.left.discussions"
:discussions-expanded="props.line.left.discussionsExpanded"
data-testid="left-discussions"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.left.line_code,
fileHash,
expanded: !line.left.discussionsExpanded,
listeners.toggleLineDiscussions({
lineCode: props.line.left.line_code,
expanded: !props.line.left.discussionsExpanded,
})
"
/>
</div>
<div v-if="inline" :class="classNameMapCellLeft" class="diff-td diff-line-num">
<div
v-if="props.inline"
:class="$options.classNameMapCellLeft(props)"
class="diff-td diff-line-num"
>
<a
v-if="line.left.new_line && line.left.type !== $options.CONFLICT_OUR"
:data-linenumber="line.left.new_line"
:href="line.lineHrefOld"
@click="setHighlightedRow(line.lineCode)"
v-if="props.line.left.new_line && props.line.left.type !== $options.CONFLICT_OUR"
:data-linenumber="props.line.left.new_line"
:href="props.line.lineHrefOld"
@click="listeners.setHighlightedRow(props.line.lineCode)"
>
</a>
</div>
<div
v-gl-tooltip.hover
:title="coverageStateLeft.text"
:class="[...parallelViewLeftLineType, coverageStateLeft.class]"
class="diff-td line-coverage left-side"
:title="$options.coverageStateLeft(props).text"
:class="[
$options.parallelViewLeftLineType(props),
$options.coverageStateLeft(props).class,
]"
class="diff-td line-coverage left-side has-tooltip"
></div>
<div class="diff-td line-codequality left-side" :class="[...parallelViewLeftLineType]">
<code-quality-gutter-icon
v-if="showCodequalityLeft"
:file-path="filePath"
:codequality="line.left.codequality"
<div
class="diff-td line-codequality left-side"
:class="$options.parallelViewLeftLineType(props)"
>
<component
:is="$options.CodeQualityGutterIcon"
v-if="$options.showCodequalityLeft(props)"
:codequality="props.line.left.codequality"
:file-path="props.filePath"
/>
</div>
<div
:key="line.left.line_code"
:class="[parallelViewLeftLineType, { parallel: !inline }]"
:key="props.line.left.line_code"
:class="[$options.parallelViewLeftLineType(props), { parallel: !props.inline }]"
class="diff-td line_content with-coverage left-side"
data-testid="left-content"
@mousedown="handleParallelLineMouseDown"
>
<strong v-if="isLeftConflictMarker">{{ conflictText(line.left) }}</strong>
<span v-else v-html="line.left.rich_text"></span>
<strong v-if="props.line.left.isConflictMarker">{{
$options.conflictText(props.line.left)
}}</strong>
<span v-else v-html="props.line.left.rich_text"></span>
</div>
</template>
<template v-else-if="!inline || (line.left && line.left.type === $options.CONFLICT_MARKER)">
<div
data-testid="left-empty-cell"
class="diff-td diff-line-num old_line empty-cell"
:class="emptyCellLeftClassMap"
>
<template
v-else-if="
!props.inline || (props.line.left && props.line.left.type === $options.CONFLICT_MARKER)
"
>
<div data-testid="left-empty-cell" class="diff-td diff-line-num old_line empty-cell">
&nbsp;
</div>
<div
v-if="inline"
class="diff-td diff-line-num old_line empty-cell"
:class="emptyCellLeftClassMap"
></div>
<div
class="diff-td line-coverage left-side empty-cell"
:class="emptyCellLeftClassMap"
></div>
<div
v-if="inline"
class="diff-td line-codequality left-side empty-cell"
:class="emptyCellLeftClassMap"
></div>
<div v-if="props.inline" class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td line-coverage left-side empty-cell"></div>
<div v-if="props.inline" class="diff-td line-codequality left-side empty-cell"></div>
<div
class="diff-td line_content with-coverage left-side empty-cell"
:class="[emptyCellLeftClassMap, { parallel: !inline }]"
:class="[{ parallel: !props.inline }]"
></div>
</template>
</div>
<div
v-if="!inline"
:id="line.right && line.right.line_code"
v-if="!props.inline"
:id="props.line.right && props.line.right.line_code"
data-testid="right-side"
class="diff-grid-right right-side"
v-bind="interopRightAttributes"
v-bind="$options.interopRightAttributes(props)"
@dragover.prevent
@dragenter="onDragEnter(line.right, index)"
@dragend="onDragEnd"
@dragenter="listeners.enterdragging({ ...props.line.right, index: props.index })"
@dragend="listeners.stopdragging"
>
<template v-if="line.right">
<div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
<template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR">
<template v-if="props.line.right">
<div :class="$options.classNameMapCellRight(props)" class="diff-td diff-line-num new_line">
<template v-if="props.line.right.type !== $options.CONFLICT_MARKER_THEIR">
<span
v-if="shouldRenderCommentButton && !line.hasDiscussionsRight"
v-gl-tooltip
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltipRight"
v-if="$options.shouldRenderCommentButton(props) && !props.line.hasDiscussionsRight"
class="add-diff-note tooltip-wrapper has-tooltip"
:title="props.line.right.addCommentTooltip"
>
<div
data-testid="right-comment-button"
role="button"
tabindex="0"
:draggable="!line.right.commentsDisabled && glFeatures.dragCommentSelection"
:draggable="!props.line.right.commentsDisabled"
type="button"
class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button"
:class="{ 'gl-cursor-grab': dragging }"
:disabled="line.right.commentsDisabled"
:aria-disabled="line.right.commentsDisabled"
@click="!line.right.commentsDisabled && handleCommentButton(line.right)"
@keydown.enter="!line.right.commentsDisabled && handleCommentButton(line.right)"
@keydown.space="!line.right.commentsDisabled && handleCommentButton(line.right)"
@dragstart="!line.right.commentsDisabled && onDragStart({ ...line.right, index })"
:disabled="props.line.right.commentsDisabled"
:aria-disabled="props.line.right.commentsDisabled"
@click="
!props.line.right.commentsDisabled &&
listeners.showCommentForm(props.line.right.line_code)
"
@keydown.enter="
!props.line.right.commentsDisabled &&
listeners.showCommentForm(props.line.right.line_code)
"
@keydown.space="
!props.line.right.commentsDisabled &&
listeners.showCommentForm(props.line.right.line_code)
"
@dragstart="
!props.line.right.commentsDisabled &&
listeners.startdragging({
event: $event,
line: { ...props.line.right, index: props.index },
})
"
></div>
</span>
</template>
<a
v-if="line.right.new_line"
:data-linenumber="line.right.new_line"
:href="line.lineHrefNew"
@click="setHighlightedRow(line.lineCode)"
v-if="props.line.right.new_line"
:data-linenumber="props.line.right.new_line"
:href="props.line.lineHrefNew"
@click="listeners.setHighlightedRow(props.line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="line.hasDiscussionsRight"
:discussions="line.right.discussions"
:discussions-expanded="line.right.discussionsExpanded"
<component
:is="$options.DiffGutterAvatars"
v-if="props.line.hasDiscussionsRight"
:discussions="props.line.right.discussions"
:discussions-expanded="props.line.right.discussionsExpanded"
data-testid="right-discussions"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.right.line_code,
fileHash,
expanded: !line.right.discussionsExpanded,
listeners.toggleLineDiscussions({
lineCode: props.line.right.line_code,
expanded: !props.line.right.discussionsExpanded,
})
"
/>
</div>
<div
v-gl-tooltip.hover
:title="coverageStateRight.text"
:title="$options.coverageStateRight(props).text"
:class="[
line.right.type,
coverageStateRight.class,
{ hll: isHighlighted, hll: isCommented },
props.line.right.type,
$options.coverageStateRight(props).class,
{ hll: props.isHighlighted, hll: props.isCommented },
]"
class="diff-td line-coverage right-side"
class="diff-td line-coverage right-side has-tooltip"
></div>
<div
class="diff-td line-codequality right-side"
:class="[line.right.type, { hll: isHighlighted, hll: isCommented }]"
:class="[props.line.right.type, { hll: props.isHighlighted, hll: props.isCommented }]"
>
<code-quality-gutter-icon
v-if="showCodequalityRight"
:file-path="filePath"
:codequality="line.right.codequality"
<component
:is="$options.CodeQualityGutterIcon"
v-if="$options.showCodequalityRight(props)"
:codequality="props.line.right.codequality"
:file-path="props.filePath"
data-testid="codeQualityIcon"
/>
</div>
<div
:key="line.right.rich_text"
:key="props.line.right.rich_text"
:class="[
line.right.type,
props.line.right.type,
{
hll: isHighlighted,
hll: isCommented,
parallel: !inline,
hll: props.isHighlighted,
hll: props.isCommented,
},
]"
class="diff-td line_content with-coverage right-side"
@mousedown="handleParallelLineMouseDown"
class="diff-td line_content with-coverage right-side parallel"
>
<strong v-if="line.right.type === $options.CONFLICT_MARKER_THEIR">{{
conflictText(line.right)
<strong v-if="props.line.right.type === $options.CONFLICT_MARKER_THEIR">{{
$options.conflictText(props.line.right)
}}</strong>
<span v-else v-html="line.right.rich_text"></span>
<span v-else v-html="props.line.right.rich_text"></span>
</div>
</template>
<template v-else>
<div
data-testid="right-empty-cell"
class="diff-td diff-line-num old_line empty-cell"
:class="emptyCellRightClassMap"
></div>
<div
v-if="inline"
class="diff-td diff-line-num old_line empty-cell"
:class="emptyCellRightClassMap"
></div>
<div
class="diff-td line-coverage right-side empty-cell"
:class="emptyCellRightClassMap"
></div>
<div
class="diff-td line-codequality right-side empty-cell"
:class="emptyCellRightClassMap"
></div>
<div
class="diff-td line_content with-coverage right-side empty-cell"
:class="[emptyCellRightClassMap, { parallel: !inline }]"
></div>
<div data-testid="right-empty-cell" 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-codequality right-side empty-cell"></div>
<div class="diff-td line_content with-coverage right-side empty-cell parallel"></div>
</template>
</div>
</div>
......
......@@ -6,13 +6,17 @@ import {
OLD_NO_NEW_LINE_TYPE,
NEW_NO_NEW_LINE_TYPE,
EMPTY_CELL_TYPE,
CONFLICT_MARKER_OUR,
CONFLICT_MARKER_THEIR,
CONFLICT_THEIR,
CONFLICT_OUR,
} from '../constants';
export const isHighlighted = (state, line, isCommented) => {
export const isHighlighted = (highlightedRow, line, isCommented) => {
if (isCommented) return true;
const lineCode = line?.line_code;
return lineCode ? lineCode === state.diffs.highlightedRow : false;
return lineCode ? lineCode === highlightedRow : false;
};
export const isContextLine = (type) => type === CONTEXT_LINE_TYPE;
......@@ -50,13 +54,11 @@ export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => {
];
};
export const addCommentTooltip = (line, dragCommentSelectionEnabled = false) => {
export const addCommentTooltip = (line) => {
let tooltip;
if (!line) return tooltip;
tooltip = dragCommentSelectionEnabled
? __('Add a comment to this line or drag for multiple lines')
: __('Add a comment to this line');
tooltip = __('Add a comment to this line or drag for multiple lines');
const brokenSymlinks = line.commentsDisabled;
if (brokenSymlinks) {
......@@ -107,6 +109,10 @@ export const mapParallel = (content) => (line) => {
hasDraft: content.hasParallelDraftLeft(content.diffFile.file_hash, line),
lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'left'),
hasCommentForm: left.hasForm,
isConflictMarker:
line.left.type === CONFLICT_MARKER_OUR || line.left.type === CONFLICT_MARKER_THEIR,
emptyCellClassMap: { conflict_our: line.right?.type === CONFLICT_THEIR },
addCommentTooltip: addCommentTooltip(line.left),
};
}
if (right) {
......@@ -116,6 +122,8 @@ export const mapParallel = (content) => (line) => {
hasDraft: content.hasParallelDraftRight(content.diffFile.file_hash, line),
lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'right'),
hasCommentForm: Boolean(right.hasForm && right.type),
emptyCellClassMap: { conflict_their: line.left?.type === CONFLICT_OUR },
addCommentTooltip: addCommentTooltip(line.right),
};
}
......
......@@ -3,10 +3,12 @@ import { mapGetters, mapState, mapActions } from 'vuex';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import { hide } from '~/tooltips';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import DiffRow from './diff_row.vue';
import { isHighlighted } from './diff_row_utils';
export default {
components: {
......@@ -43,8 +45,8 @@ export default {
};
},
computed: {
...mapGetters('diffs', ['commitId']),
...mapState('diffs', ['codequalityDiff']),
...mapGetters('diffs', ['commitId', 'fileLineCoverage']),
...mapState('diffs', ['codequalityDiff', 'highlightedRow']),
...mapState({
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
......@@ -67,14 +69,17 @@ export default {
},
methods: {
...mapActions(['setSelectedCommentPosition']),
...mapActions('diffs', ['showCommentForm']),
...mapActions('diffs', ['showCommentForm', 'setHighlightedRow', 'toggleLineDiscussions']),
showCommentLeft(line) {
return line.left && !line.right;
},
showCommentRight(line) {
return line.right && !line.left;
},
onStartDragging(line) {
onStartDragging({ event = {}, line }) {
if (event.target?.parentNode) {
hide(event.target.parentNode);
}
this.dragStart = line;
},
onDragOver(line) {
......@@ -99,6 +104,26 @@ export default {
});
this.dragStart = null;
},
isHighlighted(line) {
return isHighlighted(
this.highlightedRow,
line.left?.line_code ? line.left : line.right,
false,
);
},
handleParallelLineMouseDown(e) {
const line = e.target.closest('.diff-td');
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`);
}
},
},
userColorScheme: window.gon.user_color_scheme,
};
......@@ -109,6 +134,7 @@ export default {
:class="[$options.userColorScheme, { inline, 'with-codequality': hasCodequalityChanges }]"
:data-commit-id="commitId"
class="diff-grid diff-table code diff-wrap-lines js-syntax-highlight text-file"
@mousedown="handleParallelLineMouseDown"
>
<template v-for="(line, index) in diffLines">
<div
......@@ -136,6 +162,14 @@ export default {
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
:inline="inline"
:index="index"
:is-highlighted="isHighlighted(line)"
:file-line-coverage="fileLineCoverage"
@showCommentForm="(lineCode) => showCommentForm({ lineCode, fileHash: diffFile.file_hash })"
@setHighlightedRow="setHighlightedRow"
@toggleLineDiscussions="
({ lineCode, expanded }) =>
toggleLineDiscussions({ lineCode, fileHash: diffFile.file_hash, expanded })
"
@enterdragging="onDragOver"
@startdragging="onStartDragging"
@stopdragging="onStopDragging"
......
......@@ -235,6 +235,8 @@ export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
commit(types.VIEW_DIFF_FILE, fileHash);
handleLocationHash();
};
// This is adding line discussions to the actual lines in the diff tree
......
import AccessorUtilities from '~/lib/utils/accessor';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { getGroups, getProjects } from '~/rest_api';
import { getTopFrequentItems } from '../utils';
import * as types from './mutation_types';
......@@ -51,7 +52,7 @@ export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
const params = {
simple: true,
per_page: 20,
membership: Boolean(gon.current_user_id),
membership: isLoggedIn(),
};
let searchFunction;
......
......@@ -763,3 +763,5 @@ export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag];
* @returns {Array[String]} Converted array
*/
export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i));
export const isLoggedIn = () => Boolean(window.gon?.current_user_id);
......@@ -4,6 +4,7 @@ import { mapActions, mapGetters } from 'vuex';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import createFlash from '~/flash';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
......@@ -85,7 +86,7 @@ export default {
return this.getUserData;
},
isLoggedIn() {
return Boolean(gon.current_user_id);
return isLoggedIn();
},
autosaveKey() {
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
......
......@@ -5,6 +5,7 @@ import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
import createFlash from '~/flash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import blobInfoQuery from '../queries/blob_info.query.graphql';
import BlobButtonGroup from './blob_button_group.vue';
......@@ -90,7 +91,7 @@ export default {
},
computed: {
isLoggedIn() {
return Boolean(gon.current_user_id);
return isLoggedIn();
},
isLoading() {
return this.$apollo.queries.project.loading;
......
......@@ -2,6 +2,7 @@
import { GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { subscribedQueries } from '~/sidebar/constants';
......@@ -102,7 +103,7 @@ export default {
});
},
isLoggedIn() {
return Boolean(gon.current_user_id);
return isLoggedIn();
},
canSubscribe() {
return this.emailsDisabled || !this.isLoggedIn;
......
<script>
import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import ApplySuggestion from './apply_suggestion.vue';
......@@ -73,7 +74,7 @@ export default {
return __('Applying suggestions...');
},
isLoggedIn() {
return Boolean(gon.current_user_id);
return isLoggedIn();
},
},
methods: {
......
......@@ -763,6 +763,7 @@ $system-note-svg-size: 16px;
.note-button.add-diff-note {
@include btn-comment-icon;
opacity: 0;
will-change: opacity;
&[disabled] {
background: $white;
......
......@@ -32,7 +32,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:file_identifier_hash)
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project, default_enabled: :yaml)
push_frontend_feature_flag(:drag_comment_selection, @project, default_enabled: true)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project, default_enabled: :yaml)
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml)
......
---
name: drag_comment_selection
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49875
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/293945
milestone: '13.7'
type: development
group: group::source code
default_enabled: true
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import CodeQualityGutterIcon from 'ee/diffs/components/code_quality_gutter_icon.vue';
import DiffRow from '~/diffs/components/diff_row.vue';
import diffsModule from '~/diffs/store/modules';
......@@ -10,16 +9,24 @@ Vue.use(Vuex);
describe('EE DiffRow', () => {
let wrapper;
const findIcon = () => wrapper.findComponent(CodeQualityGutterIcon);
const findIcon = () => wrapper.find('[data-testid="codeQualityIcon"]');
const defaultProps = {
fileHash: 'abc',
filePath: 'abc',
line: {},
index: 0,
isHighlighted: false,
fileLineCoverage: () => ({}),
};
const createComponent = ({ props, state, actions, isLoggedIn = true, provide }) => {
const createComponent = ({
props,
state,
actions,
isLoggedIn = true,
codequalityMrDiffAnnotations,
}) => {
const diffs = diffsModule();
diffs.state = { ...diffs.state, ...state };
diffs.actions = { ...diffs.actions, ...actions };
......@@ -31,18 +38,38 @@ describe('EE DiffRow', () => {
getters,
});
wrapper = shallowMount(DiffRow, { propsData: { ...defaultProps, ...props }, store, provide });
window.gon = { features: { codequalityMrDiffAnnotations } };
wrapper = shallowMount(DiffRow, {
propsData: { ...defaultProps, ...props },
store,
listeners: {
enterdragging: () => {},
stopdragging: () => {},
showCommentForm: () => {},
setHighlightedRow: () => {},
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
window.gon = {};
Object.values(DiffRow).forEach(({ cache }) => {
if (cache) {
cache.clear();
}
});
});
describe('with feature flag enabled', () => {
beforeEach(() => {
createComponent({
props: { line: { right: { codequality: [{ severity: 'critical' }] } } },
provide: { glFeatures: { codequalityMrDiffAnnotations: true } },
codequalityMrDiffAnnotations: true,
});
});
......@@ -55,7 +82,7 @@ describe('EE DiffRow', () => {
beforeEach(() => {
createComponent({
props: { line: { right: { codequality: [{ severity: 'critical' }] } } },
provide: { glFeatures: { codequalityMrDiffAnnotations: false } },
codequalityMrDiffAnnotations: false,
});
});
......
......@@ -8,6 +8,12 @@ import diffsModule from '~/diffs/store/modules';
import { findInteropAttributes } from '../find_interop_attributes';
import diffFileMockData from '../mock_data/diff_file';
const showCommentForm = jest.fn();
const enterdragging = jest.fn();
const stopdragging = jest.fn();
const setHighlightedRow = jest.fn();
let wrapper;
describe('DiffRow', () => {
const testLines = [
{
......@@ -29,7 +35,7 @@ describe('DiffRow', () => {
},
];
const createWrapper = ({ props, state, actions, isLoggedIn = true }) => {
const createWrapper = ({ props, state = {}, actions, isLoggedIn = true }) => {
Vue.use(Vuex);
const diffs = diffsModule();
......@@ -43,11 +49,25 @@ describe('DiffRow', () => {
getters,
});
window.gon = { current_user_id: isLoggedIn ? 1 : 0 };
const coverageFileData = state.coverageFiles?.files ? state.coverageFiles.files : {};
const propsData = {
fileHash: 'abc',
filePath: 'abc',
line: {},
index: 0,
isHighlighted: false,
fileLineCoverage: (file, line) => {
const hits = coverageFileData[file]?.[line];
if (hits) {
return { text: `Test coverage: ${hits} hits`, class: 'coverage' };
} else if (hits === 0) {
return { text: 'No test coverage', class: 'no-coverage' };
}
return {};
},
...props,
};
......@@ -55,49 +75,37 @@ describe('DiffRow', () => {
glFeatures: { dragCommentSelection: true },
};
return shallowMount(DiffRow, { propsData, store, provide });
return shallowMount(DiffRow, {
propsData,
store,
provide,
listeners: {
enterdragging,
stopdragging,
setHighlightedRow,
showCommentForm,
},
});
};
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);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
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);
});
window.gon = {};
showCommentForm.mockReset();
enterdragging.mockReset();
stopdragging.mockReset();
setHighlightedRow.mockReset();
it('isHighlighted returns false given line.left', () => {
const props = {
line: {
left: {
line_code: 'abc',
},
},
};
const wrapper = createWrapper({ props });
expect(wrapper.vm.isHighlighted).toBe(false);
Object.values(DiffRow).forEach(({ cache }) => {
if (cache) {
cache.clear();
}
});
});
const getCommentButton = (wrapper, side) =>
wrapper.find(`[data-testid="${side}-comment-button"]`);
const getCommentButton = (side) => wrapper.find(`[data-testid="${side}-comment-button"]`);
describe.each`
side
......@@ -105,33 +113,30 @@ describe('DiffRow', () => {
${'right'}
`('$side side', ({ side }) => {
it(`renders empty cells if ${side} is unavailable`, () => {
const wrapper = createWrapper({ props: { line: testLines[2], inline: false } });
wrapper = createWrapper({ props: { line: testLines[2], inline: false } });
expect(wrapper.find(`[data-testid="${side}-line-number"]`).exists()).toBe(false);
expect(wrapper.find(`[data-testid="${side}-empty-cell"]`).exists()).toBe(true);
});
describe('comment button', () => {
const showCommentForm = jest.fn();
let line;
beforeEach(() => {
showCommentForm.mockReset();
// https://eslint.org/docs/rules/prefer-destructuring#when-not-to-use-it
// eslint-disable-next-line prefer-destructuring
line = testLines[3];
});
it('renders', () => {
const wrapper = createWrapper({ props: { line, inline: false } });
expect(getCommentButton(wrapper, side).exists()).toBe(true);
wrapper = createWrapper({ props: { line, inline: false } });
expect(getCommentButton(side).exists()).toBe(true);
});
it('responds to click and keyboard events', async () => {
const wrapper = createWrapper({
wrapper = createWrapper({
props: { line, inline: false },
actions: { showCommentForm },
});
const commentButton = getCommentButton(wrapper, side);
const commentButton = getCommentButton(side);
await commentButton.trigger('click');
await commentButton.trigger('keydown.enter');
......@@ -142,11 +147,10 @@ describe('DiffRow', () => {
it('ignores click and keyboard events when comments are disabled', async () => {
line[side].commentsDisabled = true;
const wrapper = createWrapper({
wrapper = createWrapper({
props: { line, inline: false },
actions: { showCommentForm },
});
const commentButton = getCommentButton(wrapper, side);
const commentButton = getCommentButton(side);
await commentButton.trigger('click');
await commentButton.trigger('keydown.enter');
......@@ -157,19 +161,20 @@ describe('DiffRow', () => {
});
it('renders avatars', () => {
const wrapper = createWrapper({ props: { line: testLines[0], inline: false } });
wrapper = createWrapper({ props: { line: testLines[0], inline: false } });
expect(wrapper.find(`[data-testid="${side}-discussions"]`).exists()).toBe(true);
});
});
it('renders left line numbers', () => {
const wrapper = createWrapper({ props: { line: testLines[0] } });
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] } });
wrapper = createWrapper({ props: { line: testLines[0] } });
const lineNumber = testLines[0].right.new_line;
expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true);
});
......@@ -186,12 +191,10 @@ describe('DiffRow', () => {
${'left'}
${'right'}
`('emits `enterdragging` onDragEnter $side side', ({ side }) => {
const expectation = { ...line[side], index: 0 };
const wrapper = createWrapper({ props: { line } });
wrapper = createWrapper({ props: { line } });
fireEvent.dragEnter(getByTestId(wrapper.element, `${side}-side`));
expect(wrapper.emitted().enterdragging).toBeTruthy();
expect(wrapper.emitted().enterdragging[0]).toEqual([expectation]);
expect(enterdragging).toHaveBeenCalledWith({ ...line[side], index: 0 });
});
it.each`
......@@ -199,10 +202,10 @@ describe('DiffRow', () => {
${'left'}
${'right'}
`('emits `stopdragging` onDrop $side side', ({ side }) => {
const wrapper = createWrapper({ props: { line } });
wrapper = createWrapper({ props: { line } });
fireEvent.dragEnd(getByTestId(wrapper.element, `${side}-side`));
expect(wrapper.emitted().stopdragging).toBeTruthy();
expect(stopdragging).toHaveBeenCalled();
});
});
......@@ -231,7 +234,7 @@ describe('DiffRow', () => {
it('for lines with coverage', () => {
const coverageFiles = { files: { [name]: { [line]: 5 } } };
const wrapper = createWrapper({ props, state: { coverageFiles } });
wrapper = createWrapper({ props, state: { coverageFiles } });
const coverage = wrapper.find('.line-coverage.right-side');
expect(coverage.attributes('title')).toContain('Test coverage: 5 hits');
......@@ -240,7 +243,7 @@ describe('DiffRow', () => {
it('for lines without coverage', () => {
const coverageFiles = { files: { [name]: { [line]: 0 } } };
const wrapper = createWrapper({ props, state: { coverageFiles } });
wrapper = createWrapper({ props, state: { coverageFiles } });
const coverage = wrapper.find('.line-coverage.right-side');
expect(coverage.attributes('title')).toContain('No test coverage');
......@@ -249,7 +252,7 @@ describe('DiffRow', () => {
it('for unknown lines', () => {
const coverageFiles = {};
const wrapper = createWrapper({ props, state: { coverageFiles } });
wrapper = createWrapper({ props, state: { coverageFiles } });
const coverage = wrapper.find('.line-coverage.right-side');
expect(coverage.attributes('title')).toBeFalsy();
......@@ -267,7 +270,7 @@ describe('DiffRow', () => {
${'with parallel and no left side'} | ${{ right: { old_line: 3, new_line: 5 } }} | ${false} | ${null} | ${{ type: 'new', line: '5', newLine: '5' }}
${'with parallel and right side'} | ${{ left: { old_line: 3 }, right: { new_line: 5 } }} | ${false} | ${{ type: 'old', line: '3', oldLine: '3' }} | ${{ type: 'new', line: '5', newLine: '5' }}
`('$desc, sets interop data attributes', ({ line, inline, leftSide, rightSide }) => {
const wrapper = createWrapper({ props: { line, inline } });
wrapper = createWrapper({ props: { line, inline } });
expect(findInteropAttributes(wrapper, '[data-testid="left-side"]')).toEqual(leftSide);
expect(findInteropAttributes(wrapper, '[data-testid="right-side"]')).toEqual(rightSide);
......
......@@ -11,24 +11,21 @@ const LINE_CODE = 'abc123';
describe('isHighlighted', () => {
it('should return true if line is highlighted', () => {
const state = { diffs: { highlightedRow: LINE_CODE } };
const line = { line_code: LINE_CODE };
const isCommented = false;
expect(utils.isHighlighted(state, line, isCommented)).toBe(true);
expect(utils.isHighlighted(LINE_CODE, line, isCommented)).toBe(true);
});
it('should return false if line is not highlighted', () => {
const state = { diffs: { highlightedRow: 'xxx' } };
const line = { line_code: LINE_CODE };
const isCommented = false;
expect(utils.isHighlighted(state, line, isCommented)).toBe(false);
expect(utils.isHighlighted('xxx', line, isCommented)).toBe(false);
});
it('should return true if isCommented is true', () => {
const state = { diffs: { highlightedRow: 'xxx' } };
const line = { line_code: LINE_CODE };
const isCommented = true;
expect(utils.isHighlighted(state, line, isCommented)).toBe(true);
expect(utils.isHighlighted('xxx', line, isCommented)).toBe(true);
});
});
......@@ -143,19 +140,14 @@ describe('addCommentTooltip', () => {
'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
const brokenRealTooltip =
'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
const commentTooltip = 'Add a comment to this line';
const dragTooltip = 'Add a comment to this line or drag for multiple lines';
it('should return default tooltip', () => {
expect(utils.addCommentTooltip()).toBeUndefined();
});
it('should return comment tooltip', () => {
expect(utils.addCommentTooltip({})).toEqual(commentTooltip);
});
it('should return drag comment tooltip when dragging is enabled', () => {
expect(utils.addCommentTooltip({}, true)).toEqual(dragTooltip);
expect(utils.addCommentTooltip({})).toEqual(dragTooltip);
});
it('should return broken symlink tooltip', () => {
......
......@@ -28,7 +28,7 @@ describe('DiffView', () => {
};
const diffs = {
actions: { showCommentForm },
getters: { commitId: () => 'abc123' },
getters: { commitId: () => 'abc123', fileLineCoverage: () => ({}) },
namespaced: true,
};
const notes = {
......@@ -84,7 +84,7 @@ describe('DiffView', () => {
it('sets `dragStart` onStartDragging', () => {
const wrapper = createWrapper({ diffLines: [{}] });
wrapper.findComponent(DiffRow).vm.$emit('startdragging', { test: true });
wrapper.findComponent(DiffRow).vm.$emit('startdragging', { line: { test: true } });
expect(wrapper.vm.dragStart).toEqual({ test: true });
});
......@@ -92,7 +92,7 @@ describe('DiffView', () => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
diffRow.$emit('startdragging', { chunk: 0 });
diffRow.$emit('startdragging', { line: { chunk: 0 } });
diffRow.$emit('enterdragging', { chunk: 1 });
expect(setSelectedCommentPosition).not.toHaveBeenCalled();
......@@ -109,7 +109,7 @@ describe('DiffView', () => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
diffRow.$emit('startdragging', { chunk: 1, index: start });
diffRow.$emit('startdragging', { line: { chunk: 1, index: start } });
diffRow.$emit('enterdragging', { chunk: 1, index: end });
const arg = setSelectedCommentPosition.mock.calls[0][1];
......@@ -122,7 +122,7 @@ describe('DiffView', () => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
diffRow.$emit('startdragging', { test: true });
diffRow.$emit('startdragging', { line: { test: true } });
expect(wrapper.vm.dragStart).toEqual({ test: true });
diffRow.$emit('stopdragging');
......
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