481 lines
14 KiB
Vue
481 lines
14 KiB
Vue
<script>
|
|
import {
|
|
GlTooltipDirective,
|
|
GlSafeHtmlDirective,
|
|
GlIcon,
|
|
GlBadge,
|
|
GlButton,
|
|
GlButtonGroup,
|
|
GlDropdown,
|
|
GlDropdownItem,
|
|
GlDropdownDivider,
|
|
GlFormCheckbox,
|
|
GlLoadingIcon,
|
|
} from '@gitlab/ui';
|
|
import { escape } from 'lodash';
|
|
import { mapActions, mapGetters, mapState } from 'vuex';
|
|
import { IdState } from 'vendor/vue-virtual-scroller';
|
|
import { scrollToElement } from '~/lib/utils/common_utils';
|
|
import { truncateSha } from '~/lib/utils/text_utility';
|
|
import { __, s__, sprintf } from '~/locale';
|
|
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|
|
|
import { DIFF_FILE_AUTOMATIC_COLLAPSE } from '../constants';
|
|
import { DIFF_FILE_HEADER } from '../i18n';
|
|
import { collapsedType, isCollapsed } from '../utils/diff_file';
|
|
import { reviewable } from '../utils/file_reviews';
|
|
|
|
import DiffStats from './diff_stats.vue';
|
|
|
|
export default {
|
|
components: {
|
|
ClipboardButton,
|
|
GlIcon,
|
|
DiffStats,
|
|
GlBadge,
|
|
GlButton,
|
|
GlButtonGroup,
|
|
GlDropdown,
|
|
GlDropdownItem,
|
|
GlDropdownDivider,
|
|
GlFormCheckbox,
|
|
GlLoadingIcon,
|
|
},
|
|
directives: {
|
|
GlTooltip: GlTooltipDirective,
|
|
SafeHtml: GlSafeHtmlDirective,
|
|
},
|
|
mixins: [IdState({ idProp: (vm) => vm.diffFile.file_hash }), glFeatureFlagsMixin()],
|
|
i18n: {
|
|
...DIFF_FILE_HEADER,
|
|
compareButtonLabel: __('Compare submodule commit revisions'),
|
|
},
|
|
props: {
|
|
discussionPath: {
|
|
type: String,
|
|
required: false,
|
|
default: '',
|
|
},
|
|
diffFile: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
collapsible: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
addMergeRequestButtons: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
expanded: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true,
|
|
},
|
|
canCurrentUserFork: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
viewDiffsFileByFile: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
showLocalFileReviews: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
reviewed: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
codequalityDiff: {
|
|
type: Array,
|
|
required: false,
|
|
default: () => [],
|
|
},
|
|
},
|
|
idState() {
|
|
return {
|
|
moreActionsShown: false,
|
|
};
|
|
},
|
|
computed: {
|
|
...mapState('diffs', ['latestDiff']),
|
|
...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
|
|
diffContentIDSelector() {
|
|
return `#diff-content-${this.diffFile.file_hash}`;
|
|
},
|
|
|
|
titleLink() {
|
|
if (this.diffFile.submodule) {
|
|
return this.diffFile.submodule_tree_url || this.diffFile.submodule_link;
|
|
}
|
|
|
|
if (!this.discussionPath) {
|
|
return this.diffContentIDSelector;
|
|
}
|
|
|
|
return this.discussionPath;
|
|
},
|
|
submoduleDiffCompareLinkText() {
|
|
if (this.diffFile.submodule_compare) {
|
|
const truncatedOldSha = escape(truncateSha(this.diffFile.submodule_compare.old_sha));
|
|
const truncatedNewSha = escape(truncateSha(this.diffFile.submodule_compare.new_sha));
|
|
return sprintf(
|
|
__('Compare %{oldCommitId}...%{newCommitId}'),
|
|
{
|
|
oldCommitId: `<span class="commit-sha">${truncatedOldSha}</span>`,
|
|
newCommitId: `<span class="commit-sha">${truncatedNewSha}</span>`,
|
|
},
|
|
false,
|
|
);
|
|
}
|
|
return null;
|
|
},
|
|
filePath() {
|
|
if (this.diffFile.submodule) {
|
|
return `${this.diffFile.file_path} @ ${truncateSha(this.diffFile.blob.id)}`;
|
|
}
|
|
|
|
if (this.diffFile.deleted_file) {
|
|
return sprintf(__('%{filePath} deleted'), { filePath: this.diffFile.file_path }, false);
|
|
}
|
|
|
|
return this.diffFile.file_path;
|
|
},
|
|
isUsingLfs() {
|
|
return this.diffFile.stored_externally && this.diffFile.external_storage === 'lfs';
|
|
},
|
|
isCollapsed() {
|
|
return isCollapsed(this.diffFile, { fileByFile: this.viewDiffsFileByFile });
|
|
},
|
|
collapseIcon() {
|
|
return this.expanded ? 'chevron-down' : 'chevron-right';
|
|
},
|
|
viewFileButtonText() {
|
|
const truncatedContentSha = escape(truncateSha(this.diffFile.content_sha));
|
|
return sprintf(
|
|
s__('MergeRequests|View file @ %{commitId}'),
|
|
{ commitId: truncatedContentSha },
|
|
false,
|
|
);
|
|
},
|
|
viewReplacedFileButtonText() {
|
|
const truncatedBaseSha = escape(truncateSha(this.diffFile.diff_refs.base_sha));
|
|
return sprintf(s__('MergeRequests|View replaced file @ %{commitId}'), {
|
|
commitId: truncatedBaseSha,
|
|
});
|
|
},
|
|
gfmCopyText() {
|
|
return `\`${this.diffFile.file_path}\``;
|
|
},
|
|
isFileRenamed() {
|
|
return this.diffFile.renamed_file;
|
|
},
|
|
isModeChanged() {
|
|
return this.diffFile.mode_changed;
|
|
},
|
|
expandDiffToFullFileTitle() {
|
|
if (this.diffFile.isShowingFullFile) {
|
|
return s__('MRDiff|Show changes only');
|
|
}
|
|
return s__('MRDiff|Show full file');
|
|
},
|
|
showEditButton() {
|
|
return (
|
|
this.diffFile.blob?.readable_text &&
|
|
!this.diffFile.deleted_file &&
|
|
(this.diffFile.edit_path || this.diffFile.ide_edit_path)
|
|
);
|
|
},
|
|
isReviewable() {
|
|
return reviewable(this.diffFile);
|
|
},
|
|
externalUrlLabel() {
|
|
return sprintf(__('View on %{url}'), { url: this.diffFile.formatted_external_url });
|
|
},
|
|
},
|
|
watch: {
|
|
'idState.moreActionsShown': {
|
|
handler(val) {
|
|
const el = this.$el.closest('.vue-recycle-scroller__item-view');
|
|
|
|
if (el) {
|
|
// We can't add a style with Vue because of the way the virtual
|
|
// scroller library renders the diff files
|
|
el.style.zIndex = val ? '1' : null;
|
|
}
|
|
},
|
|
},
|
|
},
|
|
methods: {
|
|
...mapActions('diffs', [
|
|
'toggleFileDiscussions',
|
|
'toggleFileDiscussionWrappers',
|
|
'toggleFullDiff',
|
|
'setCurrentFileHash',
|
|
'reviewFile',
|
|
'setFileCollapsedByUser',
|
|
]),
|
|
handleToggleFile() {
|
|
this.$emit('toggleFile');
|
|
},
|
|
showForkMessage(e) {
|
|
if (this.canCurrentUserFork && !this.diffFile.can_modify_blob) {
|
|
e.preventDefault();
|
|
this.$emit('showForkMessage');
|
|
}
|
|
},
|
|
handleFileNameClick(e) {
|
|
const isLinkToOtherPage =
|
|
this.diffFile.submodule_tree_url || this.diffFile.submodule_link || this.discussionPath;
|
|
|
|
if (!isLinkToOtherPage) {
|
|
e.preventDefault();
|
|
const selector = this.diffContentIDSelector;
|
|
scrollToElement(document.querySelector(selector));
|
|
window.location.hash = selector;
|
|
if (!this.viewDiffsFileByFile) {
|
|
this.setCurrentFileHash(this.diffFile.file_hash);
|
|
}
|
|
}
|
|
},
|
|
setMoreActionsShown(val) {
|
|
this.idState.moreActionsShown = val;
|
|
},
|
|
toggleReview(newReviewedStatus) {
|
|
const autoCollapsed =
|
|
this.isCollapsed && collapsedType(this.diffFile) === DIFF_FILE_AUTOMATIC_COLLAPSE;
|
|
const open = this.expanded;
|
|
const closed = !open;
|
|
const reviewed = newReviewedStatus;
|
|
|
|
this.reviewFile({ file: this.diffFile, reviewed });
|
|
|
|
if (reviewed && autoCollapsed) {
|
|
this.setFileCollapsedByUser({
|
|
filePath: this.diffFile.file_path,
|
|
collapsed: true,
|
|
});
|
|
}
|
|
|
|
if ((open && reviewed) || (closed && !reviewed)) {
|
|
this.$emit('toggleFile');
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
ref="header"
|
|
:class="{
|
|
'gl-z-dropdown-menu!': idState.moreActionsShown,
|
|
'is-sidebar-moved': glFeatures.movedMrSidebar,
|
|
}"
|
|
class="js-file-title file-title file-title-flex-parent"
|
|
data-qa-selector="file_title_container"
|
|
:data-qa-file-name="filePath"
|
|
@click.self="handleToggleFile"
|
|
>
|
|
<div class="file-header-content">
|
|
<gl-icon
|
|
v-if="collapsible"
|
|
ref="collapseIcon"
|
|
:name="collapseIcon"
|
|
:size="16"
|
|
class="diff-toggle-caret gl-mr-2"
|
|
@click.stop="handleToggleFile"
|
|
/>
|
|
<a
|
|
ref="titleWrapper"
|
|
:v-once="!viewDiffsFileByFile"
|
|
class="gl-mr-2 gl-text-decoration-none! gl-word-break-all"
|
|
:href="titleLink"
|
|
@click="handleFileNameClick"
|
|
>
|
|
<span v-if="isFileRenamed">
|
|
<strong
|
|
v-gl-tooltip
|
|
v-safe-html="diffFile.old_path_html"
|
|
:title="diffFile.old_path"
|
|
class="file-title-name"
|
|
></strong>
|
|
→
|
|
<strong
|
|
v-gl-tooltip
|
|
v-safe-html="diffFile.new_path_html"
|
|
:title="diffFile.new_path"
|
|
class="file-title-name"
|
|
></strong>
|
|
</span>
|
|
|
|
<strong
|
|
v-else
|
|
v-gl-tooltip
|
|
:title="filePath"
|
|
class="file-title-name"
|
|
data-container="body"
|
|
data-qa-selector="file_name_content"
|
|
>
|
|
{{ filePath }}
|
|
</strong>
|
|
</a>
|
|
|
|
<clipboard-button
|
|
:title="__('Copy file path')"
|
|
:text="diffFile.file_path"
|
|
:gfm="gfmCopyText"
|
|
size="small"
|
|
data-testid="diff-file-copy-clipboard"
|
|
category="tertiary"
|
|
data-track-action="click_copy_file_button"
|
|
data-track-label="diff_copy_file_path_button"
|
|
data-track-property="diff_copy_file"
|
|
/>
|
|
|
|
<small v-if="isModeChanged" ref="fileMode" class="mr-1">
|
|
{{ diffFile.a_mode }} → {{ diffFile.b_mode }}
|
|
</small>
|
|
|
|
<gl-badge v-if="isUsingLfs" variant="neutral" class="gl-mr-2" data-testid="label-lfs">{{
|
|
__('LFS')
|
|
}}</gl-badge>
|
|
</div>
|
|
|
|
<div
|
|
v-if="!diffFile.submodule && addMergeRequestButtons"
|
|
class="file-actions d-flex align-items-center gl-ml-auto gl-align-self-start"
|
|
>
|
|
<diff-stats
|
|
:diff-file="diffFile"
|
|
:added-lines="diffFile.added_lines"
|
|
:removed-lines="diffFile.removed_lines"
|
|
/>
|
|
<gl-form-checkbox
|
|
v-if="isReviewable && showLocalFileReviews"
|
|
v-gl-tooltip.hover
|
|
data-testid="fileReviewCheckbox"
|
|
class="gl-mr-5 gl-display-flex gl-align-items-center"
|
|
:title="$options.i18n.fileReviewTooltip"
|
|
:checked="reviewed"
|
|
@change="toggleReview"
|
|
>
|
|
{{ $options.i18n.fileReviewLabel }}
|
|
</gl-form-checkbox>
|
|
<gl-button-group class="gl-pt-0!">
|
|
<gl-button
|
|
v-if="diffFile.external_url"
|
|
ref="externalLink"
|
|
v-gl-tooltip.hover
|
|
:href="diffFile.external_url"
|
|
:title="externalUrlLabel"
|
|
:aria-label="externalUrlLabel"
|
|
target="_blank"
|
|
data-track-action="click_toggle_external_button"
|
|
data-track-label="diff_toggle_external_button"
|
|
data-track-property="diff_toggle_external"
|
|
icon="external-link"
|
|
/>
|
|
<gl-dropdown
|
|
v-gl-tooltip.hover.focus="$options.i18n.optionsDropdownTitle"
|
|
size="small"
|
|
category="tertiary"
|
|
right
|
|
toggle-class="btn-icon js-diff-more-actions"
|
|
class="gl-pt-0!"
|
|
data-qa-selector="dropdown_button"
|
|
@show="setMoreActionsShown(true)"
|
|
@hidden="setMoreActionsShown(false)"
|
|
>
|
|
<template #button-content>
|
|
<gl-icon name="ellipsis_v" class="mr-0" />
|
|
<span class="sr-only">{{ $options.i18n.optionsDropdownTitle }}</span>
|
|
</template>
|
|
<gl-dropdown-item
|
|
v-if="diffFile.replaced_view_path"
|
|
ref="replacedFileButton"
|
|
:href="diffFile.replaced_view_path"
|
|
target="_blank"
|
|
>
|
|
{{ viewReplacedFileButtonText }}
|
|
</gl-dropdown-item>
|
|
<gl-dropdown-item ref="viewButton" :href="diffFile.view_path" target="_blank">
|
|
{{ viewFileButtonText }}
|
|
</gl-dropdown-item>
|
|
<template v-if="showEditButton">
|
|
<gl-dropdown-item
|
|
v-if="diffFile.edit_path"
|
|
ref="editButton"
|
|
:href="diffFile.edit_path"
|
|
class="js-edit-blob"
|
|
@click="showForkMessage"
|
|
>
|
|
{{ __('Edit in single-file editor') }}
|
|
</gl-dropdown-item>
|
|
<gl-dropdown-item
|
|
v-if="diffFile.edit_path"
|
|
ref="ideEditButton"
|
|
:href="diffFile.ide_edit_path"
|
|
class="js-ide-edit-blob"
|
|
data-qa-selector="edit_in_ide_button"
|
|
>
|
|
{{ __('Open in Web IDE') }}
|
|
</gl-dropdown-item>
|
|
</template>
|
|
|
|
<template v-if="!isCollapsed">
|
|
<gl-dropdown-divider
|
|
v-if="!diffFile.is_fully_expanded || diffHasDiscussions(diffFile)"
|
|
/>
|
|
|
|
<gl-dropdown-item
|
|
v-if="diffHasDiscussions(diffFile)"
|
|
ref="toggleDiscussionsButton"
|
|
data-qa-selector="toggle_comments_button"
|
|
@click="toggleFileDiscussionWrappers(diffFile)"
|
|
>
|
|
<template v-if="diffHasExpandedDiscussions(diffFile)">
|
|
{{ __('Hide comments on this file') }}
|
|
</template>
|
|
<template v-else>
|
|
{{ __('Show comments on this file') }}
|
|
</template>
|
|
</gl-dropdown-item>
|
|
<gl-dropdown-item
|
|
v-if="!diffFile.is_fully_expanded"
|
|
ref="expandDiffToFullFileButton"
|
|
:disabled="diffFile.isLoadingFullFile"
|
|
@click="toggleFullDiff(diffFile.file_path)"
|
|
>
|
|
<gl-loading-icon v-if="diffFile.isLoadingFullFile" size="sm" inline />
|
|
{{ expandDiffToFullFileTitle }}
|
|
</gl-dropdown-item>
|
|
</template>
|
|
</gl-dropdown>
|
|
</gl-button-group>
|
|
</div>
|
|
|
|
<div
|
|
v-if="diffFile.submodule_compare"
|
|
class="file-actions d-none d-sm-flex align-items-center flex-wrap"
|
|
>
|
|
<gl-button
|
|
v-gl-tooltip.hover
|
|
v-safe-html="submoduleDiffCompareLinkText"
|
|
class="submodule-compare"
|
|
:title="$options.i18n.compareButtonLabel"
|
|
:aria-label="$options.i18n.compareButtonLabel"
|
|
:href="diffFile.submodule_compare.url"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|