Commit 5a38068a authored by Phil Hughes's avatar Phil Hughes

Merge branch '48798-keybinding-mr-diff' into 'master'

Resolve "Add keybinding for next/previous file in merge request diff"

See merge request gitlab-org/gitlab-ce!25355
parents d40a3809 0aff8e27
...@@ -5,6 +5,7 @@ import { __ } from '~/locale'; ...@@ -5,6 +5,7 @@ import { __ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import Mousetrap from 'mousetrap';
import eventHub from '../../notes/event_hub'; import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue'; import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue'; import DiffFile from './diff_file.vue';
...@@ -87,7 +88,7 @@ export default { ...@@ -87,7 +88,7 @@ export default {
emailPatchPath: state => state.diffs.emailPatchPath, emailPatchPath: state => state.diffs.emailPatchPath,
}), }),
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']), ...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
...mapGetters('diffs', ['isParallelView']), ...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']), ...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() { targetBranch() {
return { return {
...@@ -149,6 +150,7 @@ export default { ...@@ -149,6 +150,7 @@ export default {
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('fetchDiffData', this.fetchData); eventHub.$off('fetchDiffData', this.fetchData);
this.removeEventListeners();
}, },
methods: { methods: {
...mapActions(['startTaskList']), ...mapActions(['startTaskList']),
...@@ -159,6 +161,7 @@ export default { ...@@ -159,6 +161,7 @@ export default {
'assignDiscussionsToDiff', 'assignDiscussionsToDiff',
'setHighlightedRow', 'setHighlightedRow',
'cacheTreeListWidth', 'cacheTreeListWidth',
'scrollToFile',
]), ]),
fetchData() { fetchData() {
this.fetchDiffFiles() this.fetchDiffFiles()
...@@ -197,7 +200,35 @@ export default { ...@@ -197,7 +200,35 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
window.mrTabs.resetViewContainer(); window.mrTabs.resetViewContainer();
window.mrTabs.expandViewContainer(this.showTreeList); window.mrTabs.expandViewContainer(this.showTreeList);
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);
} }
}, },
}, },
......
...@@ -52,7 +52,9 @@ export const fetchDiffFiles = ({ state, commit }) => { ...@@ -52,7 +52,9 @@ export const fetchDiffFiles = ({ state, commit }) => {
}; };
export const setHighlightedRow = ({ commit }, lineCode) => { export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode); commit(types.SET_HIGHLIGHTED_ROW, lineCode);
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
}; };
// This is adding line discussions to the actual lines in the diff tree // This is adding line discussions to the actual lines in the diff tree
...@@ -262,8 +264,6 @@ export const scrollToFile = ({ state, commit }, path) => { ...@@ -262,8 +264,6 @@ export const scrollToFile = ({ state, commit }, path) => {
document.location.hash = fileHash; document.location.hash = fileHash;
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash); commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
setTimeout(() => commit(types.UPDATE_CURRENT_DIFF_FILE_ID, ''), 1000);
}; };
export const toggleShowTreeList = ({ commit, state }) => { export const toggleShowTreeList = ({ commit, state }) => {
......
...@@ -100,5 +100,12 @@ export const diffFilesLength = state => state.diffFiles.length; ...@@ -100,5 +100,12 @@ export const diffFilesLength = state => state.diffFiles.length;
export const getCommentFormForDiffFile = state => fileHash => export const getCommentFormForDiffFile = state => fileHash =>
state.commentForms.find(form => form.fileHash === fileHash); state.commentForms.find(form => form.fileHash === fileHash);
/**
* Returns index of a currently selected diff in diffFiles
* @returns {number}
*/
export const currentDiffIndex = state =>
Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId));
// 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 () => {};
...@@ -356,6 +356,18 @@ ...@@ -356,6 +356,18 @@
%td.shortcut %td.shortcut
%kbd l %kbd l
%td Change Label %td Change Label
%tr
%td.shortcut
%kbd ]
\/
%kbd j
%td Move to next file
%tr
%td.shortcut
%kbd [
\/
%kbd k
%td Move to previous file
%tbody.hidden-shortcut{ style: 'display:none' } %tbody.hidden-shortcut{ style: 'display:none' }
%tr %tr
%th %th
......
---
title: Next/previous navigation between files in MR review
merge_request: 25355
author:
type: added
\ No newline at end of file
...@@ -82,6 +82,8 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' ...@@ -82,6 +82,8 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>r</kbd> | Reply (quoting selected text) | | <kbd>r</kbd> | Reply (quoting selected text) |
| <kbd>e</kbd> | Edit issue/merge request | | <kbd>e</kbd> | Edit issue/merge request |
| <kbd>l</kbd> | Change label | | <kbd>l</kbd> | Change label |
| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file |
| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file |
## Wiki pages ## Wiki pages
......
...@@ -4,12 +4,13 @@ import { TEST_HOST } from 'spec/test_constants'; ...@@ -4,12 +4,13 @@ import { TEST_HOST } from 'spec/test_constants';
import App from '~/diffs/components/app.vue'; import App from '~/diffs/components/app.vue';
import NoChanges from '~/diffs/components/no_changes.vue'; import NoChanges from '~/diffs/components/no_changes.vue';
import DiffFile from '~/diffs/components/diff_file.vue'; import DiffFile from '~/diffs/components/diff_file.vue';
import Mousetrap from 'mousetrap';
import createDiffsStore from '../create_diffs_store'; import createDiffsStore from '../create_diffs_store';
describe('diffs/components/app', () => { describe('diffs/components/app', () => {
const oldMrTabs = window.mrTabs; const oldMrTabs = window.mrTabs;
let store; let store;
let vm; let wrapper;
function createComponent(props = {}, extendStore = () => {}) { function createComponent(props = {}, extendStore = () => {}) {
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -21,7 +22,7 @@ describe('diffs/components/app', () => { ...@@ -21,7 +22,7 @@ describe('diffs/components/app', () => {
extendStore(store); extendStore(store);
vm = shallowMount(localVue.extend(App), { wrapper = shallowMount(localVue.extend(App), {
localVue, localVue,
propsData: { propsData: {
endpoint: `${TEST_HOST}/diff/endpoint`, endpoint: `${TEST_HOST}/diff/endpoint`,
...@@ -38,7 +39,6 @@ describe('diffs/components/app', () => { ...@@ -38,7 +39,6 @@ describe('diffs/components/app', () => {
// setup globals (needed for component to mount :/) // setup globals (needed for component to mount :/)
window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']); window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
window.mrTabs.expandViewContainer = jasmine.createSpy(); window.mrTabs.expandViewContainer = jasmine.createSpy();
window.location.hash = 'ABC_123';
}); });
afterEach(() => { afterEach(() => {
...@@ -46,25 +46,44 @@ describe('diffs/components/app', () => { ...@@ -46,25 +46,44 @@ describe('diffs/components/app', () => {
window.mrTabs = oldMrTabs; window.mrTabs = oldMrTabs;
// reset component // reset component
vm.destroy(); wrapper.destroy();
}); });
it('does not show commit info', () => { it('does not show commit info', () => {
createComponent(); createComponent();
expect(vm.contains('.blob-commit-info')).toBe(false); expect(wrapper.contains('.blob-commit-info')).toBe(false);
}); });
it('sets highlighted row if hash exists in location object', done => { describe('row highlighting', () => {
createComponent({ beforeEach(() => {
shouldShow: true, window.location.hash = 'ABC_123';
}); });
// Component uses $nextTick so we wait until that has finished it('sets highlighted row if hash exists in location object', done => {
setTimeout(() => { createComponent({
expect(store.state.diffs.highlightedRow).toBe('ABC_123'); shouldShow: true,
});
done(); // Component uses $nextTick so we wait until that has finished
setTimeout(() => {
expect(store.state.diffs.highlightedRow).toBe('ABC_123');
done();
});
});
it('marks current diff file based on currently highlighted row', done => {
createComponent({
shouldShow: true,
});
// Component uses $nextTick so we wait until that has finished
setTimeout(() => {
expect(store.state.diffs.currentDiffFileId).toBe('ABC');
done();
});
}); });
}); });
...@@ -76,7 +95,7 @@ describe('diffs/components/app', () => { ...@@ -76,7 +95,7 @@ describe('diffs/components/app', () => {
it('sets initial width when no localStorage has been set', () => { it('sets initial width when no localStorage has been set', () => {
createComponent(); createComponent();
expect(vm.vm.treeWidth).toEqual(320); expect(wrapper.vm.treeWidth).toEqual(320);
}); });
it('sets initial width to localStorage size', () => { it('sets initial width to localStorage size', () => {
...@@ -84,13 +103,26 @@ describe('diffs/components/app', () => { ...@@ -84,13 +103,26 @@ describe('diffs/components/app', () => {
createComponent(); createComponent();
expect(vm.vm.treeWidth).toEqual(200); expect(wrapper.vm.treeWidth).toEqual(200);
}); });
it('sets width of tree list', () => { it('sets width of tree list', () => {
createComponent(); createComponent();
expect(vm.find('.js-diff-tree-list').element.style.width).toEqual('320px'); expect(wrapper.find('.js-diff-tree-list').element.style.width).toEqual('320px');
});
});
it('marks current diff file based on currently highlighted row', done => {
createComponent({
shouldShow: true,
});
// Component uses $nextTick so we wait until that has finished
setTimeout(() => {
expect(store.state.diffs.currentDiffFileId).toBe('ABC');
done();
}); });
}); });
...@@ -98,7 +130,7 @@ describe('diffs/components/app', () => { ...@@ -98,7 +130,7 @@ describe('diffs/components/app', () => {
it('renders empty state when no diff files exist', () => { it('renders empty state when no diff files exist', () => {
createComponent(); createComponent();
expect(vm.contains(NoChanges)).toBe(true); expect(wrapper.contains(NoChanges)).toBe(true);
}); });
it('does not render empty state when diff files exist', () => { it('does not render empty state when diff files exist', () => {
...@@ -108,8 +140,8 @@ describe('diffs/components/app', () => { ...@@ -108,8 +140,8 @@ describe('diffs/components/app', () => {
}); });
}); });
expect(vm.contains(NoChanges)).toBe(false); expect(wrapper.contains(NoChanges)).toBe(false);
expect(vm.findAll(DiffFile).length).toBe(1); expect(wrapper.findAll(DiffFile).length).toBe(1);
}); });
it('does not render empty state when versions match', () => { it('does not render empty state when versions match', () => {
...@@ -118,7 +150,161 @@ describe('diffs/components/app', () => { ...@@ -118,7 +150,161 @@ describe('diffs/components/app', () => {
store.state.diffs.mergeRequestDiff = { version_index: 1 }; store.state.diffs.mergeRequestDiff = { version_index: 1 };
}); });
expect(vm.contains(NoChanges)).toBe(false); expect(wrapper.contains(NoChanges)).toBe(false);
});
});
describe('keyboard shortcut navigation', () => {
const mappings = {
'[': -1,
k: -1,
']': +1,
j: +1,
};
let spy;
describe('visible app', () => {
beforeEach(() => {
spy = jasmine.createSpy('spy');
createComponent({
shouldShow: true,
});
wrapper.setMethods({
jumpToFile: spy,
});
});
it('calls `jumpToFile()` with correct parameter whenever pre-defined key is pressed', done => {
wrapper.vm
.$nextTick()
.then(() => {
Object.keys(mappings).forEach(function(key) {
Mousetrap.trigger(key);
expect(spy.calls.mostRecent().args).toEqual([mappings[key]]);
});
expect(spy.calls.count()).toEqual(Object.keys(mappings).length);
})
.then(done)
.catch(done.fail);
});
it('does not call `jumpToFile()` when unknown key is pressed', done => {
wrapper.vm
.$nextTick()
.then(() => {
Mousetrap.trigger('d');
expect(spy).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
describe('hideen app', () => {
beforeEach(() => {
spy = jasmine.createSpy('spy');
createComponent({
shouldShow: false,
});
wrapper.setMethods({
jumpToFile: spy,
});
});
it('stops calling `jumpToFile()` when application is hidden', done => {
wrapper.vm
.$nextTick()
.then(() => {
Object.keys(mappings).forEach(function(key) {
Mousetrap.trigger(key);
expect(spy).not.toHaveBeenCalled();
});
})
.then(done)
.catch(done.fail);
});
});
});
describe('jumpToFile', () => {
let spy;
beforeEach(() => {
spy = jasmine.createSpy();
createComponent({}, () => {
store.state.diffs.diffFiles = [
{ file_hash: '111', file_path: '111.js' },
{ file_hash: '222', file_path: '222.js' },
{ file_hash: '333', file_path: '333.js' },
];
});
wrapper.setMethods({
scrollToFile: spy,
});
});
afterEach(() => {
wrapper.destroy();
});
it('jumps to next and previous files in the list', done => {
wrapper.vm
.$nextTick()
.then(() => {
wrapper.vm.jumpToFile(+1);
expect(spy.calls.mostRecent().args).toEqual(['222.js']);
store.state.diffs.currentDiffFileId = '222';
wrapper.vm.jumpToFile(+1);
expect(spy.calls.mostRecent().args).toEqual(['333.js']);
store.state.diffs.currentDiffFileId = '333';
wrapper.vm.jumpToFile(-1);
expect(spy.calls.mostRecent().args).toEqual(['222.js']);
})
.then(done)
.catch(done.fail);
});
it('does not jump to previous file from the first one', done => {
wrapper.vm
.$nextTick()
.then(() => {
store.state.diffs.currentDiffFileId = '333';
expect(wrapper.vm.currentDiffIndex).toEqual(2);
wrapper.vm.jumpToFile(+1);
expect(wrapper.vm.currentDiffIndex).toEqual(2);
expect(spy).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('does not jump to next file from the last one', done => {
wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.vm.currentDiffIndex).toEqual(0);
wrapper.vm.jumpToFile(-1);
expect(wrapper.vm.currentDiffIndex).toEqual(0);
expect(spy).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
...@@ -100,9 +100,10 @@ describe('DiffsStoreActions', () => { ...@@ -100,9 +100,10 @@ describe('DiffsStoreActions', () => {
}); });
describe('setHighlightedRow', () => { describe('setHighlightedRow', () => {
it('should set lineHash and fileHash of highlightedRow', () => { it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => {
testAction(setHighlightedRow, 'ABC_123', {}, [ testAction(setHighlightedRow, 'ABC_123', {}, [
{ type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' }, { type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' },
{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'ABC' },
]); ]);
}); });
}); });
...@@ -713,22 +714,6 @@ describe('DiffsStoreActions', () => { ...@@ -713,22 +714,6 @@ describe('DiffsStoreActions', () => {
expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, 'test'); expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, 'test');
}); });
it('resets currentDiffId after timeout', () => {
const state = {
treeEntries: {
path: {
fileHash: 'test',
},
},
};
scrollToFile({ state, commit }, 'path');
jasmine.clock().tick(1000);
expect(commit.calls.argsFor(1)).toEqual([types.UPDATE_CURRENT_DIFF_FILE_ID, '']);
});
}); });
describe('toggleShowTreeList', () => { describe('toggleShowTreeList', () => {
......
...@@ -270,4 +270,24 @@ describe('Diffs Module Getters', () => { ...@@ -270,4 +270,24 @@ describe('Diffs Module Getters', () => {
expect(getters.diffFilesLength(localState)).toBe(2); expect(getters.diffFilesLength(localState)).toBe(2);
}); });
}); });
describe('currentDiffIndex', () => {
it('returns index of currently selected diff in diffList', () => {
localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }];
localState.currentDiffFileId = '222';
expect(getters.currentDiffIndex(localState)).toEqual(1);
localState.currentDiffFileId = '333';
expect(getters.currentDiffIndex(localState)).toEqual(2);
});
it('returns 0 if no diff is selected yet or diff is not found', () => {
localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }];
localState.currentDiffFileId = '';
expect(getters.currentDiffIndex(localState)).toEqual(0);
});
});
}); });
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