diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index f40a7b25fde31df21fe79935154d54683b070bb2..eb8f274aff32fcb725eefe5a84a94113fc3d5366 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -34,14 +34,18 @@ export default {
if (search === '') return this.renderTreeList ? this.tree : this.allBlobs;
- return this.allBlobs.filter(f => f.path.toLowerCase().indexOf(search) >= 0);
- },
- rowDisplayTextKey() {
- if (this.renderTreeList && this.search.trim() === '') {
- return 'name';
- }
+ return this.allBlobs.reduce((acc, folder) => {
+ const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0);
- return 'path';
+ if (tree.length) {
+ return acc.concat({
+ ...folder,
+ tree,
+ });
+ }
+
+ return acc;
+ }, []);
},
},
methods: {
@@ -119,7 +123,7 @@ export default {
-
+
+
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index fdf1efbb10e104e9f041c2bc5c65ea1869259ea2..86c0c7190f9a38a2317cad46350451f7c5ec19ea 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -74,7 +74,24 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.file_hash === fileHash);
-export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.type === 'blob');
+export const allBlobs = state =>
+ Object.values(state.treeEntries)
+ .filter(f => f.type === 'blob')
+ .reduce((acc, file) => {
+ const { parentPath } = file;
+
+ if (parentPath && !acc.some(f => f.path === parentPath)) {
+ acc.push({
+ path: parentPath,
+ isHeader: true,
+ tree: [],
+ });
+ }
+
+ acc.find(f => f.path === parentPath).tree.push(file);
+
+ return acc;
+ }, []);
export const diffFilesLength = state => state.diffFiles.length;
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 2fe205516429344ca4718cdb167562a645c4ca11..f427367c11edb3e6aefbceef1f565ec89ba842fb 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -318,6 +318,7 @@ export const generateTreeList = files =>
fileHash: file.file_hash,
addedLines: file.added_lines,
removedLines: file.removed_lines,
+ parentPath: parent ? `${parent.path}/` : '/',
});
} else {
Object.assign(entry, {
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 7cc7cd6d20ebf7c5e6f92cf1f687aba0030273a1..c49b1bb5a2fa58c1dd9ffb02b1161f789475f169 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -72,6 +72,29 @@ export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3
*/
export const truncateSha = sha => sha.substr(0, 8);
+const ELLIPSIS_CHAR = '…';
+export const truncatePathMiddleToLength = (text, maxWidth) => {
+ let returnText = text;
+ let ellipsisCount = 0;
+
+ while (returnText.length >= maxWidth) {
+ const textSplit = returnText.split('/').filter(s => s !== ELLIPSIS_CHAR);
+ const middleIndex = Math.floor(textSplit.length / 2);
+
+ returnText = textSplit
+ .slice(0, middleIndex)
+ .concat(
+ new Array(ellipsisCount + 1).fill().map(() => ELLIPSIS_CHAR),
+ textSplit.slice(middleIndex + 1),
+ )
+ .join('/');
+
+ ellipsisCount += 1;
+ }
+
+ return returnText;
+};
+
/**
* Capitalizes first character
*
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 9e7136736782eadb7e160ed1b76cef4176eae00a..4c884c55a3019a85a43f93acc4914e6dd50ca2cb 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -1,11 +1,13 @@
+
+
+
+
diff --git a/changelogs/unreleased/mr-file-tree-blob-truncate-improvements.yml b/changelogs/unreleased/mr-file-tree-blob-truncate-improvements.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b01962591c6e28706707cbd18dbb63c353a99661
--- /dev/null
+++ b/changelogs/unreleased/mr-file-tree-blob-truncate-improvements.yml
@@ -0,0 +1,5 @@
+---
+title: Add folder header to files in merge request tree list
+merge_request:
+author:
+type: changed
diff --git a/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..26eae2d5e61abb68b444077deb517faf62d877a3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`File row header component adds multiple ellipsises after 40 characters 1`] = `
+
+`;
+
+exports[`File row header component renders file path 1`] = `
+
+`;
+
+exports[`File row header component trucates path after 40 characters 1`] = `
+
+`;
diff --git a/spec/frontend/vue_shared/components/file_row_header_spec.js b/spec/frontend/vue_shared/components/file_row_header_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..80f4c275dcca526aa85286550482c3cd277bf892
--- /dev/null
+++ b/spec/frontend/vue_shared/components/file_row_header_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+import FileRowHeader from '~/vue_shared/components/file_row_header.vue';
+
+describe('File row header component', () => {
+ let vm;
+
+ function createComponent(path) {
+ vm = shallowMount(FileRowHeader, {
+ propsData: {
+ path,
+ },
+ });
+ }
+
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it('renders file path', () => {
+ createComponent('app/assets');
+
+ expect(vm.element).toMatchSnapshot();
+ });
+
+ it('trucates path after 40 characters', () => {
+ createComponent('app/assets/javascripts/merge_requests');
+
+ expect(vm.element).toMatchSnapshot();
+ });
+
+ it('adds multiple ellipsises after 40 characters', () => {
+ createComponent('app/assets/javascripts/merge_requests/widget/diffs/notes');
+
+ expect(vm.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js
index a0b380adfd691fb0870f62e82ee22990ce5b25f4..0a903bb7519ec6ce2b6781fe98dae6cc0cc6ee82 100644
--- a/spec/javascripts/diffs/components/tree_list_spec.js
+++ b/spec/javascripts/diffs/components/tree_list_spec.js
@@ -26,6 +26,8 @@ describe('Diffs tree list component', () => {
store.state.diffs.removedLines = 20;
store.state.diffs.diffFiles.push('test');
+ localStorage.removeItem('mr_diff_tree_list');
+
vm = mountComponentWithStore(Component, { store });
});
@@ -57,6 +59,7 @@ describe('Diffs tree list component', () => {
removedLines: 0,
tempFile: true,
type: 'blob',
+ parentPath: 'app',
},
app: {
key: 'app',
@@ -121,7 +124,7 @@ describe('Diffs tree list component', () => {
vm.renderTreeList = false;
vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-row').textContent).toContain('app/index.js');
+ expect(vm.$el.querySelector('.file-row').textContent).toContain('index.js');
done();
});
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
index 582535e0a5383e8166487d81064d8d6605f104cb..190ca1230cab457b031c98c6e70a5de29938bb43 100644
--- a/spec/javascripts/diffs/store/getters_spec.js
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -230,15 +230,30 @@ describe('Diffs Module Getters', () => {
localState.treeEntries = {
file: {
type: 'blob',
+ path: 'file',
+ parentPath: '/',
+ tree: [],
},
tree: {
type: 'tree',
+ path: 'tree',
+ parentPath: '/',
+ tree: [],
},
};
expect(getters.allBlobs(localState)).toEqual([
{
- type: 'blob',
+ isHeader: true,
+ path: '/',
+ tree: [
+ {
+ parentPath: '/',
+ path: 'file',
+ tree: [],
+ type: 'blob',
+ },
+ ],
},
]);
});
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
index 4268634d302d235c595c73f8ec3546d3023a1a39..036b320b3143aef4aba1f9baf2f882b76a89416f 100644
--- a/spec/javascripts/diffs/store/utils_spec.js
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -502,6 +502,7 @@ describe('DiffsStoreUtils', () => {
fileHash: 'test',
key: 'app/index.js',
name: 'index.js',
+ parentPath: 'app/',
path: 'app/index.js',
removedLines: 10,
tempFile: false,
@@ -522,6 +523,7 @@ describe('DiffsStoreUtils', () => {
fileHash: 'test',
key: 'app/test/index.js',
name: 'index.js',
+ parentPath: 'app/test/',
path: 'app/test/index.js',
removedLines: 0,
tempFile: true,
@@ -535,6 +537,7 @@ describe('DiffsStoreUtils', () => {
fileHash: 'test',
key: 'app/test/filepathneedstruncating.js',
name: 'filepathneedstruncating.js',
+ parentPath: 'app/test/',
path: 'app/test/filepathneedstruncating.js',
removedLines: 0,
tempFile: true,
@@ -548,6 +551,7 @@ describe('DiffsStoreUtils', () => {
},
{
key: 'package.json',
+ parentPath: '/',
path: 'package.json',
name: 'package.json',
type: 'blob',
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index 92ebfc38722de7a085de5ed2d8d1f1dce2a20a4c..0a266b19ea51215d8a11cdf552346c4e66dbcf2c 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -135,4 +135,20 @@ describe('text_utility', () => {
expect(textUtils.getFirstCharacterCapitalized(null)).toEqual('');
});
});
+
+ describe('truncatePathMiddleToLength', () => {
+ it('does not truncate text', () => {
+ expect(textUtils.truncatePathMiddleToLength('app/test', 50)).toEqual('app/test');
+ });
+
+ it('truncates middle of the path', () => {
+ expect(textUtils.truncatePathMiddleToLength('app/test/diff', 13)).toEqual('app/…/diff');
+ });
+
+ it('truncates multiple times in the middle of the path', () => {
+ expect(textUtils.truncatePathMiddleToLength('app/test/merge_request/diff', 13)).toEqual(
+ 'app/…/…/diff',
+ );
+ });
+ });
});
diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js
index 67752c1c455c074bffef9b7f3af13546eecfb556..d1fd899c1a800782f517a167c1ab23025834d565 100644
--- a/spec/javascripts/vue_shared/components/file_row_spec.js
+++ b/spec/javascripts/vue_shared/components/file_row_spec.js
@@ -3,7 +3,7 @@ import FileRow from '~/vue_shared/components/file_row.vue';
import { file } from 'spec/ide/helpers';
import mountComponent from '../../helpers/vue_mount_component_helper';
-describe('RepoFile', () => {
+describe('File row component', () => {
let vm;
function createComponent(propsData) {
@@ -72,39 +72,16 @@ describe('RepoFile', () => {
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('renders header for file', () => {
+ createComponent({
+ file: {
+ isHeader: true,
+ path: 'app/assets',
+ tree: [],
+ },
+ level: 0,
});
- it('returns text truncated at start', done => {
- vm.truncateStart = 5;
-
- vm.$nextTick(() => {
- expect(vm.outputText).toBe('...ssets/index.js');
-
- done();
- });
- });
+ expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null);
});
});