Commit fea813ab authored by Filipa Lacerda's avatar Filipa Lacerda
Browse files

Merge branch 'diff-tree-resizable' into 'master'

Make the file tree in merge requests resizable

Closes #51857

See merge request gitlab-org/gitlab-ce!25097
parents 280fe56a d17de657
...@@ -4,6 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -4,6 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale'; 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 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';
...@@ -11,6 +12,13 @@ import NoChanges from './no_changes.vue'; ...@@ -11,6 +12,13 @@ import NoChanges from './no_changes.vue';
import HiddenFilesWarning from './hidden_files_warning.vue'; import HiddenFilesWarning from './hidden_files_warning.vue';
import CommitWidget from './commit_widget.vue'; import CommitWidget from './commit_widget.vue';
import TreeList from './tree_list.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,
} from '../constants';
export default { export default {
name: 'DiffsApp', name: 'DiffsApp',
...@@ -23,6 +31,7 @@ export default { ...@@ -23,6 +31,7 @@ export default {
CommitWidget, CommitWidget,
TreeList, TreeList,
GlLoadingIcon, GlLoadingIcon,
PanelResizer,
}, },
props: { props: {
endpoint: { endpoint: {
...@@ -54,8 +63,12 @@ export default { ...@@ -54,8 +63,12 @@ export default {
}, },
}, },
data() { data() {
const treeWidth =
parseInt(localStorage.getItem(TREE_LIST_WIDTH_STORAGE_KEY), 10) || INITIAL_TREE_WIDTH;
return { return {
assignedDiscussions: false, assignedDiscussions: false,
treeWidth,
}; };
}, },
computed: { computed: {
...@@ -96,6 +109,9 @@ export default { ...@@ -96,6 +109,9 @@ export default {
this.startVersion.version_index === this.mergeRequestDiff.version_index) this.startVersion.version_index === this.mergeRequestDiff.version_index)
); );
}, },
hideFileStats() {
return this.treeWidth <= TREE_HIDE_STATS_WIDTH;
},
}, },
watch: { watch: {
diffViewType() { diffViewType() {
...@@ -142,6 +158,7 @@ export default { ...@@ -142,6 +158,7 @@ export default {
'startRenderDiffsQueue', 'startRenderDiffsQueue',
'assignDiscussionsToDiff', 'assignDiscussionsToDiff',
'setHighlightedRow', 'setHighlightedRow',
'cacheTreeListWidth',
]), ]),
fetchData() { fetchData() {
this.fetchDiffFiles() this.fetchDiffFiles()
...@@ -184,6 +201,8 @@ export default { ...@@ -184,6 +201,8 @@ export default {
} }
}, },
}, },
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
}; };
</script> </script>
...@@ -209,7 +228,21 @@ export default { ...@@ -209,7 +228,21 @@ export default {
:data-can-create-note="getNoteableData.current_user.can_create_note" :data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex prepend-top-default" class="files d-flex prepend-top-default"
> >
<div v-show="showTreeList" class="diff-tree-list"><tree-list /></div> <div
v-show="showTreeList"
:style="{ width: `${treeWidth}px` }"
class="diff-tree-list js-diff-tree-list"
>
<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"> <div class="diff-files-holder">
<commit-widget v-if="commit" :commit="commit" /> <commit-widget v-if="commit" :commit="commit" />
<template v-if="renderDiffFiles"> <template v-if="renderDiffFiles">
......
...@@ -13,6 +13,12 @@ export default { ...@@ -13,6 +13,12 @@ export default {
Icon, Icon,
FileRow, FileRow,
}, },
props: {
hideFileStats: {
type: Boolean,
required: true,
},
},
data() { data() {
return { return {
search: '', search: '',
...@@ -40,6 +46,9 @@ export default { ...@@ -40,6 +46,9 @@ export default {
return acc; return acc;
}, []); }, []);
}, },
fileRowExtraComponent() {
return this.hideFileStats ? null : FileRowStats;
},
}, },
methods: { methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']), ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']),
...@@ -48,7 +57,6 @@ export default { ...@@ -48,7 +57,6 @@ export default {
}, },
}, },
shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '&#8984;' : 'Ctrl'}+P`, shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '&#8984;' : 'Ctrl'}+P`,
FileRowStats,
diffTreeFiltering: gon.features && gon.features.diffTreeFiltering, diffTreeFiltering: gon.features && gon.features.diffTreeFiltering,
}; };
</script> </script>
...@@ -98,7 +106,7 @@ export default { ...@@ -98,7 +106,7 @@ export default {
:file="file" :file="file"
:level="0" :level="0"
:hide-extra-on-tree="true" :hide-extra-on-tree="true"
:extra-component="$options.FileRowStats" :extra-component="fileRowExtraComponent"
:show-changed-icon="true" :show-changed-icon="true"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile" @clickFile="scrollToFile"
......
...@@ -36,3 +36,9 @@ export const MR_TREE_SHOW_KEY = 'mr_tree_show'; ...@@ -36,3 +36,9 @@ export const MR_TREE_SHOW_KEY = 'mr_tree_show';
export const TREE_TYPE = 'tree'; export const TREE_TYPE = 'tree';
export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list'; export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list';
export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace'; export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace';
export const TREE_LIST_WIDTH_STORAGE_KEY = 'mr_tree_list_width';
export const INITIAL_TREE_WIDTH = 320;
export const MIN_TREE_WIDTH = 240;
export const MAX_TREE_WIDTH = 400;
export const TREE_HIDE_STATS_WIDTH = 260;
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
MR_TREE_SHOW_KEY, MR_TREE_SHOW_KEY,
TREE_LIST_STORAGE_KEY, TREE_LIST_STORAGE_KEY,
WHITESPACE_STORAGE_KEY, WHITESPACE_STORAGE_KEY,
TREE_LIST_WIDTH_STORAGE_KEY,
} from '../constants'; } from '../constants';
import { diffViewerModes } from '~/ide/constants'; import { diffViewerModes } from '~/ide/constants';
...@@ -302,5 +303,9 @@ export const toggleFileFinder = ({ commit }, visible) => { ...@@ -302,5 +303,9 @@ export const toggleFileFinder = ({ commit }, visible) => {
commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible); commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible);
}; };
export const cacheTreeListWidth = (_, size) => {
localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size);
};
// 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 () => {};
...@@ -28,11 +28,12 @@ export default { ...@@ -28,11 +28,12 @@ export default {
data() { data() {
return { return {
size: this.startSize, size: this.startSize,
isDragging: false,
}; };
}, },
computed: { computed: {
className() { className() {
return `drag-${this.side}`; return [`position-${this.side}-0`, { 'is-dragging': this.isDragging }];
}, },
cursorStyle() { cursorStyle() {
if (this.enabled) { if (this.enabled) {
...@@ -57,6 +58,7 @@ export default { ...@@ -57,6 +58,7 @@ export default {
startDrag(e) { startDrag(e) {
if (this.enabled) { if (this.enabled) {
e.preventDefault(); e.preventDefault();
this.isDragging = true;
this.startPos = e.clientX; this.startPos = e.clientX;
this.currentStartSize = this.size; this.currentStartSize = this.size;
document.addEventListener('mousemove', this.drag); document.addEventListener('mousemove', this.drag);
...@@ -80,6 +82,7 @@ export default { ...@@ -80,6 +82,7 @@ export default {
}, },
endDrag(e) { endDrag(e) {
e.preventDefault(); e.preventDefault();
this.isDragging = false;
document.removeEventListener('mousemove', this.drag); document.removeEventListener('mousemove', this.drag);
this.$emit('resize-end', this.size); this.$emit('resize-end', this.size);
}, },
...@@ -91,7 +94,7 @@ export default { ...@@ -91,7 +94,7 @@ export default {
<div <div
:class="className" :class="className"
:style="cursorStyle" :style="cursorStyle"
class="drag-handle" class="position-absolute position-top-0 position-bottom-0 drag-handle"
@mousedown="startDrag" @mousedown="startDrag"
@dblclick="resetSize" @dblclick="resetSize"
></div> ></div>
......
...@@ -446,3 +446,15 @@ img.emoji { ...@@ -446,3 +446,15 @@ img.emoji {
.position-left-0 { left: 0; } .position-left-0 { left: 0; }
.position-right-0 { right: 0; } .position-right-0 { right: 0; }
.position-top-0 { top: 0; } .position-top-0 { top: 0; }
.drag-handle {
width: 4px;
&:hover {
background-color: $white-normal;
}
&.is-dragging {
background-color: $gray-600;
}
}
...@@ -682,25 +682,6 @@ $ide-commit-header-height: 48px; ...@@ -682,25 +682,6 @@ $ide-commit-header-height: 48px;
flex: 1; flex: 1;
} }
.drag-handle {
position: absolute;
top: 0;
bottom: 0;
width: 4px;
&:hover {
background-color: $white-normal;
}
&.drag-right {
right: 0;
}
&.drag-left {
left: 0;
}
}
.ide-commit-list-container { .ide-commit-list-container {
display: flex; display: flex;
flex: 1; flex: 1;
......
...@@ -1038,12 +1038,30 @@ ...@@ -1038,12 +1038,30 @@
} }
.diff-tree-list { .diff-tree-list {
width: 320px; position: -webkit-sticky;
position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
max-height: calc(100vh - #{$top-pos});
padding-right: $gl-padding;
z-index: 202;
.with-performance-bar & {
$performance-bar-top-pos: $performance-bar-height + $top-pos;
top: $performance-bar-top-pos;
max-height: calc(100vh - #{$performance-bar-top-pos});
}
.drag-handle {
bottom: 16px;
transform: translateX(-6px);
}
} }
.diff-files-holder { .diff-files-holder {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
z-index: 201;
} }
.compare-versions-container { .compare-versions-container {
...@@ -1051,23 +1069,12 @@ ...@@ -1051,23 +1069,12 @@
} }
.tree-list-holder { .tree-list-holder {
position: -webkit-sticky; height: 100%;
position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
max-height: calc(100vh - #{$top-pos});
padding-right: $gl-padding;
.file-row { .file-row {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
} }
.with-performance-bar & {
$performance-bar-top-pos: $performance-bar-height + $top-pos;
top: $performance-bar-top-pos;
max-height: calc(100vh - #{$performance-bar-top-pos});
}
} }
.tree-list-scroll { .tree-list-scroll {
......
...@@ -735,7 +735,7 @@ ...@@ -735,7 +735,7 @@
.mr-version-controls { .mr-version-controls {
position: relative; position: relative;
z-index: 103; z-index: 203;
background: $gray-light; background: $gray-light;
color: $gl-text-color; color: $gl-text-color;
margin-top: -1px; margin-top: -1px;
...@@ -809,7 +809,7 @@ ...@@ -809,7 +809,7 @@
.merge-request-tabs-holder { .merge-request-tabs-holder {
top: $header-height; top: $header-height;
z-index: 200; z-index: 300;
background-color: $white-light; background-color: $white-light;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
......
---
title: Make file tree in merge requests resizable
merge_request:
author:
type: added
...@@ -68,6 +68,32 @@ describe('diffs/components/app', () => { ...@@ -68,6 +68,32 @@ describe('diffs/components/app', () => {
}); });
}); });
describe('resizable', () => {
afterEach(() => {
localStorage.removeItem('mr_tree_list_width');
});
it('sets initial width when no localStorage has been set', () => {
createComponent();
expect(vm.vm.treeWidth).toEqual(320);
});
it('sets initial width to localStorage size', () => {
localStorage.setItem('mr_tree_list_width', '200');
createComponent();
expect(vm.vm.treeWidth).toEqual(200);
});
it('sets width of tree list', () => {
createComponent();
expect(vm.find('.js-diff-tree-list').element.style.width).toEqual('320px');
});
});
describe('empty state', () => { describe('empty state', () => {
it('renders empty state when no diff files exist', () => { it('renders empty state when no diff files exist', () => {
createComponent(); createComponent();
......
...@@ -28,7 +28,7 @@ describe('Diffs tree list component', () => { ...@@ -28,7 +28,7 @@ describe('Diffs tree list component', () => {
localStorage.removeItem('mr_diff_tree_list'); localStorage.removeItem('mr_diff_tree_list');
vm = mountComponentWithStore(Component, { store }); vm = mountComponentWithStore(Component, { store, props: { hideFileStats: false } });
}); });
afterEach(() => { afterEach(() => {
...@@ -77,6 +77,16 @@ describe('Diffs tree list component', () => { ...@@ -77,6 +77,16 @@ describe('Diffs tree list component', () => {
expect(vm.$el.querySelectorAll('.file-row')[1].textContent).toContain('app'); expect(vm.$el.querySelectorAll('.file-row')[1].textContent).toContain('app');
}); });
it('hides file stats', done => {
vm.hideFileStats = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.file-row-stats')).toBe(null);
done();
});
});
it('calls toggleTreeOpen when clicking folder', () => { it('calls toggleTreeOpen when clicking folder', () => {
spyOn(vm.$store, 'dispatch').and.stub(); spyOn(vm.$store, 'dispatch').and.stub();
......
...@@ -44,7 +44,10 @@ describe('Panel Resizer component', () => { ...@@ -44,7 +44,10 @@ describe('Panel Resizer component', () => {
}); });
expect(vm.$el.tagName).toEqual('DIV'); expect(vm.$el.tagName).toEqual('DIV');
expect(vm.$el.getAttribute('class')).toBe('drag-handle drag-left'); expect(vm.$el.getAttribute('class')).toBe(
'position-absolute position-top-0 position-bottom-0 drag-handle position-left-0',
);
expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;'); expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;');
}); });
...@@ -55,7 +58,9 @@ describe('Panel Resizer component', () => { ...@@ -55,7 +58,9 @@ describe('Panel Resizer component', () => {
}); });
expect(vm.$el.tagName).toEqual('DIV'); expect(vm.$el.tagName).toEqual('DIV');
expect(vm.$el.getAttribute('class')).toBe('drag-handle drag-right'); expect(vm.$el.getAttribute('class')).toBe(
'position-absolute position-top-0 position-bottom-0 drag-handle position-right-0',
);
}); });
it('drag the resizer', () => { it('drag the resizer', () => {
......
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