Commit 3a9cc78f authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'ph/222790/diffSingleFileView' into 'master'

Added option to view diffs file-by-file

See merge request gitlab-org/gitlab!35223
parents d152c63d 3a0ab2a9
<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);
const discussionFilePath = discussion.diff_file?.file_path;
if (discussionFilePath) {
self.scrollToFile(discussionFilePath);
}
self.$nextTick(() => {
jumpToDiscussion(self, discussion); jumpToDiscussion(self, discussion);
self.setCurrentDiscussionId(targetId); 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
...@@ -15718,7 +15718,8 @@ CREATE TABLE public.user_preferences ( ...@@ -15718,7 +15718,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
...@@ -23705,6 +23706,7 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -23705,6 +23706,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200623000320 20200623000320
20200623090030 20200623090030
20200623121135 20200623121135
20200623141217
20200623141544 20200623141544
20200623170000 20200623170000
20200623185440 20200623185440
......
...@@ -17325,6 +17325,9 @@ msgstr "" ...@@ -17325,6 +17325,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 ""
...@@ -17343,6 +17346,9 @@ msgstr "" ...@@ -17343,6 +17346,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