<script> import { GlTooltipDirective, GlSafeHtmlDirective, GlIcon, 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 { diffViewerModes } from '~/ide/constants'; 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 FileIcon from '~/vue_shared/components/file_icon.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, FileIcon, DiffStats, GlButton, GlButtonGroup, GlDropdown, GlDropdownItem, GlDropdownDivider, GlFormCheckbox, GlLoadingIcon, }, directives: { GlTooltip: GlTooltipDirective, SafeHtml: GlSafeHtmlDirective, }, mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.diffFile.file_hash })], i18n: { ...DIFF_FILE_HEADER, compareButtonLabel: s__('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( s__('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.viewer.name === diffViewerModes.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 (this.glFeatures.diffsVirtualScrolling && 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', 'toggleActiveFileByHash', '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.toggleActiveFileByHash(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 }" 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" > <file-icon :file-name="filePath" :size="16" aria-hidden="true" css-classes="gl-mr-2" :submodule="diffFile.submodule" /> <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" data-testid="diff-file-copy-clipboard" category="tertiary" data-track-event="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> <span v-if="isUsingLfs" class="badge label label-lfs gl-mr-2"> {{ __('LFS') }} </span> </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-event="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" 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" > {{ __('Edit 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>