Commit 3a0ab2a9 authored by Phil Hughes's avatar Phil Hughes

Added option to view diffs file-by-file

This adds a user preference to view diffs file-by-file instead
of viewing all the diff files in one big list.
This helps with performance on large merge request,
but it can also be handy sometimes to view merge requests
file-by-file instead of in a full list of files.

The option to enable this feature is inside of user prefences.
For now this is also behind a feature flag so we can roll
this out safely.

https://gitlab.com/gitlab-org/gitlab/-/issues/222790
parent 0169a9d3
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlButtonGroup, GlButton } from '@gitlab/ui';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import { __ } from '~/locale'; import { __ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -36,6 +36,8 @@ export default { ...@@ -36,6 +36,8 @@ export default {
TreeList, TreeList,
GlLoadingIcon, GlLoadingIcon,
PanelResizer, PanelResizer,
GlButtonGroup,
GlButton,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
props: { props: {
...@@ -94,6 +96,11 @@ export default { ...@@ -94,6 +96,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
viewDiffsFileByFile: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
const treeWidth = const treeWidth =
...@@ -120,9 +127,18 @@ export default { ...@@ -120,9 +127,18 @@ export default {
emailPatchPath: state => state.diffs.emailPatchPath, emailPatchPath: state => state.diffs.emailPatchPath,
retrievingBatches: state => state.diffs.retrievingBatches, retrievingBatches: state => state.diffs.retrievingBatches,
}), }),
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']), ...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion', 'currentDiffFileId']),
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']), ...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']), ...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
if (!this.viewDiffsFileByFile) {
return this.diffFiles;
}
return this.diffFiles.filter((file, i) => {
return file.file_hash === this.currentDiffFileId || (i === 0 && !this.currentDiffFileId);
});
},
canCurrentUserFork() { canCurrentUserFork() {
return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request; return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request;
}, },
...@@ -183,16 +199,22 @@ export default { ...@@ -183,16 +199,22 @@ export default {
dismissEndpoint: this.dismissEndpoint, dismissEndpoint: this.dismissEndpoint,
showSuggestPopover: this.showSuggestPopover, showSuggestPopover: this.showSuggestPopover,
useSingleDiffStyle: this.glFeatures.singleMrDiffView, useSingleDiffStyle: this.glFeatures.singleMrDiffView,
viewDiffsFileByFile: this.viewDiffsFileByFile,
}); });
if (this.shouldShow) { if (this.shouldShow) {
this.fetchData(); this.fetchData();
} }
const id = window && window.location && window.location.hash; const id = window?.location?.hash;
if (id) { if (id && id.indexOf('#note') !== 0) {
this.setHighlightedRow(id.slice(1)); this.setHighlightedRow(
id
.split('diff-content')
.pop()
.slice(1),
);
} }
}, },
created() { created() {
...@@ -236,6 +258,7 @@ export default { ...@@ -236,6 +258,7 @@ export default {
'cacheTreeListWidth', 'cacheTreeListWidth',
'scrollToFile', 'scrollToFile',
'toggleShowTreeList', 'toggleShowTreeList',
'navigateToDiffFileIndex',
]), ]),
refetchDiffData() { refetchDiffData() {
this.fetchData(false); this.fetchData(false);
...@@ -422,12 +445,31 @@ export default { ...@@ -422,12 +445,31 @@ export default {
<div v-if="isBatchLoading" class="loading"><gl-loading-icon size="lg" /></div> <div v-if="isBatchLoading" class="loading"><gl-loading-icon size="lg" /></div>
<template v-else-if="renderDiffFiles"> <template v-else-if="renderDiffFiles">
<diff-file <diff-file
v-for="file in diffFiles" v-for="file in diffs"
:key="file.newPath" :key="file.newPath"
:file="file" :file="file"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork" :can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
/> />
<div v-if="viewDiffsFileByFile" class="d-flex gl-justify-content-center">
<gl-button-group>
<gl-button
:disabled="currentDiffIndex === 0"
data-testid="singleFilePrevious"
@click="navigateToDiffFileIndex(currentDiffIndex - 1)"
>
{{ __('Prev') }}
</gl-button>
<gl-button
:disabled="currentDiffIndex === diffFiles.length - 1"
data-testid="singleFileNext"
@click="navigateToDiffFileIndex(currentDiffIndex + 1)"
>
{{ __('Next') }}
</gl-button>
</gl-button-group>
</div>
</template> </template>
<no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" /> <no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" />
</div> </div>
......
...@@ -30,6 +30,10 @@ export default { ...@@ -30,6 +30,10 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
viewDiffsFileByFile: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -154,6 +158,7 @@ export default { ...@@ -154,6 +158,7 @@ export default {
:collapsible="true" :collapsible="true"
:expanded="!isCollapsed" :expanded="!isCollapsed"
:add-merge-request-buttons="true" :add-merge-request-buttons="true"
:view-diffs-file-by-file="viewDiffsFileByFile"
class="js-file-title file-title" class="js-file-title file-title"
@toggleFile="handleToggle" @toggleFile="handleToggle"
@showForkMessage="showForkMessage" @showForkMessage="showForkMessage"
......
...@@ -54,6 +54,11 @@ export default { ...@@ -54,6 +54,11 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
viewDiffsFileByFile: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']), ...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
...@@ -166,7 +171,13 @@ export default { ...@@ -166,7 +171,13 @@ export default {
class="diff-toggle-caret gl-mr-2" class="diff-toggle-caret gl-mr-2"
@click.stop="handleToggleFile" @click.stop="handleToggleFile"
/> />
<a v-once ref="titleWrapper" class="gl-mr-2" :href="titleLink" @click="handleFileNameClick"> <a
ref="titleWrapper"
:v-once="!viewDiffsFileByFile"
class="gl-mr-2"
:href="titleLink"
@click="handleFileNameClick"
>
<file-icon :file-name="filePath" :size="18" aria-hidden="true" css-classes="gl-mr-2" /> <file-icon :file-name="filePath" :size="18" aria-hidden="true" css-classes="gl-mr-2" />
<span v-if="isFileRenamed"> <span v-if="isFileRenamed">
<strong <strong
......
...@@ -23,6 +23,11 @@ export default { ...@@ -23,6 +23,11 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
currentDiffFileId: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
showFileRowStats() { showFileRowStats() {
...@@ -33,7 +38,13 @@ export default { ...@@ -33,7 +38,13 @@ export default {
</script> </script>
<template> <template>
<file-row :file="file" v-bind="$attrs" v-on="$listeners"> <file-row
:file="file"
v-bind="$attrs"
:class="{ 'is-active': currentDiffFileId === file.fileHash }"
class="diff-file-row"
v-on="$listeners"
>
<file-row-stats v-if="showFileRowStats" :file="file" class="mr-1" /> <file-row-stats v-if="showFileRowStats" :file="file" class="mr-1" />
<changed-file-icon :file="file" :size="16" :show-tooltip="true" /> <changed-file-icon :file="file" :size="16" :show-tooltip="true" />
</file-row> </file-row>
......
...@@ -26,7 +26,7 @@ export default { ...@@ -26,7 +26,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState('diffs', ['tree', 'renderTreeList']), ...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId']),
...mapGetters('diffs', ['allBlobs']), ...mapGetters('diffs', ['allBlobs']),
filteredTreeList() { filteredTreeList() {
const search = this.search.toLowerCase().trim(); const search = this.search.toLowerCase().trim();
...@@ -96,6 +96,7 @@ export default { ...@@ -96,6 +96,7 @@ export default {
:level="0" :level="0"
:hide-file-stats="hideFileStats" :hide-file-stats="hideFileStats"
:file-row-component="$options.DiffFileRow" :file-row-component="$options.DiffFileRow"
:current-diff-file-id="currentDiffFileId"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile" @clickFile="scrollToFile"
/> />
......
...@@ -78,6 +78,7 @@ export default function initDiffsApp(store) { ...@@ -78,6 +78,7 @@ export default function initDiffsApp(store) {
dismissEndpoint: dataset.dismissEndpoint, dismissEndpoint: dataset.dismissEndpoint,
showSuggestPopover: parseBoolean(dataset.showSuggestPopover), showSuggestPopover: parseBoolean(dataset.showSuggestPopover),
showWhitespaceDefault: parseBoolean(dataset.showWhitespaceDefault), showWhitespaceDefault: parseBoolean(dataset.showWhitespaceDefault),
viewDiffsFileByFile: parseBoolean(dataset.fileByFileDefault),
}; };
}, },
computed: { computed: {
...@@ -115,6 +116,7 @@ export default function initDiffsApp(store) { ...@@ -115,6 +116,7 @@ export default function initDiffsApp(store) {
isFluidLayout: this.isFluidLayout, isFluidLayout: this.isFluidLayout,
dismissEndpoint: this.dismissEndpoint, dismissEndpoint: this.dismissEndpoint,
showSuggestPopover: this.showSuggestPopover, showSuggestPopover: this.showSuggestPopover,
viewDiffsFileByFile: this.viewDiffsFileByFile,
}, },
}); });
}, },
......
...@@ -105,7 +105,9 @@ export const fetchDiffFiles = ({ state, commit }) => { ...@@ -105,7 +105,9 @@ export const fetchDiffFiles = ({ state, commit }) => {
.catch(() => worker.terminate()); .catch(() => worker.terminate());
}; };
export const fetchDiffFilesBatch = ({ commit, state }) => { export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
const id = window?.location?.hash;
const isNoteLink = id.indexOf('#note') === 0;
const urlParams = { const urlParams = {
per_page: DIFFS_PER_PAGE, per_page: DIFFS_PER_PAGE,
w: state.showWhitespace ? '0' : '1', w: state.showWhitespace ? '0' : '1',
...@@ -125,8 +127,26 @@ export const fetchDiffFilesBatch = ({ commit, state }) => { ...@@ -125,8 +127,26 @@ export const fetchDiffFilesBatch = ({ commit, state }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files }); commit(types.SET_DIFF_DATA_BATCH, { diff_files });
commit(types.SET_BATCH_LOADING, false); commit(types.SET_BATCH_LOADING, false);
if (!isNoteLink && !state.currentDiffFileId) {
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, diff_files[0].file_hash);
}
if (isNoteLink) {
dispatch('setCurrentDiffFileIdFromNote', id.split('_').pop());
}
if (!pagination.next_page) { if (!pagination.next_page) {
commit(types.SET_RETRIEVING_BATCHES, false); commit(types.SET_RETRIEVING_BATCHES, false);
// We need to check that the currentDiffFileId points to a file that exists
if (
state.currentDiffFileId &&
!state.diffFiles.some(f => f.file_hash === state.currentDiffFileId) &&
!isNoteLink
) {
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, state.diffFiles[0].file_hash);
}
if (gon.features?.codeNavigation) { if (gon.features?.codeNavigation) {
// eslint-disable-next-line promise/catch-or-return,promise/no-nesting // eslint-disable-next-line promise/catch-or-return,promise/no-nesting
import('~/code_navigation').then(m => import('~/code_navigation').then(m =>
...@@ -450,6 +470,8 @@ export const toggleTreeOpen = ({ commit }, path) => { ...@@ -450,6 +470,8 @@ export const toggleTreeOpen = ({ commit }, path) => {
}; };
export const scrollToFile = ({ state, commit }, path) => { export const scrollToFile = ({ state, commit }, path) => {
if (!state.treeEntries[path]) return;
const { fileHash } = state.treeEntries[path]; const { fileHash } = state.treeEntries[path];
document.location.hash = fileHash; document.location.hash = fileHash;
...@@ -713,5 +735,19 @@ export function moveToNeighboringCommit({ dispatch, state }, { direction }) { ...@@ -713,5 +735,19 @@ export function moveToNeighboringCommit({ dispatch, state }, { direction }) {
} }
} }
export const setCurrentDiffFileIdFromNote = ({ commit, rootGetters }, noteId) => {
const fileHash = rootGetters.getDiscussion(rootGetters.notesById[noteId].discussion_id).diff_file
.file_hash;
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
};
export const navigateToDiffFileIndex = ({ commit, state }, index) => {
const fileHash = state.diffFiles[index].file_hash;
document.location.hash = fileHash;
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -78,8 +78,16 @@ function handleDiscussionJump(self, fn, discussionId = self.currentDiscussionId) ...@@ -78,8 +78,16 @@ function handleDiscussionJump(self, fn, discussionId = self.currentDiscussionId)
const isDiffView = window.mrTabs.currentAction === 'diffs'; const isDiffView = window.mrTabs.currentAction === 'diffs';
const targetId = fn(discussionId, isDiffView); const targetId = fn(discussionId, isDiffView);
const discussion = self.getDiscussion(targetId); const discussion = self.getDiscussion(targetId);
jumpToDiscussion(self, discussion); const discussionFilePath = discussion.diff_file?.file_path;
self.setCurrentDiscussionId(targetId);
if (discussionFilePath) {
self.scrollToFile(discussionFilePath);
}
self.$nextTick(() => {
jumpToDiscussion(self, discussion);
self.setCurrentDiscussionId(targetId);
});
} }
export default { export default {
...@@ -95,6 +103,7 @@ export default { ...@@ -95,6 +103,7 @@ export default {
}, },
methods: { methods: {
...mapActions(['expandDiscussion', 'setCurrentDiscussionId']), ...mapActions(['expandDiscussion', 'setCurrentDiscussionId']),
...mapActions('diffs', ['scrollToFile']),
jumpToNextDiscussion() { jumpToNextDiscussion() {
handleDiscussionJump(this, this.nextUnresolvedDiscussionId); handleDiscussionJump(this, this.nextUnresolvedDiscussionId);
......
...@@ -1036,3 +1036,7 @@ $mr-widget-min-height: 69px; ...@@ -1036,3 +1036,7 @@ $mr-widget-min-height: 69px;
} }
} }
} }
.diff-file-row.is-active {
background-color: $gray-50;
}
...@@ -48,6 +48,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController ...@@ -48,6 +48,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:time_display_relative, :time_display_relative,
:time_format_in_24h, :time_format_in_24h,
:show_whitespace_in_diffs, :show_whitespace_in_diffs,
:view_diffs_file_by_file,
:tab_width, :tab_width,
:sourcegraph_enabled, :sourcegraph_enabled,
:render_whitespace_in_code :render_whitespace_in_code
......
...@@ -84,6 +84,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -84,6 +84,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar') @issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
@current_user_data = UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json @current_user_data = UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json
@show_whitespace_default = current_user.nil? || current_user.show_whitespace_in_diffs @show_whitespace_default = current_user.nil? || current_user.show_whitespace_in_diffs
@file_by_file_default = Feature.enabled?(:view_diffs_file_by_file) && current_user&.view_diffs_file_by_file
@coverage_path = coverage_reports_project_merge_request_path(@project, @merge_request, format: :json) if @merge_request.has_coverage_reports? @coverage_path = coverage_reports_project_merge_request_path(@project, @merge_request, format: :json) if @merge_request.has_coverage_reports?
set_pipeline_variables set_pipeline_variables
......
...@@ -271,6 +271,7 @@ class User < ApplicationRecord ...@@ -271,6 +271,7 @@ class User < ApplicationRecord
:time_display_relative, :time_display_relative=, :time_display_relative, :time_display_relative=,
:time_format_in_24h, :time_format_in_24h=, :time_format_in_24h, :time_format_in_24h=,
:show_whitespace_in_diffs, :show_whitespace_in_diffs=, :show_whitespace_in_diffs, :show_whitespace_in_diffs=,
:view_diffs_file_by_file, :view_diffs_file_by_file=,
:tab_width, :tab_width=, :tab_width, :tab_width=,
:sourcegraph_enabled, :sourcegraph_enabled=, :sourcegraph_enabled, :sourcegraph_enabled=,
:setup_for_company, :setup_for_company=, :setup_for_company, :setup_for_company=,
......
...@@ -70,6 +70,13 @@ ...@@ -70,6 +70,13 @@
= f.check_box :show_whitespace_in_diffs, class: 'form-check-input' = f.check_box :show_whitespace_in_diffs, class: 'form-check-input'
= f.label :show_whitespace_in_diffs, class: 'form-check-label' do = f.label :show_whitespace_in_diffs, class: 'form-check-label' do
= s_('Preferences|Show whitespace changes in diffs') = s_('Preferences|Show whitespace changes in diffs')
- if Feature.enabled?(:view_diffs_file_by_file)
.form-group.form-check
= f.check_box :view_diffs_file_by_file, class: 'form-check-input'
= f.label :view_diffs_file_by_file, class: 'form-check-label' do
= s_("Preferences|Show one file at a time on merge request's Changes tab")
.form-text.text-muted
= s_("Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser.")
.form-group .form-group
= f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold' = f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold'
= f.number_field :tab_width, = f.number_field :tab_width,
......
...@@ -88,7 +88,8 @@ ...@@ -88,7 +88,8 @@
is_fluid_layout: fluid_layout.to_s, is_fluid_layout: fluid_layout.to_s,
dismiss_endpoint: user_callouts_path, dismiss_endpoint: user_callouts_path,
show_suggest_popover: show_suggest_popover?.to_s, show_suggest_popover: show_suggest_popover?.to_s,
show_whitespace_default: @show_whitespace_default.to_s } show_whitespace_default: @show_whitespace_default.to_s,
file_by_file_default: @file_by_file_default.to_s }
.mr-loading-status .mr-loading-status
.loading.hide .loading.hide
......
---
title: Allow diffs to be viewed file-by-file
merge_request: 35223
author: rinslow
type: added
# frozen_string_literal: true
class AddViewDiffsFileByFileToUserPreferences < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
with_lock_retries do
add_column :user_preferences, :view_diffs_file_by_file, :boolean, default: false, null: false
end
end
def down
with_lock_retries do
remove_column :user_preferences, :view_diffs_file_by_file, :boolean
end
end
end
...@@ -15717,7 +15717,8 @@ CREATE TABLE public.user_preferences ( ...@@ -15717,7 +15717,8 @@ CREATE TABLE public.user_preferences (
render_whitespace_in_code boolean, render_whitespace_in_code boolean,
tab_width smallint, tab_width smallint,
feature_filter_type bigint, feature_filter_type bigint,
experience_level smallint experience_level smallint,
view_diffs_file_by_file boolean DEFAULT false NOT NULL
); );
CREATE SEQUENCE public.user_preferences_id_seq CREATE SEQUENCE public.user_preferences_id_seq
...@@ -23704,6 +23705,7 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -23704,6 +23705,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200623000320 20200623000320
20200623090030 20200623090030
20200623121135 20200623121135
20200623141217
20200623141544 20200623141544
20200623170000 20200623170000
20200623185440 20200623185440
......
...@@ -17277,6 +17277,9 @@ msgstr "" ...@@ -17277,6 +17277,9 @@ msgstr ""
msgid "Preferences|For example: 30 mins ago." msgid "Preferences|For example: 30 mins ago."
msgstr "" msgstr ""
msgid "Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser."
msgstr ""
msgid "Preferences|Integrations" msgid "Preferences|Integrations"
msgstr "" msgstr ""
...@@ -17295,6 +17298,9 @@ msgstr "" ...@@ -17295,6 +17298,9 @@ msgstr ""
msgid "Preferences|Render whitespace characters in the Web IDE" msgid "Preferences|Render whitespace characters in the Web IDE"
msgstr "" msgstr ""
msgid "Preferences|Show one file at a time on merge request's Changes tab"
msgstr ""
msgid "Preferences|Show whitespace changes in diffs" msgid "Preferences|Show whitespace changes in diffs"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User views diffs file-by-file', :js do
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
end
let(:project) { create(:project, :repository) }
let(:user) { create(:user, view_diffs_file_by_file: true) }
before do
project.add_developer(user)
sign_in(user)
visit(diffs_project_merge_request_path(project, merge_request))
wait_for_requests
end
it 'shows diffs file-by-file' do
page.within('#diffs') do
expect(page).to have_selector('.file-holder', count: 1)
expect(page).to have_selector('.diff-file .file-title', text: '.DS_Store')
click_button 'Next'
expect(page).to have_selector('.file-holder', count: 1)
expect(page).to have_selector('.diff-file .file-title', text: '.gitignore')
end
end
end
...@@ -56,6 +56,7 @@ describe('diffs/components/app', () => { ...@@ -56,6 +56,7 @@ describe('diffs/components/app', () => {
changesEmptyStateIllustration: '', changesEmptyStateIllustration: '',
dismissEndpoint: '', dismissEndpoint: '',
showSuggestPopover: true, showSuggestPopover: true,
viewDiffsFileByFile: false,
...props, ...props,
}, },
provide, provide,
...@@ -829,4 +830,58 @@ describe('diffs/components/app', () => { ...@@ -829,4 +830,58 @@ describe('diffs/components/app', () => {
expect(toggleShowTreeList).not.toHaveBeenCalled(); expect(toggleShowTreeList).not.toHaveBeenCalled();
}); });
}); });
describe('file-by-file', () => {
it('renders a single diff', () => {
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
state.diffs.diffFiles.push({ file_hash: '123' });
state.diffs.diffFiles.push({ file_hash: '312' });
});
expect(wrapper.findAll(DiffFile).length).toBe(1);
});
describe('pagination', () => {
it('sets previous button as disabled', () => {
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
});
expect(wrapper.find('[data-testid="singleFilePrevious"]').props('disabled')).toBe(true);
expect(wrapper.find('[data-testid="singleFileNext"]').props('disabled')).toBe(false);
});
it('sets next button as disabled', () => {
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
state.diffs.currentDiffFileId = '312';
});
expect(wrapper.find('[data-testid="singleFilePrevious"]').props('disabled')).toBe(false);
expect(wrapper.find('[data-testid="singleFileNext"]').props('disabled')).toBe(true);
});
it.each`
currentDiffFileId | button | index
${'123'} | ${'singleFileNext'} | ${1}
${'312'} | ${'singleFilePrevious'} | ${0}
`(
'it calls navigateToDiffFileIndex with $index when $button is clicked',
({ currentDiffFileId, button, index }) => {
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
state.diffs.currentDiffFileId = currentDiffFileId;
});
jest.spyOn(wrapper.vm, 'navigateToDiffFileIndex');
wrapper.find(`[data-testid="${button}"]`).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith(index);
});
},
);
});
});
}); });
...@@ -87,6 +87,7 @@ describe('DiffFileHeader component', () => { ...@@ -87,6 +87,7 @@ describe('DiffFileHeader component', () => {
propsData: { propsData: {
diffFile, diffFile,
canCurrentUserFork: false, canCurrentUserFork: false,
viewDiffsFileByFile: false,
...props, ...props,
}, },
localVue, localVue,
......
...@@ -73,4 +73,15 @@ describe('Diff File Row component', () => { ...@@ -73,4 +73,15 @@ describe('Diff File Row component', () => {
expect(wrapper.find(FileRowStats).exists()).toEqual(value); expect(wrapper.find(FileRowStats).exists()).toEqual(value);
}); });
}); });
it('adds is-active class when currentDiffFileId matches file_hash', () => {
createComponent({
level: 0,
currentDiffFileId: '123',
file: { fileHash: '123' },
hideFileStats: false,
});
expect(wrapper.classes('is-active')).toBe(true);
});
}); });
...@@ -15,6 +15,7 @@ describe('DiffFile', () => { ...@@ -15,6 +15,7 @@ describe('DiffFile', () => {
vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
file: JSON.parse(JSON.stringify(diffFileMockDataReadable)), file: JSON.parse(JSON.stringify(diffFileMockDataReadable)),
canCurrentUserFork: false, canCurrentUserFork: false,
viewDiffsFileByFile: false,
}).$mount(); }).$mount();
trackingSpy = mockTracking('_category_', vm.$el, jest.spyOn); trackingSpy = mockTracking('_category_', vm.$el, jest.spyOn);
}); });
...@@ -113,6 +114,7 @@ describe('DiffFile', () => { ...@@ -113,6 +114,7 @@ describe('DiffFile', () => {
vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)), file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)),
canCurrentUserFork: false, canCurrentUserFork: false,
viewDiffsFileByFile: false,
}).$mount(); }).$mount();
vm.renderIt = false; vm.renderIt = false;
...@@ -235,6 +237,7 @@ describe('DiffFile', () => { ...@@ -235,6 +237,7 @@ describe('DiffFile', () => {
vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)), file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)),
canCurrentUserFork: false, canCurrentUserFork: false,
viewDiffsFileByFile: false,
}).$mount(); }).$mount();
jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {}); jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {});
......
...@@ -46,6 +46,8 @@ import { ...@@ -46,6 +46,8 @@ import {
setSuggestPopoverDismissed, setSuggestPopoverDismissed,
changeCurrentCommit, changeCurrentCommit,
moveToNeighboringCommit, moveToNeighboringCommit,
setCurrentDiffFileIdFromNote,
navigateToDiffFileIndex,
} from '~/diffs/store/actions'; } from '~/diffs/store/actions';
import eventHub from '~/notes/event_hub'; import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types'; import * as types from '~/diffs/store/mutation_types';
...@@ -189,8 +191,8 @@ describe('DiffsStoreActions', () => { ...@@ -189,8 +191,8 @@ describe('DiffsStoreActions', () => {
it('should fetch batch diff files', done => { it('should fetch batch diff files', done => {
const endpointBatch = '/fetch/diffs_batch'; const endpointBatch = '/fetch/diffs_batch';
const res1 = { diff_files: [], pagination: { next_page: 2 } }; const res1 = { diff_files: [{ file_hash: 'test' }], pagination: { next_page: 2 } };
const res2 = { diff_files: [], pagination: {} }; const res2 = { diff_files: [{ file_hash: 'test2' }], pagination: {} };
mock mock
.onGet( .onGet(
mergeUrlParams( mergeUrlParams(
...@@ -226,8 +228,10 @@ describe('DiffsStoreActions', () => { ...@@ -226,8 +228,10 @@ describe('DiffsStoreActions', () => {
{ type: types.SET_RETRIEVING_BATCHES, payload: true }, { type: types.SET_RETRIEVING_BATCHES, payload: true },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } }, { type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } },
{ type: types.SET_BATCH_LOADING, payload: false }, { type: types.SET_BATCH_LOADING, payload: false },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: [] } }, { type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test' },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res2.diff_files } },
{ type: types.SET_BATCH_LOADING, payload: false }, { type: types.SET_BATCH_LOADING, payload: false },
{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test2' },
{ type: types.SET_RETRIEVING_BATCHES, payload: false }, { type: types.SET_RETRIEVING_BATCHES, payload: false },
], ],
[], [],
...@@ -311,6 +315,7 @@ describe('DiffsStoreActions', () => { ...@@ -311,6 +315,7 @@ describe('DiffsStoreActions', () => {
showWhitespace: false, showWhitespace: false,
diffViewType: 'inline', diffViewType: 'inline',
useSingleDiffStyle: false, useSingleDiffStyle: false,
currentDiffFileId: null,
}, },
[ [
{ type: types.SET_LOADING, payload: true }, { type: types.SET_LOADING, payload: true },
...@@ -347,8 +352,8 @@ describe('DiffsStoreActions', () => { ...@@ -347,8 +352,8 @@ describe('DiffsStoreActions', () => {
it('should fetch batch diff files', done => { it('should fetch batch diff files', done => {
const endpointBatch = '/fetch/diffs_batch'; const endpointBatch = '/fetch/diffs_batch';
const res1 = { diff_files: [], pagination: { next_page: 2 } }; const res1 = { diff_files: [{ file_hash: 'test' }], pagination: { next_page: 2 } };
const res2 = { diff_files: [], pagination: {} }; const res2 = { diff_files: [{ file_hash: 'test2' }], pagination: {} };
mock mock
.onGet(mergeUrlParams({ per_page: DIFFS_PER_PAGE, w: '1', page: 1 }, endpointBatch)) .onGet(mergeUrlParams({ per_page: DIFFS_PER_PAGE, w: '1', page: 1 }, endpointBatch))
.reply(200, res1) .reply(200, res1)
...@@ -358,14 +363,16 @@ describe('DiffsStoreActions', () => { ...@@ -358,14 +363,16 @@ describe('DiffsStoreActions', () => {
testAction( testAction(
fetchDiffFilesBatch, fetchDiffFilesBatch,
{}, {},
{ endpointBatch, useSingleDiffStyle: false }, { endpointBatch, useSingleDiffStyle: false, currentDiffFileId: null },
[ [
{ type: types.SET_BATCH_LOADING, payload: true }, { type: types.SET_BATCH_LOADING, payload: true },
{ type: types.SET_RETRIEVING_BATCHES, payload: true }, { type: types.SET_RETRIEVING_BATCHES, payload: true },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } }, { type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } },
{ type: types.SET_BATCH_LOADING, payload: false }, { type: types.SET_BATCH_LOADING, payload: false },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: [] } }, { type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test' },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res2.diff_files } },
{ type: types.SET_BATCH_LOADING, payload: false }, { type: types.SET_BATCH_LOADING, payload: false },
{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test2' },
{ type: types.SET_RETRIEVING_BATCHES, payload: false }, { type: types.SET_RETRIEVING_BATCHES, payload: false },
], ],
[], [],
...@@ -1565,4 +1572,31 @@ describe('DiffsStoreActions', () => { ...@@ -1565,4 +1572,31 @@ describe('DiffsStoreActions', () => {
}, },
); );
}); });
describe('setCurrentDiffFileIdFromNote', () => {
it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => {
const commit = jest.fn();
const rootGetters = {
getDiscussion: () => ({ diff_file: { file_hash: '123' } }),
notesById: { '1': { discussion_id: '2' } },
};
setCurrentDiffFileIdFromNote({ commit, rootGetters }, '1');
expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, '123');
});
});
describe('navigateToDiffFileIndex', () => {
it('commits UPDATE_CURRENT_DIFF_FILE_ID', done => {
testAction(
navigateToDiffFileIndex,
0,
{ diffFiles: [{ file_hash: '123' }] },
[{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: '123' }],
[],
done,
);
});
});
}); });
...@@ -91,6 +91,8 @@ describe('Discussion navigation mixin', () => { ...@@ -91,6 +91,8 @@ describe('Discussion navigation mixin', () => {
beforeEach(() => { beforeEach(() => {
window.mrTabs.currentAction = 'show'; window.mrTabs.currentAction = 'show';
wrapper.vm[fn](...args); wrapper.vm[fn](...args);
return wrapper.vm.$nextTick();
}); });
it('sets current discussion', () => { it('sets current discussion', () => {
...@@ -112,6 +114,8 @@ describe('Discussion navigation mixin', () => { ...@@ -112,6 +114,8 @@ describe('Discussion navigation mixin', () => {
beforeEach(() => { beforeEach(() => {
window.mrTabs.currentAction = 'diffs'; window.mrTabs.currentAction = 'diffs';
wrapper.vm[fn](...args); wrapper.vm[fn](...args);
return wrapper.vm.$nextTick();
}); });
it('sets current discussion', () => { it('sets current discussion', () => {
...@@ -137,6 +141,8 @@ describe('Discussion navigation mixin', () => { ...@@ -137,6 +141,8 @@ describe('Discussion navigation mixin', () => {
beforeEach(() => { beforeEach(() => {
window.mrTabs.currentAction = 'other'; window.mrTabs.currentAction = 'other';
wrapper.vm[fn](...args); wrapper.vm[fn](...args);
return wrapper.vm.$nextTick();
}); });
it('sets current discussion', () => { it('sets current discussion', () => {
......
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