Commit 2d00e7fc authored by Phil Hughes's avatar Phil Hughes

Add list mode to file browser in diffs

This adds toggle buttons to switch between file & tree list.
For file list, it renders the truncated paths with the ellipsis
at the start of the path.

When focusing the input, it hides the toggle buttons.
On blur, the buttons get shown again.

Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/51859
parent 10bb8297
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { TooltipDirective as Tooltip } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue'; import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue'; import FileRowStats from './file_row_stats.vue';
export default { export default {
directives: {
Tooltip,
},
components: { components: {
Icon, Icon,
FileRow, FileRow,
...@@ -12,6 +16,8 @@ export default { ...@@ -12,6 +16,8 @@ export default {
data() { data() {
return { return {
search: '', search: '',
renderTreeList: true,
focusSearch: false,
}; };
}, },
computed: { computed: {
...@@ -20,16 +26,29 @@ export default { ...@@ -20,16 +26,29 @@ export default {
filteredTreeList() { filteredTreeList() {
const search = this.search.toLowerCase().trim(); const search = this.search.toLowerCase().trim();
if (search === '') return this.tree; if (search === '') return this.renderTreeList ? this.tree : this.allBlobs;
return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0); return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0);
}, },
rowDisplayTextKey() {
if (this.renderTreeList && this.search.trim() === '') {
return 'name';
}
return 'truncatedPath';
},
}, },
methods: { methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
clearSearch() { clearSearch() {
this.search = ''; this.search = '';
}, },
toggleRenderTreeList(toggle) {
this.renderTreeList = toggle;
},
toggleFocusSearch(toggle) {
this.focusSearch = toggle;
},
}, },
FileRowStats, FileRowStats,
}; };
...@@ -37,7 +56,8 @@ export default { ...@@ -37,7 +56,8 @@ export default {
<template> <template>
<div class="tree-list-holder d-flex flex-column"> <div class="tree-list-holder d-flex flex-column">
<div class="append-bottom-8 position-relative tree-list-search"> <div class="append-bottom-8 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<icon <icon
name="search" name="search"
class="position-absolute tree-list-icon" class="position-absolute tree-list-icon"
...@@ -47,6 +67,8 @@ export default { ...@@ -47,6 +67,8 @@ export default {
:placeholder="s__('MergeRequest|Filter files')" :placeholder="s__('MergeRequest|Filter files')"
type="search" type="search"
class="form-control" class="form-control"
@focus="toggleFocusSearch(true)"
@blur="toggleFocusSearch(false)"
/> />
<button <button
v-show="search" v-show="search"
...@@ -60,6 +82,42 @@ export default { ...@@ -60,6 +82,42 @@ export default {
/> />
</button> </button>
</div> </div>
<div
v-show="!focusSearch"
class="btn-group prepend-left-8 tree-list-view-toggle"
>
<button
v-tooltip.hover
:aria-label="__('Switch to file list')"
:title="__('Switch to file list')"
:class="{
active: !renderTreeList
}"
class="btn btn-default pt-0 pb-0 d-flex align-items-center"
type="button"
@click="toggleRenderTreeList(false)"
>
<icon
name="hamburger"
/>
</button>
<button
v-tooltip.hover
:aria-label="__('Switch to tree list')"
:title="__('Switch to tree list')"
:class="{
active: renderTreeList
}"
class="btn btn-default pt-0 pb-0 d-flex align-items-center"
type="button"
@click="toggleRenderTreeList(true)"
>
<icon
name="hamburger"
/>
</button>
</div>
</div>
<div <div
class="tree-list-scroll" class="tree-list-scroll"
> >
...@@ -72,6 +130,7 @@ export default { ...@@ -72,6 +130,7 @@ export default {
:hide-extra-on-tree="true" :hide-extra-on-tree="true"
:extra-component="$options.FileRowStats" :extra-component="$options.FileRowStats"
:show-changed-icon="true" :show-changed-icon="true"
:display-text-key="rowDisplayTextKey"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile" @clickFile="scrollToFile"
/> />
......
...@@ -275,6 +275,18 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD ...@@ -275,6 +275,18 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
return latestDiff && discussion.active && lineCode === discussion.line_code; return latestDiff && discussion.active && lineCode === discussion.line_code;
} }
export const truncatedName = path => {
const maxLength = 30;
if (path.length > maxLength) {
const start = path.length - maxLength;
const end = start + maxLength;
return `...${path.slice(start, end)}`;
}
return path;
};
export const generateTreeList = files => export const generateTreeList = files =>
files.reduce( files.reduce(
(acc, file) => { (acc, file) => {
...@@ -290,6 +302,7 @@ export const generateTreeList = files => ...@@ -290,6 +302,7 @@ export const generateTreeList = files =>
acc.treeEntries[path] = { acc.treeEntries[path] = {
key: path, key: path,
path, path,
truncatedPath: truncatedName(path),
name, name,
type, type,
tree: [], tree: [],
......
...@@ -34,6 +34,11 @@ export default { ...@@ -34,6 +34,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
displayTextKey: {
type: String,
required: false,
default: 'name',
},
}, },
data() { data() {
return { return {
...@@ -156,7 +161,7 @@ export default { ...@@ -156,7 +161,7 @@ export default {
:size="16" :size="16"
class="append-right-5" class="append-right-5"
/> />
{{ file.name }} {{ file[displayTextKey] }}
</span> </span>
<component <component
:is="extraComponent" :is="extraComponent"
...@@ -175,6 +180,7 @@ export default { ...@@ -175,6 +180,7 @@ export default {
:hide-extra-on-tree="hideExtraOnTree" :hide-extra-on-tree="hideExtraOnTree"
:extra-component="extraComponent" :extra-component="extraComponent"
:show-changed-icon="showChangedIcon" :show-changed-icon="showChangedIcon"
:display-text-key="displayTextKey"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile" @clickFile="clickedFile"
/> />
......
...@@ -1027,8 +1027,12 @@ ...@@ -1027,8 +1027,12 @@
overflow-x: auto; overflow-x: auto;
} }
.tree-list-search .form-control { .tree-list-search {
flex: 0 0 34px;
.form-control {
padding-left: 30px; padding-left: 30px;
}
} }
.tree-list-icon { .tree-list-icon {
...@@ -1063,3 +1067,9 @@ ...@@ -1063,3 +1067,9 @@
} }
} }
} }
.tree-list-view-toggle {
svg {
top: 0;
}
}
---
title: Switch between tree list & file list in diffs file browser
merge_request:
author:
type: added
...@@ -5823,6 +5823,12 @@ msgstr "" ...@@ -5823,6 +5823,12 @@ msgstr ""
msgid "Switch branch/tag" msgid "Switch branch/tag"
msgstr "" msgstr ""
msgid "Switch to file list"
msgstr ""
msgid "Switch to tree list"
msgstr ""
msgid "System Hooks" msgid "System Hooks"
msgstr "" msgstr ""
......
...@@ -54,6 +54,7 @@ describe('Diffs tree list component', () => { ...@@ -54,6 +54,7 @@ describe('Diffs tree list component', () => {
key: 'index.js', key: 'index.js',
name: 'index.js', name: 'index.js',
path: 'index.js', path: 'index.js',
truncatedPath: '../index.js',
removedLines: 0, removedLines: 0,
tempFile: true, tempFile: true,
type: 'blob', type: 'blob',
...@@ -106,6 +107,55 @@ describe('Diffs tree list component', () => { ...@@ -106,6 +107,55 @@ describe('Diffs tree list component', () => {
expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'index.js'); expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'index.js');
}); });
it('renders as file list when renderTreeList is false', done => {
vm.renderTreeList = false;
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('.file-row').length).toBe(1);
done();
});
});
it('renders file paths when renderTreeList is false', done => {
vm.renderTreeList = false;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.file-row').textContent).toContain('../index.js');
done();
});
});
it('hides render buttons when input is focused', done => {
const focusEvent = new Event('focus');
vm.$el.querySelector('.form-control').dispatchEvent(focusEvent);
vm.$nextTick(() => {
expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).toBe('none');
done();
});
});
it('shows render buttons when input is blurred', done => {
const blurEvent = new Event('blur');
vm.focusSearch = true;
vm
.$nextTick()
.then(() => {
vm.$el.querySelector('.form-control').dispatchEvent(blurEvent);
})
.then(vm.$nextTick)
.then(() => {
expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).not.toBe('none');
})
.then(done)
.catch(done.fail);
});
}); });
describe('clearSearch', () => { describe('clearSearch', () => {
...@@ -117,4 +167,24 @@ describe('Diffs tree list component', () => { ...@@ -117,4 +167,24 @@ describe('Diffs tree list component', () => {
expect(vm.search).toBe(''); expect(vm.search).toBe('');
}); });
}); });
describe('toggleRenderTreeList', () => {
it('updates renderTreeList', () => {
expect(vm.renderTreeList).toBe(true);
vm.toggleRenderTreeList(false);
expect(vm.renderTreeList).toBe(false);
});
});
describe('toggleFocusSearch', () => {
it('updates focusSearch', () => {
expect(vm.focusSearch).toBe(false);
vm.toggleFocusSearch(true);
expect(vm.focusSearch).toBe(true);
});
});
}); });
...@@ -444,6 +444,14 @@ describe('DiffsStoreUtils', () => { ...@@ -444,6 +444,14 @@ describe('DiffsStoreUtils', () => {
addedLines: 0, addedLines: 0,
fileHash: 'test', fileHash: 'test',
}, },
{
newPath: 'app/test/filepathneedstruncating.js',
deletedFile: false,
newFile: true,
removedLines: 0,
addedLines: 0,
fileHash: 'test',
},
{ {
newPath: 'package.json', newPath: 'package.json',
deletedFile: true, deletedFile: true,
...@@ -462,6 +470,7 @@ describe('DiffsStoreUtils', () => { ...@@ -462,6 +470,7 @@ describe('DiffsStoreUtils', () => {
{ {
key: 'app', key: 'app',
path: 'app', path: 'app',
truncatedPath: 'app',
name: 'app', name: 'app',
type: 'tree', type: 'tree',
tree: [ tree: [
...@@ -473,6 +482,7 @@ describe('DiffsStoreUtils', () => { ...@@ -473,6 +482,7 @@ describe('DiffsStoreUtils', () => {
key: 'app/index.js', key: 'app/index.js',
name: 'index.js', name: 'index.js',
path: 'app/index.js', path: 'app/index.js',
truncatedPath: 'app/index.js',
removedLines: 10, removedLines: 10,
tempFile: false, tempFile: false,
type: 'blob', type: 'blob',
...@@ -481,6 +491,7 @@ describe('DiffsStoreUtils', () => { ...@@ -481,6 +491,7 @@ describe('DiffsStoreUtils', () => {
{ {
key: 'app/test', key: 'app/test',
path: 'app/test', path: 'app/test',
truncatedPath: 'app/test',
name: 'test', name: 'test',
type: 'tree', type: 'tree',
opened: true, opened: true,
...@@ -493,6 +504,21 @@ describe('DiffsStoreUtils', () => { ...@@ -493,6 +504,21 @@ describe('DiffsStoreUtils', () => {
key: 'app/test/index.js', key: 'app/test/index.js',
name: 'index.js', name: 'index.js',
path: 'app/test/index.js', path: 'app/test/index.js',
truncatedPath: 'app/test/index.js',
removedLines: 0,
tempFile: true,
type: 'blob',
tree: [],
},
{
addedLines: 0,
changed: true,
deleted: false,
fileHash: 'test',
key: 'app/test/filepathneedstruncating.js',
name: 'filepathneedstruncating.js',
path: 'app/test/filepathneedstruncating.js',
truncatedPath: '...est/filepathneedstruncating.js',
removedLines: 0, removedLines: 0,
tempFile: true, tempFile: true,
type: 'blob', type: 'blob',
...@@ -506,6 +532,7 @@ describe('DiffsStoreUtils', () => { ...@@ -506,6 +532,7 @@ describe('DiffsStoreUtils', () => {
{ {
key: 'package.json', key: 'package.json',
path: 'package.json', path: 'package.json',
truncatedPath: 'package.json',
name: 'package.json', name: 'package.json',
type: 'blob', type: 'blob',
changed: true, changed: true,
...@@ -527,6 +554,7 @@ describe('DiffsStoreUtils', () => { ...@@ -527,6 +554,7 @@ describe('DiffsStoreUtils', () => {
'app/index.js', 'app/index.js',
'app/test', 'app/test',
'app/test/index.js', 'app/test/index.js',
'app/test/filepathneedstruncating.js',
'package.json', 'package.json',
]); ]);
}); });
......
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