<script> import { GlTooltipDirective, GlSafeHtmlDirective as SafeHtml } 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 { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE, CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR, CONFLICT_OUR, CONFLICT_THEIR, CONFLICT_MARKER, } from '../constants'; import DiffGutterAvatars from './diff_gutter_avatars.vue'; import * as utils from './diff_row_utils'; export default { components: { DiffGutterAvatars, }, directives: { GlTooltip: GlTooltipDirective, SafeHtml, }, mixins: [glFeatureFlagsMixin()], 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, }, index: { type: Number, 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() { return { [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft, [PARALLEL_DIFF_VIEW_TYPE]: !this.inline, commented: this.isCommented, }; }, parallelViewLeftLineType() { return utils.parallelViewLeftLineType(this.line, this.isHighlighted || this.isCommented); }, coverageStateLeft() { if (!this.inline || !this.line.left) return {}; return this.fileLineCoverage(this.filePath, this.line.left.new_line); }, coverageStateRight() { if (!this.line.right) return {}; return this.fileLineCoverage(this.filePath, this.line.right.new_line); }, classNameMapCellLeft() { return utils.classNameMapCell({ line: this.line.left, hll: this.isHighlighted || this.isCommented, isLoggedIn: this.isLoggedIn, }); }, classNameMapCellRight() { return utils.classNameMapCell({ line: this.line.right, hll: this.isHighlighted || this.isCommented, isLoggedIn: this.isLoggedIn, }); }, addCommentTooltipLeft() { return utils.addCommentTooltip(this.line.left, this.glFeatures.dragCommentSelection); }, 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); }, }, 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 }); }, 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 }); }, 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', CONFLICT_MARKER, CONFLICT_MARKER_THEIR, CONFLICT_OUR, CONFLICT_THEIR, }; </script> <template> <div :class="classNameMap" class="diff-grid-row diff-tr line_holder"> <div data-testid="left-side" class="diff-grid-left left-side" @dragover.prevent @dragenter="onDragEnter(line.left, index)" @dragend="onDragEnd" > <template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER"> <div :class="classNameMapCellLeft" data-testid="leftLineNumber" class="diff-td diff-line-num" > <template v-if="!isLeftConflictMarker"> <span v-if="shouldRenderCommentButton && !line.hasDiscussionsLeft" v-gl-tooltip data-testid="leftCommentButton" class="add-diff-note tooltip-wrapper" :title="addCommentTooltipLeft" > <button :draggable="glFeatures.dragCommentSelection" type="button" class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button qa-diff-comment" data-qa-selector="diff_comment_button" :class="{ 'gl-cursor-grab': dragging }" :disabled="line.left.commentsDisabled" @click="handleCommentButton(line.left)" @dragstart="onDragStart({ ...line.left, index })" ></button> </span> </template> <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)" > </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 v-if="inline" :class="classNameMapCellLeft" 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)" > </a> </div> <div v-gl-tooltip.hover :title="coverageStateLeft.text" :class="[...parallelViewLeftLineType, coverageStateLeft.class]" class="diff-td line-coverage left-side" ></div> <div :id="line.left.line_code" :key="line.left.line_code" :class="[parallelViewLeftLineType, { parallel: !inline }]" class="diff-td line_content with-coverage left-side" data-testid="leftContent" @mousedown="handleParallelLineMouseDown" > <strong v-if="isLeftConflictMarker">{{ conflictText(line.left) }}</strong> <span v-else v-safe-html="line.left.rich_text"></span> </div> </template> <template v-else-if="!inline || (line.left && line.left.type === $options.CONFLICT_MARKER)"> <div data-testid="leftEmptyCell" class="diff-td diff-line-num old_line empty-cell" :class="emptyCellLeftClassMap" > </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 class="diff-td line_content with-coverage left-side empty-cell" :class="[emptyCellLeftClassMap, { parallel: !inline }]" ></div> </template> </div> <div v-if="!inline" data-testid="right-side" class="diff-grid-right right-side" @dragover.prevent @dragenter="onDragEnter(line.right, index)" @dragend="onDragEnd" > <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"> <span v-if="shouldRenderCommentButton && !line.hasDiscussionsRight" v-gl-tooltip data-testid="rightCommentButton" class="add-diff-note tooltip-wrapper" :title="addCommentTooltipRight" > <button :draggable="glFeatures.dragCommentSelection" type="button" class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button qa-diff-comment" :class="{ 'gl-cursor-grab': dragging }" :disabled="line.right.commentsDisabled" @click="handleCommentButton(line.right)" @dragstart="onDragStart({ ...line.right, index })" ></button> </span> </template> <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 v-gl-tooltip.hover :title="coverageStateRight.text" :class="[ line.right.type, coverageStateRight.class, { hll: isHighlighted, hll: isCommented }, ]" 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, hll: isCommented, parallel: !inline, }, ]" class="diff-td line_content with-coverage right-side" @mousedown="handleParallelLineMouseDown" > <strong v-if="line.right.type === $options.CONFLICT_MARKER_THEIR">{{ conflictText(line.right) }}</strong> <span v-else v-safe-html="line.right.rich_text"></span> </div> </template> <template v-else> <div data-testid="rightEmptyCell" 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_content with-coverage right-side empty-cell" :class="[emptyCellRightClassMap, { parallel: !inline }]" ></div> </template> </div> </div> </template>