diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index a16d00b5cef..a75d1a4b8d0 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -54,12 +54,14 @@ LineHighlighter.prototype.bindEvents = function() { $fileHolder.on('highlight:line', this.highlightHash); }; -LineHighlighter.prototype.highlightHash = function() { - var range; +LineHighlighter.prototype.highlightHash = function(newHash) { + let range; + if (newHash && typeof newHash === 'string') this._hash = newHash; + + this.clearHighlight(); if (this._hash !== '') { range = this.hashToRange(this._hash); - if (range[0]) { this.highlightRange(range); const lineSelector = `#L${range[0]}`; diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 96d6a75bb61..02d9c775046 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -63,12 +63,7 @@ const RepoEditor = { const lineNumber = e.target.position.lineNumber; if (e.target.element.classList.contains('line-numbers')) { location.hash = `L${lineNumber}`; - Store.activeLine = lineNumber; - - Helper.monacoInstance.setPosition({ - lineNumber: this.activeLine, - column: 1, - }); + Store.setActiveLine(lineNumber); } }, }, @@ -101,6 +96,15 @@ const RepoEditor = { this.setupEditor(); } }, + + activeLine() { + if (Helper.monacoInstance) { + Helper.monacoInstance.setPosition({ + lineNumber: this.activeLine, + column: 1, + }); + } + }, }, computed: { shouldHideEditor() { diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index 2fe369a4693..a87bef6084a 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -14,6 +14,11 @@ export default { highlightFile() { $(this.$el).find('.file-content').syntaxHighlight(); }, + highlightLine() { + if (Store.activeLine > -1) { + this.lineHighlighter.highlightHash(`#L${Store.activeLine}`); + } + }, }, mounted() { this.highlightFile(); @@ -26,8 +31,12 @@ export default { html() { this.$nextTick(() => { this.highlightFile(); + this.highlightLine(); }); }, + activeLine() { + this.highlightLine(); + }, }, }; diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index 685f6ff806f..e0f3c33003a 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -18,22 +18,40 @@ export default { }, created() { - this.addPopEventListener(); + window.addEventListener('popstate', this.checkHistory); + }, + destroyed() { + window.removeEventListener('popstate', this.checkHistory); }, data: () => Store, methods: { - addPopEventListener() { - window.addEventListener('popstate', () => { - if (location.href.indexOf('#') > -1) return; - this.linkClicked({ + checkHistory() { + let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1); + if (!selectedFile) { + // Maybe it is not in the current tree but in the opened tabs + selectedFile = Helper.getFileFromPath(location.pathname); + } + + let lineNumber = null; + if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2)); + + if (selectedFile) { + if (selectedFile.url !== this.activeFile.url) { + this.fileClicked(selectedFile, lineNumber); + } else { + Store.setActiveLine(lineNumber); + } + } else { + // Not opened at all lets open new tab + this.fileClicked({ url: location.href, - }); - }); + }, lineNumber); + } }, - fileClicked(clickedFile) { + fileClicked(clickedFile, lineNumber) { let file = clickedFile; if (file.loading) return; file.loading = true; @@ -41,17 +59,20 @@ export default { if (file.type === 'tree' && file.opened) { file = Store.removeChildFilesOfTree(file); file.loading = false; + Store.setActiveLine(lineNumber); } else { const openFile = Helper.getFileFromPath(file.url); if (openFile) { file.loading = false; Store.setActiveFiles(openFile); + Store.setActiveLine(lineNumber); } else { Service.url = file.url; Helper.getContent(file) .then(() => { file.loading = false; Helper.scrollTabsRight(); + Store.setActiveLine(lineNumber); }) .catch(Helper.loadingError); } diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index ac59a2bed23..7483f8bc305 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -254,7 +254,9 @@ const RepoHelper = { RepoHelper.key = RepoHelper.genKey(); - history.pushState({ key: RepoHelper.key }, '', url); + if (document.location.pathname !== url) { + history.pushState({ key: RepoHelper.key }, '', url); + } if (title) { document.title = title; diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index 9a4fc40bc69..93b39cff27e 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -26,7 +26,7 @@ const RepoStore = { }, activeFile: Helper.getDefaultActiveFile(), activeFileIndex: 0, - activeLine: 0, + activeLine: -1, activeFileLabel: 'Raw', files: [], isCommitable: false, @@ -85,6 +85,7 @@ const RepoStore = { if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); RepoStore.binary = file.binary; + RepoStore.setActiveLine(-1); }, setFileActivity(file, openedFile, i) { @@ -101,6 +102,10 @@ const RepoStore = { RepoStore.activeFileIndex = i; }, + setActiveLine(activeLine) { + if (!isNaN(activeLine)) RepoStore.activeLine = activeLine; + }, + setActiveToRaw() { RepoStore.activeFile.raw = false; // can't get vue to listen to raw for some reason so RepoStore for now. diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index f15633bd8b9..620b604f404 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -29,15 +29,17 @@ describe('RepoFile', () => { }).$mount(); } - beforeEach(() => { - spyOn(repoFile.mixins[0].methods, 'timeFormated').and.returnValue(updated); - }); - it('renders link, icon, name and last commit details', () => { - const vm = createComponent({ - file, - activeFile, + const RepoFile = Vue.extend(repoFile); + const vm = new RepoFile({ + propsData: { + file, + activeFile, + }, }); + spyOn(vm, 'timeFormated').and.returnValue(updated); + vm.$mount(); + const name = vm.$el.querySelector('.repo-file-name'); const fileIcon = vm.$el.querySelector('.file-icon'); diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index 23c10ea022e..35d2b37ac2a 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -5,18 +5,26 @@ import RepoStore from '~/repo/stores/repo_store'; import repoSidebar from '~/repo/components/repo_sidebar.vue'; describe('RepoSidebar', () => { + let vm; + function createComponent() { const RepoSidebar = Vue.extend(repoSidebar); return new RepoSidebar().$mount(); } + afterEach(() => { + vm.$destroy(); + }); + it('renders a sidebar', () => { RepoStore.files = [{ id: 0, }]; RepoStore.openedFiles = []; - const vm = createComponent(); + RepoStore.isRoot = false; + + vm = createComponent(); const thead = vm.$el.querySelector('thead'); const tbody = vm.$el.querySelector('tbody'); @@ -35,7 +43,7 @@ describe('RepoSidebar', () => { RepoStore.openedFiles = [{ id: 0, }]; - const vm = createComponent(); + vm = createComponent(); expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy(); expect(vm.$el.querySelector('thead')).toBeFalsy(); @@ -47,7 +55,7 @@ describe('RepoSidebar', () => { tree: true, }; RepoStore.files = []; - const vm = createComponent(); + vm = createComponent(); expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5); }); @@ -57,7 +65,7 @@ describe('RepoSidebar', () => { id: 0, }]; RepoStore.isRoot = true; - const vm = createComponent(); + vm = createComponent(); expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); }); @@ -72,7 +80,7 @@ describe('RepoSidebar', () => { }; RepoStore.files = [file1]; RepoStore.isRoot = true; - const vm = createComponent(); + vm = createComponent(); vm.fileClicked(file1); @@ -87,7 +95,7 @@ describe('RepoSidebar', () => { spyOn(Helper, 'getFileFromPath').and.returnValue(file); spyOn(RepoStore, 'setActiveFiles'); - const vm = createComponent(); + vm = createComponent(); vm.fileClicked(file); expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file); @@ -103,7 +111,7 @@ describe('RepoSidebar', () => { }; RepoStore.files = [file1]; RepoStore.isRoot = true; - const vm = createComponent(); + vm = createComponent(); vm.fileClicked(file1); @@ -114,12 +122,48 @@ describe('RepoSidebar', () => { describe('goToPreviousDirectoryClicked', () => { it('should hide files in directory if already open', () => { const prevUrl = 'foo/bar'; - const vm = createComponent(); + vm = createComponent(); vm.goToPreviousDirectoryClicked(prevUrl); expect(RepoService.url).toEqual(prevUrl); }); }); + + describe('back button', () => { + const file1 = { + id: 1, + url: 'file1', + }; + const file2 = { + id: 2, + url: 'file2', + }; + RepoStore.files = [file1, file2]; + RepoStore.openedFiles = [file1, file2]; + RepoStore.isRoot = true; + + vm = createComponent(); + vm.fileClicked(file1); + + it('render previous file when using back button', () => { + spyOn(Helper, 'getContent').and.callThrough(); + + vm.fileClicked(file2); + expect(Helper.getContent).toHaveBeenCalledWith(file2); + Helper.getContent.calls.reset(); + + history.pushState({ + key: Math.random(), + }, '', file1.url); + const popEvent = document.createEvent('Event'); + popEvent.initEvent('popstate', true, true); + window.dispatchEvent(popEvent); + + expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(file1.url); + + window.history.pushState({}, null, '/'); + }); + }); }); }); diff --git a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js index b63633c03b8..e667b4b3677 100644 --- a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js +++ b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js @@ -31,6 +31,7 @@ describe('MRWidgetService', () => { }); it('should have methods defined', () => { + window.history.pushState({}, null, '/'); const service = new MRWidgetService(mr); expect(service.merge()).toBeDefined();