<script> import { mapState, mapGetters, mapActions } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; import Mousetrap from 'mousetrap'; import Icon from '~/vue_shared/components/icon.vue'; import { __ } from '~/locale'; import createFlash from '~/flash'; import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../../notes/event_hub'; import CompareVersions from './compare_versions.vue'; import DiffFile from './diff_file.vue'; import NoChanges from './no_changes.vue'; import HiddenFilesWarning from './hidden_files_warning.vue'; import CommitWidget from './commit_widget.vue'; import TreeList from './tree_list.vue'; import { TREE_LIST_WIDTH_STORAGE_KEY, INITIAL_TREE_WIDTH, MIN_TREE_WIDTH, MAX_TREE_WIDTH, TREE_HIDE_STATS_WIDTH, MR_TREE_SHOW_KEY, CENTERED_LIMITED_CONTAINER_CLASSES, } from '../constants'; export default { name: 'DiffsApp', components: { Icon, CompareVersions, DiffFile, NoChanges, HiddenFilesWarning, CommitWidget, TreeList, GlLoadingIcon, PanelResizer, }, mixins: [glFeatureFlagsMixin()], props: { endpoint: { type: String, required: true, }, endpointMetadata: { type: String, required: true, }, endpointBatch: { type: String, required: true, }, projectPath: { type: String, required: true, }, shouldShow: { type: Boolean, required: false, default: false, }, currentUser: { type: Object, required: true, }, helpPagePath: { type: String, required: false, default: '', }, changesEmptyStateIllustration: { type: String, required: false, default: '', }, isFluidLayout: { type: Boolean, required: false, default: false, }, dismissEndpoint: { type: String, required: false, default: '', }, showSuggestPopover: { type: Boolean, required: false, default: false, }, }, data() { const treeWidth = parseInt(localStorage.getItem(TREE_LIST_WIDTH_STORAGE_KEY), 10) || INITIAL_TREE_WIDTH; return { assignedDiscussions: false, treeWidth, }; }, computed: { ...mapState({ isLoading: state => state.diffs.isLoading, isBatchLoading: state => state.diffs.isBatchLoading, diffFiles: state => state.diffs.diffFiles, diffViewType: state => state.diffs.diffViewType, mergeRequestDiffs: state => state.diffs.mergeRequestDiffs, mergeRequestDiff: state => state.diffs.mergeRequestDiff, commit: state => state.diffs.commit, targetBranchName: state => state.diffs.targetBranchName, renderOverflowWarning: state => state.diffs.renderOverflowWarning, numTotalFiles: state => state.diffs.realSize, numVisibleFiles: state => state.diffs.size, plainDiffPath: state => state.diffs.plainDiffPath, emailPatchPath: state => state.diffs.emailPatchPath, }), ...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']), ...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']), ...mapGetters(['isNotesFetched', 'getNoteableData']), targetBranch() { return { branchName: this.targetBranchName, versionIndex: -1, path: '', }; }, canCurrentUserFork() { return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request; }, showCompareVersions() { return this.mergeRequestDiffs && this.mergeRequestDiff; }, renderDiffFiles() { return ( this.diffFiles.length > 0 || (this.startVersion && this.startVersion.version_index === this.mergeRequestDiff.version_index) ); }, hideFileStats() { return this.treeWidth <= TREE_HIDE_STATS_WIDTH; }, isLimitedContainer() { return !this.showTreeList && !this.isParallelView && !this.isFluidLayout; }, shouldSetDiscussions() { return this.isNotesFetched && !this.assignedDiscussions && !this.isLoading; }, }, watch: { diffViewType() { this.adjustView(); }, shouldShow() { // When the shouldShow property changed to true, the route is rendered for the first time // and if we have the isLoading as true this means we didn't fetch the data if (this.isLoading) { this.fetchData(); } this.adjustView(); }, isLoading: 'adjustView', showTreeList: 'adjustView', shouldSetDiscussions(newVal) { if (newVal) { this.setDiscussions(); } }, }, mounted() { this.setBaseConfig({ endpoint: this.endpoint, endpointMetadata: this.endpointMetadata, endpointBatch: this.endpointBatch, projectPath: this.projectPath, dismissEndpoint: this.dismissEndpoint, showSuggestPopover: this.showSuggestPopover, useSingleDiffStyle: this.glFeatures.singleMrDiffView, }); if (this.shouldShow) { this.fetchData(); } const id = window && window.location && window.location.hash; if (id) { this.setHighlightedRow(id.slice(1)); } }, created() { this.adjustView(); eventHub.$once('fetchedNotesData', this.setDiscussions); eventHub.$once('fetchDiffData', this.fetchData); eventHub.$on('refetchDiffData', this.refetchDiffData); this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES; }, beforeDestroy() { eventHub.$off('fetchDiffData', this.fetchData); eventHub.$off('refetchDiffData', this.refetchDiffData); this.removeEventListeners(); }, methods: { ...mapActions(['startTaskList']), ...mapActions('diffs', [ 'setBaseConfig', 'fetchDiffFiles', 'fetchDiffFilesMeta', 'fetchDiffFilesBatch', 'startRenderDiffsQueue', 'assignDiscussionsToDiff', 'setHighlightedRow', 'cacheTreeListWidth', 'scrollToFile', 'toggleShowTreeList', ]), refetchDiffData() { this.assignedDiscussions = false; this.fetchData(false); }, isLatestVersion() { return window.location.search.indexOf('diff_id') === -1; }, startDiffRendering() { requestIdleCallback( () => { this.startRenderDiffsQueue(); }, { timeout: 1000 }, ); }, fetchData(toggleTree = true) { if (this.isLatestVersion() && this.glFeatures.diffsBatchLoad) { this.fetchDiffFilesMeta() .then(() => { if (toggleTree) this.hideTreeListIfJustOneFile(); this.startDiffRendering(); }) .catch(() => { createFlash(__('Something went wrong on our end. Please try again!')); }); this.fetchDiffFilesBatch() .then(() => this.startDiffRendering()) .catch(() => { createFlash(__('Something went wrong on our end. Please try again!')); }); } else { this.fetchDiffFiles() .then(() => { if (toggleTree) { this.hideTreeListIfJustOneFile(); } requestIdleCallback( () => { this.startRenderDiffsQueue(); }, { timeout: 1000 }, ); }) .catch(() => { createFlash(__('Something went wrong on our end. Please try again!')); }); } if (!this.isNotesFetched) { eventHub.$emit('fetchNotesData'); } }, setDiscussions() { if (this.shouldSetDiscussions) { this.assignedDiscussions = true; requestIdleCallback( () => this.assignDiscussionsToDiff() .then(this.$nextTick) .then(this.startTaskList), { timeout: 1000 }, ); } }, adjustView() { if (this.shouldShow) { this.$nextTick(() => { this.setEventListeners(); }); } else { this.removeEventListeners(); } }, setEventListeners() { Mousetrap.bind(['[', 'k', ']', 'j'], (e, combo) => { switch (combo) { case '[': case 'k': this.jumpToFile(-1); break; case ']': case 'j': this.jumpToFile(+1); break; default: break; } }); }, removeEventListeners() { Mousetrap.unbind(['[', 'k', ']', 'j']); }, jumpToFile(step) { const targetIndex = this.currentDiffIndex + step; if (targetIndex >= 0 && targetIndex < this.diffFiles.length) { this.scrollToFile(this.diffFiles[targetIndex].file_path); } }, hideTreeListIfJustOneFile() { const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY); if ((storedTreeShow === null && this.diffFiles.length <= 1) || storedTreeShow === 'false') { this.toggleShowTreeList(false); } }, }, minTreeWidth: MIN_TREE_WIDTH, maxTreeWidth: MAX_TREE_WIDTH, }; </script> <template> <div v-show="shouldShow"> <div v-if="isLoading" class="loading"><gl-loading-icon /></div> <div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane"> <compare-versions :merge-request-diffs="mergeRequestDiffs" :merge-request-diff="mergeRequestDiff" :target-branch="targetBranch" :is-limited-container="isLimitedContainer" /> <hidden-files-warning v-if="renderOverflowWarning" :visible="numVisibleFiles" :total="numTotalFiles" :plain-diff-path="plainDiffPath" :email-patch-path="emailPatchPath" /> <div :data-can-create-note="getNoteableData.current_user.can_create_note" class="files d-flex prepend-top-default" > <div v-show="showTreeList" :style="{ width: `${treeWidth}px` }" class="diff-tree-list js-diff-tree-list mr-3" > <panel-resizer :size.sync="treeWidth" :start-size="treeWidth" :min-size="$options.minTreeWidth" :max-size="$options.maxTreeWidth" side="right" @resize-end="cacheTreeListWidth" /> <tree-list :hide-file-stats="hideFileStats" /> </div> <div class="diff-files-holder" :class="{ [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer, }" > <commit-widget v-if="commit" :commit="commit" /> <div v-if="isBatchLoading" class="loading"><gl-loading-icon /></div> <template v-else-if="renderDiffFiles"> <diff-file v-for="file in diffFiles" :key="file.newPath" :file="file" :help-page-path="helpPagePath" :can-current-user-fork="canCurrentUserFork" /> </template> <no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" /> </div> </div> </div> </div> </template>