Skip to content
Snippets Groups Projects
Unverified Commit 2d00e7fc authored by Phil Hughes's avatar Phil Hughes
Browse files

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
No related branches found
No related tags found
No related merge requests found
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { TooltipDirective as Tooltip } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue';
 
export default {
directives: {
Tooltip,
},
components: {
Icon,
FileRow,
Loading
Loading
@@ -12,6 +16,8 @@ export default {
data() {
return {
search: '',
renderTreeList: true,
focusSearch: false,
};
},
computed: {
Loading
Loading
@@ -20,16 +26,29 @@ export default {
filteredTreeList() {
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);
},
rowDisplayTextKey() {
if (this.renderTreeList && this.search.trim() === '') {
return 'name';
}
return 'truncatedPath';
},
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
clearSearch() {
this.search = '';
},
toggleRenderTreeList(toggle) {
this.renderTreeList = toggle;
},
toggleFocusSearch(toggle) {
this.focusSearch = toggle;
},
},
FileRowStats,
};
Loading
Loading
@@ -37,28 +56,67 @@ export default {
 
<template>
<div class="tree-list-holder d-flex flex-column">
<div class="append-bottom-8 position-relative tree-list-search">
<icon
name="search"
class="position-absolute tree-list-icon"
/>
<input
v-model="search"
:placeholder="s__('MergeRequest|Filter files')"
type="search"
class="form-control"
/>
<button
v-show="search"
:aria-label="__('Clear search')"
type="button"
class="position-absolute tree-list-icon tree-list-clear-icon border-0 p-0"
@click="clearSearch"
>
<div class="append-bottom-8 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<icon
name="close"
name="search"
class="position-absolute tree-list-icon"
/>
<input
v-model="search"
:placeholder="s__('MergeRequest|Filter files')"
type="search"
class="form-control"
@focus="toggleFocusSearch(true)"
@blur="toggleFocusSearch(false)"
/>
</button>
<button
v-show="search"
:aria-label="__('Clear search')"
type="button"
class="position-absolute tree-list-icon tree-list-clear-icon border-0 p-0"
@click="clearSearch"
>
<icon
name="close"
/>
</button>
</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
class="tree-list-scroll"
Loading
Loading
@@ -72,6 +130,7 @@ export default {
:hide-extra-on-tree="true"
:extra-component="$options.FileRowStats"
:show-changed-icon="true"
:display-text-key="rowDisplayTextKey"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile"
/>
Loading
Loading
Loading
Loading
@@ -275,6 +275,18 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
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 =>
files.reduce(
(acc, file) => {
Loading
Loading
@@ -290,6 +302,7 @@ export const generateTreeList = files =>
acc.treeEntries[path] = {
key: path,
path,
truncatedPath: truncatedName(path),
name,
type,
tree: [],
Loading
Loading
Loading
Loading
@@ -34,6 +34,11 @@ export default {
required: false,
default: false,
},
displayTextKey: {
type: String,
required: false,
default: 'name',
},
},
data() {
return {
Loading
Loading
@@ -156,7 +161,7 @@ export default {
:size="16"
class="append-right-5"
/>
{{ file.name }}
{{ file[displayTextKey] }}
</span>
<component
:is="extraComponent"
Loading
Loading
@@ -175,6 +180,7 @@ export default {
:hide-extra-on-tree="hideExtraOnTree"
:extra-component="extraComponent"
:show-changed-icon="showChangedIcon"
:display-text-key="displayTextKey"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile"
/>
Loading
Loading
Loading
Loading
@@ -1027,8 +1027,12 @@
overflow-x: auto;
}
 
.tree-list-search .form-control {
padding-left: 30px;
.tree-list-search {
flex: 0 0 34px;
.form-control {
padding-left: 30px;
}
}
 
.tree-list-icon {
Loading
Loading
@@ -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
Loading
Loading
@@ -5823,6 +5823,12 @@ msgstr ""
msgid "Switch branch/tag"
msgstr ""
 
msgid "Switch to file list"
msgstr ""
msgid "Switch to tree list"
msgstr ""
msgid "System Hooks"
msgstr ""
 
Loading
Loading
Loading
Loading
@@ -54,6 +54,7 @@ describe('Diffs tree list component', () => {
key: 'index.js',
name: 'index.js',
path: 'index.js',
truncatedPath: '../index.js',
removedLines: 0,
tempFile: true,
type: 'blob',
Loading
Loading
@@ -106,6 +107,55 @@ describe('Diffs tree list component', () => {
 
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', () => {
Loading
Loading
@@ -117,4 +167,24 @@ describe('Diffs tree list component', () => {
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);
});
});
});
Loading
Loading
@@ -444,6 +444,14 @@ describe('DiffsStoreUtils', () => {
addedLines: 0,
fileHash: 'test',
},
{
newPath: 'app/test/filepathneedstruncating.js',
deletedFile: false,
newFile: true,
removedLines: 0,
addedLines: 0,
fileHash: 'test',
},
{
newPath: 'package.json',
deletedFile: true,
Loading
Loading
@@ -462,6 +470,7 @@ describe('DiffsStoreUtils', () => {
{
key: 'app',
path: 'app',
truncatedPath: 'app',
name: 'app',
type: 'tree',
tree: [
Loading
Loading
@@ -473,6 +482,7 @@ describe('DiffsStoreUtils', () => {
key: 'app/index.js',
name: 'index.js',
path: 'app/index.js',
truncatedPath: 'app/index.js',
removedLines: 10,
tempFile: false,
type: 'blob',
Loading
Loading
@@ -481,6 +491,7 @@ describe('DiffsStoreUtils', () => {
{
key: 'app/test',
path: 'app/test',
truncatedPath: 'app/test',
name: 'test',
type: 'tree',
opened: true,
Loading
Loading
@@ -493,6 +504,21 @@ describe('DiffsStoreUtils', () => {
key: 'app/test/index.js',
name: '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,
tempFile: true,
type: 'blob',
Loading
Loading
@@ -506,6 +532,7 @@ describe('DiffsStoreUtils', () => {
{
key: 'package.json',
path: 'package.json',
truncatedPath: 'package.json',
name: 'package.json',
type: 'blob',
changed: true,
Loading
Loading
@@ -527,6 +554,7 @@ describe('DiffsStoreUtils', () => {
'app/index.js',
'app/test',
'app/test/index.js',
'app/test/filepathneedstruncating.js',
'package.json',
]);
});
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment