Commit cb47f40d authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'mr-file-list' into 'master'

Add list mode to file browser in diffs

Closes #51859

See merge request gitlab-org/gitlab-ce!22191
parents 379b4d8a 3b3aa28c
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { TooltipDirective as Tooltip } from '@gitlab-org/gitlab-ui';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
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';
const treeListStorageKey = 'mr_diff_tree_list';
export default { export default {
directives: {
Tooltip,
},
components: { components: {
Icon, Icon,
FileRow, FileRow,
}, },
data() { data() {
const treeListStored = localStorage.getItem(treeListStorageKey);
const renderTreeList = treeListStored !== null ?
convertPermissionToBoolean(treeListStored) : true;
return { return {
search: '', search: '',
renderTreeList,
focusSearch: false,
}; };
}, },
computed: { computed: {
...@@ -20,15 +33,35 @@ export default { ...@@ -20,15 +33,35 @@ 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 'path';
},
}, },
methods: { methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
clearSearch() { clearSearch() {
this.search = ''; this.search = '';
this.toggleFocusSearch(false);
},
toggleRenderTreeList(toggle) {
this.renderTreeList = toggle;
localStorage.setItem(treeListStorageKey, this.renderTreeList);
},
toggleFocusSearch(toggle) {
this.focusSearch = toggle;
},
blurSearch() {
if (this.search.trim() === '') {
this.toggleFocusSearch(false);
}
}, },
}, },
FileRowStats, FileRowStats,
...@@ -37,28 +70,67 @@ export default { ...@@ -37,28 +70,67 @@ 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">
<icon <div class="flex-fill d-flex">
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"
>
<icon <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="blurSearch"
/> />
</button> <button
v-show="search"
:aria-label="__('Clear search')"
type="button"
class="position-absolute bg-transparent 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="__('List view')"
:title="__('List view')"
: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="__('Tree view')"
:title="__('Tree view')"
:class="{
active: renderTreeList
}"
class="btn btn-default pt-0 pb-0 d-flex align-items-center"
type="button"
@click="toggleRenderTreeList(true)"
>
<icon
name="file-tree"
/>
</button>
</div>
</div> </div>
<div <div
class="tree-list-scroll" class="tree-list-scroll"
...@@ -72,6 +144,8 @@ export default { ...@@ -72,6 +144,8 @@ 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"
:should-truncate-start="true"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile" @clickFile="scrollToFile"
/> />
......
...@@ -34,10 +34,21 @@ export default { ...@@ -34,10 +34,21 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
displayTextKey: {
type: String,
required: false,
default: 'name',
},
shouldTruncateStart: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
mouseOver: false, mouseOver: false,
truncateStart: 0,
}; };
}, },
computed: { computed: {
...@@ -60,6 +71,15 @@ export default { ...@@ -60,6 +71,15 @@ export default {
'is-open': this.file.opened, 'is-open': this.file.opened,
}; };
}, },
outputText() {
const text = this.file[this.displayTextKey];
if (this.truncateStart === 0) {
return text;
}
return `...${text.substring(this.truncateStart, text.length)}`;
},
}, },
watch: { watch: {
'file.active': function fileActiveWatch(active) { 'file.active': function fileActiveWatch(active) {
...@@ -72,6 +92,15 @@ export default { ...@@ -72,6 +92,15 @@ export default {
if (this.hasPathAtCurrentRoute()) { if (this.hasPathAtCurrentRoute()) {
this.scrollIntoView(true); this.scrollIntoView(true);
} }
if (this.shouldTruncateStart) {
const { scrollWidth, offsetWidth } = this.$refs.textOutput;
const textOverflow = scrollWidth - offsetWidth;
if (textOverflow > 0) {
this.truncateStart = Math.ceil(textOverflow / 5) + 3;
}
}
}, },
methods: { methods: {
toggleTreeOpen(path) { toggleTreeOpen(path) {
...@@ -139,6 +168,7 @@ export default { ...@@ -139,6 +168,7 @@ export default {
class="file-row-name-container" class="file-row-name-container"
> >
<span <span
ref="textOutput"
:style="levelIndentation" :style="levelIndentation"
class="file-row-name str-truncated" class="file-row-name str-truncated"
> >
...@@ -156,7 +186,7 @@ export default { ...@@ -156,7 +186,7 @@ export default {
:size="16" :size="16"
class="append-right-5" class="append-right-5"
/> />
{{ file.name }} {{ outputText }}
</span> </span>
<component <component
:is="extraComponent" :is="extraComponent"
...@@ -175,6 +205,8 @@ export default { ...@@ -175,6 +205,8 @@ 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"
:should-truncate-start="shouldTruncateStart"
@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 {
padding-left: 30px; flex: 0 0 34px;
.form-control {
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: 22191
author:
type: added
...@@ -3599,6 +3599,9 @@ msgstr "" ...@@ -3599,6 +3599,9 @@ msgstr ""
msgid "List available repositories" msgid "List available repositories"
msgstr "" msgstr ""
msgid "List view"
msgstr ""
msgid "List your Bitbucket Server repositories" msgid "List your Bitbucket Server repositories"
msgstr "" msgstr ""
...@@ -6519,6 +6522,9 @@ msgstr "" ...@@ -6519,6 +6522,9 @@ msgstr ""
msgid "Track time with quick actions" msgid "Track time with quick actions"
msgstr "" msgstr ""
msgid "Tree view"
msgstr ""
msgid "Trending" msgid "Trending"
msgstr "" msgstr ""
......
...@@ -53,7 +53,7 @@ describe('Diffs tree list component', () => { ...@@ -53,7 +53,7 @@ describe('Diffs tree list component', () => {
fileHash: 'test', fileHash: 'test',
key: 'index.js', key: 'index.js',
name: 'index.js', name: 'index.js',
path: 'index.js', path: 'app/index.js',
removedLines: 0, removedLines: 0,
tempFile: true, tempFile: true,
type: 'blob', type: 'blob',
...@@ -104,7 +104,55 @@ describe('Diffs tree list component', () => { ...@@ -104,7 +104,55 @@ describe('Diffs tree list component', () => {
vm.$el.querySelector('.file-row').click(); vm.$el.querySelector('.file-row').click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'index.js'); expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'app/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('app/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);
}); });
}); });
...@@ -117,4 +165,24 @@ describe('Diffs tree list component', () => { ...@@ -117,4 +165,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,
...@@ -498,6 +506,19 @@ describe('DiffsStoreUtils', () => { ...@@ -498,6 +506,19 @@ describe('DiffsStoreUtils', () => {
type: 'blob', type: 'blob',
tree: [], tree: [],
}, },
{
addedLines: 0,
changed: true,
deleted: false,
fileHash: 'test',
key: 'app/test/filepathneedstruncating.js',
name: 'filepathneedstruncating.js',
path: 'app/test/filepathneedstruncating.js',
removedLines: 0,
tempFile: true,
type: 'blob',
tree: [],
},
], ],
}, },
], ],
...@@ -527,6 +548,7 @@ describe('DiffsStoreUtils', () => { ...@@ -527,6 +548,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',
]); ]);
}); });
......
...@@ -71,4 +71,40 @@ describe('RepoFile', () => { ...@@ -71,4 +71,40 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector('.file-row-name').style.marginLeft).toBe('32px'); expect(vm.$el.querySelector('.file-row-name').style.marginLeft).toBe('32px');
}); });
describe('outputText', () => {
beforeEach(done => {
createComponent({
file: {
...file(),
path: 'app/assets/index.js',
},
level: 0,
});
vm.displayTextKey = 'path';
vm.$nextTick(done);
});
it('returns text if truncateStart is 0', done => {
vm.truncateStart = 0;
vm.$nextTick(() => {
expect(vm.outputText).toBe('app/assets/index.js');
done();
});
});
it('returns text truncated at start', done => {
vm.truncateStart = 5;
vm.$nextTick(() => {
expect(vm.outputText).toBe('...ssets/index.js');
done();
});
});
});
}); });
...@@ -616,11 +616,16 @@ ...@@ -616,11 +616,16 @@
lodash "^4.17.10" lodash "^4.17.10"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.32.0": "@gitlab-org/gitlab-svgs@^1.23.0":
version "1.32.0" version "1.32.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.32.0.tgz#a65ab7724fa7d55be8e5cc9b2dbe3f0757432fd3" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.32.0.tgz#a65ab7724fa7d55be8e5cc9b2dbe3f0757432fd3"
integrity sha512-L3o8dFUd2nSkVZBwh2hCJWzNzADJ3dTBZxamND8NLosZK9/ohNhccmsQOZGyMCUHaOzm4vifaaXkAXh04UtMKA== integrity sha512-L3o8dFUd2nSkVZBwh2hCJWzNzADJ3dTBZxamND8NLosZK9/ohNhccmsQOZGyMCUHaOzm4vifaaXkAXh04UtMKA==
"@gitlab-org/gitlab-svgs@^1.33.0":
version "1.33.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.33.0.tgz#068566e8ee00795f6f09f58236f08e1716f9f04a"
integrity sha512-8ajtUHk6gQ1xosL/CO5IzHSFM/t18hx5pfzQ3cd0VuQXcyR6QKGuXTLwbYdmJDYOw1Etoo5DqDWxPEClHyZpiA==
"@gitlab-org/gitlab-ui@^1.8.0": "@gitlab-org/gitlab-ui@^1.8.0":
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.8.0.tgz#dee33d78f68c91644273dbd51734b796108263ee" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.8.0.tgz#dee33d78f68c91644273dbd51734b796108263ee"
......
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