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`] = ` +
+ + app/assets/javascripts/…/…/diffs/notes + +
+`; + +exports[`File row header component renders file path 1`] = ` +
+ + app/assets + +
+`; + +exports[`File row header component trucates path after 40 characters 1`] = ` +
+ + app/assets/javascripts/merge_requests + +
+`; 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); }); });