diff options
author | Phil Hughes <me@iamphill.com> | 2017-11-20 16:17:59 +0300 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2017-11-21 14:27:58 +0300 |
commit | c0b26796272f09e512cd20674707b033442c956c (patch) | |
tree | 3f878fc10e089566456b09685a89e54850b7b6cc | |
parent | ee22172edd987e902903e3b12dbbfb855ce4145e (diff) |
fixed up some JS based on comments
added feature spec
18 files changed, 415 insertions, 33 deletions
diff --git a/app/assets/javascripts/repo/components/blob_viewer_switch.vue b/app/assets/javascripts/repo/components/blob_viewer_switch.vue index cd1fd00bdf7..0b4379773a6 100644 --- a/app/assets/javascripts/repo/components/blob_viewer_switch.vue +++ b/app/assets/javascripts/repo/components/blob_viewer_switch.vue @@ -13,6 +13,9 @@ displayTooltip() { return `Display ${this.activeFile.simple.switcherTitle}`; }, + richTooltip() { + return `Display ${this.activeFile.rich.switcherTitle}`; + }, }, methods: { ...mapActions([ @@ -24,18 +27,19 @@ <template> <div - class="btn-group" + class="btn-group js-blob-viewer-switcher" role="group" > <button v-tooltip type="button" - class="btn btn-default btn-sm" + class="btn btn-default btn-sm js-blob-viewer-switch-btn" :class="{ active: activeFile.currentViewer === 'simple', }" :title="displayTooltip" data-container="body" + data-viewer="simple" @click="changeFileViewer({ file: activeFile, type: 'simple' })" > <i @@ -48,12 +52,13 @@ <button v-tooltip type="button" - class="btn btn-default btn-sm" + class="btn btn-default btn-sm js-blob-viewer-switch-btn" :class="{ active: activeFile.currentViewer === 'rich', }" - title="Display rendered file" + :title="richTooltip" data-container="body" + data-viewer="rich" @click="changeFileViewer({ file: activeFile, type: 'rich' })" > <i diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 6dd7c30cc23..aba91dc1d63 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -92,7 +92,7 @@ export default { 'activeFile', ]), shouldHideEditor() { - return this.activeFile.binary && !this.activeFile.raw; + return (this.activeFile.binary || this.activeFile.storedExternally) && !this.activeFile.raw; }, }, }; diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue index 3560206909b..dfbd282ba20 100644 --- a/app/assets/javascripts/repo/components/repo_file_buttons.vue +++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue @@ -1,9 +1,13 @@ <script> import { mapGetters } from 'vuex'; +import tooltip from '../../vue_shared/directives/tooltip'; import viewerSwitch from './blob_viewer_switch.vue'; import sourceCopyButton from './source_copy_button.vue'; export default { + directives: { + tooltip, + }, components: { viewerSwitch, sourceCopyButton, @@ -12,6 +16,7 @@ export default { ...mapGetters([ 'activeFile', 'canActiveFileSwitchViewer', + 'canCopySource', ]), showButtons() { return this.activeFile.rawPath || @@ -20,10 +25,10 @@ export default { this.activeFile.permalink; }, rawDownloadButtonLabel() { - return this.activeFile.binary ? 'Download' : 'Raw'; + return this.activeFile.binary || this.activeFile.storedExternally ? 'Download' : 'Open raw'; }, rawDownloadButtonIcon() { - return this.activeFile.binary ? 'fa-download' : 'fa-file-code-o'; + return this.activeFile.binary || this.activeFile.storedExternally ? 'fa-download' : 'fa-file-code-o'; }, }, }; @@ -42,14 +47,17 @@ export default { role="group" > <source-copy-button - v-if="canActiveFileSwitchViewer" + v-if="canCopySource" /> <a + v-tooltip :href="activeFile.rawPath" target="_blank" class="btn btn-default btn-sm raw" rel="noopener noreferrer" :aria-label="rawDownloadButtonLabel" + :title="rawDownloadButtonLabel" + data-container="body" > <i class="fa" diff --git a/app/assets/javascripts/repo/components/source_copy_button.vue b/app/assets/javascripts/repo/components/source_copy_button.vue index 889a05dec03..3a21c214fa1 100644 --- a/app/assets/javascripts/repo/components/source_copy_button.vue +++ b/app/assets/javascripts/repo/components/source_copy_button.vue @@ -54,7 +54,7 @@ <button v-tooltip type="button" - class="btn btn-default btn-sm" + class="btn btn-default btn-sm js-copy-blob-source-btn" :class="{ disabled: copySourceButtonDisabled, }" diff --git a/app/assets/javascripts/repo/components/viewers/error.vue b/app/assets/javascripts/repo/components/viewers/error.vue index 607ab96cc67..44f53fce875 100644 --- a/app/assets/javascripts/repo/components/viewers/error.vue +++ b/app/assets/javascripts/repo/components/viewers/error.vue @@ -17,14 +17,15 @@ text: 'load it anyway', callback: () => this.getFileHTML({ file: this.activeFile, - expanded: true, + override: true, }), }); } if ( this.activeFile.simple.name === 'text' && - (this.activeFileCurrentViewer.renderError === 'server_side_but_stored_externally' || this.activeFileCurrentViewer.renderError === 'too_large') + this.activeFile.currentViewer === 'rich' && + (this.activeFileCurrentViewer.renderError !== 'server_side_but_stored_externally') ) { links.push({ href: '#', @@ -64,7 +65,7 @@ <div class="nothing-here-block" > - The {{ activeFileCurrentViewer.name }} could not be displayed because {{ activeFileCurrentViewer.renderErrorReason }}. + The {{ activeFileCurrentViewer.switcherTitle }} could not be displayed because {{ activeFileCurrentViewer.renderErrorReason }}. You can <template v-for="(link, index) in optionLinks" diff --git a/app/assets/javascripts/repo/components/viewers/html.vue b/app/assets/javascripts/repo/components/viewers/html.vue index 44d4a26577f..d933e3cb9b2 100644 --- a/app/assets/javascripts/repo/components/viewers/html.vue +++ b/app/assets/javascripts/repo/components/viewers/html.vue @@ -15,10 +15,7 @@ }, methods: { highlightFile() { - const $el = $(this.$el).find('.file-content'); - - $el.syntaxHighlight(); - $el.renderGFM(); + $(this.$el).find('.blob-viewer').renderGFM(); }, }, mounted() { @@ -32,6 +29,7 @@ this.$nextTick(() => { this.highlightFile(); this.lineHighlighter.highlightHash(); + gl.utils.handleLocationHash(); }); }, }; diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js index 483b5803cf2..b1908cfaafb 100644 --- a/app/assets/javascripts/repo/services/index.js +++ b/app/assets/javascripts/repo/services/index.js @@ -11,8 +11,8 @@ export default { getFileData(endpoint) { return Vue.http.get(endpoint, { params: { format: 'json' } }); }, - getFileHTML(endpoint, expanded) { - return Vue.http.get(endpoint, { params: { expanded } }); + getFileHTML(endpoint) { + return Vue.http.get(endpoint); }, getRawFileData(file) { if (file.tempFile) { diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js index e3fab085ab3..7fa53f5e26a 100644 --- a/app/assets/javascripts/repo/stores/actions.js +++ b/app/assets/javascripts/repo/stores/actions.js @@ -117,7 +117,9 @@ export const createTempEntry = ({ state, dispatch }, { name, type, content = '', } }; -export const popHistoryState = ({ state, dispatch, getters }) => { +export const popHistoryState = ({ state, dispatch, getters }, e) => { + if (!e.state) return; + const treeList = getters.treeList; const tree = treeList.find(file => file.url === state.previousUrl); diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/repo/stores/actions/file.js index b7b81f4ff30..11560bfb7a8 100644 --- a/app/assets/javascripts/repo/stores/actions/file.js +++ b/app/assets/javascripts/repo/stores/actions/file.js @@ -111,10 +111,10 @@ export const createTempFile = ({ state, commit, dispatch }, { tree, name, conten return Promise.resolve(file); }; -export const getFileHTML = ({ commit, getters }, { file, expanded = false }) => { +export const getFileHTML = ({ commit, getters }, { file, override = false }) => { const currentViewer = getters.activeFileCurrentViewer; - if (expanded) { + if (override) { commit(types.RESET_VIEWER_RENDER_ERROR, currentViewer); } @@ -122,7 +122,7 @@ export const getFileHTML = ({ commit, getters }, { file, expanded = false }) => commit(types.TOGGLE_FILE_VIEWER_LOADING, currentViewer); - service.getFileHTML(currentViewer.path, expanded) + service.getFileHTML(currentViewer.path) .then(res => res.json()) .then((data) => { commit(types.TOGGLE_FILE_VIEWER_LOADING, currentViewer); diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js index abbc5869d4b..55c187159cf 100644 --- a/app/assets/javascripts/repo/stores/getters.js +++ b/app/assets/javascripts/repo/stores/getters.js @@ -31,10 +31,19 @@ export const activeFileCurrentViewer = (state) => { export const canActiveFileSwitchViewer = (state) => { const file = activeFile(state); - if (!file) return false; + if (!file || state.editMode) return false; if (file.binary) return false; - return file.rich.path !== '' && file.simple.path !== '' && file.simple.name === 'text' && !file.simple.renderError; + return file.rich.path !== '' && file.simple.path !== '' && file.simple.name === 'text' && !file.binary; +}; + +export const canCopySource = (state) => { + const file = activeFile(state); + + if (!file || state.editMode) return false; + if (file.binary) return false; + + return file.simple.name === 'text' && !file.simple.renderError; }; export const isCollapsed = state => !!state.openFiles.length; diff --git a/app/assets/javascripts/repo/stores/mutations/file.js b/app/assets/javascripts/repo/stores/mutations/file.js index 710780e677b..e96d73282f4 100644 --- a/app/assets/javascripts/repo/stores/mutations/file.js +++ b/app/assets/javascripts/repo/stores/mutations/file.js @@ -31,6 +31,7 @@ export default { renderError: data.render_error, currentViewer: data.rich_viewer ? 'rich' : 'simple', extension: data.extension, + storedExternally: data.stored_externally, }); createViewerStructure(file, 'rich', data); diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/repo/stores/utils.js index 803377afaa1..5d4864074fb 100644 --- a/app/assets/javascripts/repo/stores/utils.js +++ b/app/assets/javascripts/repo/stores/utils.js @@ -44,6 +44,7 @@ export const dataStructure = () => ({ currentViewer: 'rich', rich: viewerDataStructure(), simple: viewerDataStructure(), + storedExternally: false, }); export const decorateData = (entity) => { diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 0dca17c2b50..b7876775dc9 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -94,7 +94,7 @@ overflow: auto; .blob-full-height, - .file-content:not(.wiki) { + .file-content:not(.wiki):not(.image_file) { display: flex; } diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 2e49e2ab0fa..00b3d4c8e18 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -49,7 +49,7 @@ class Projects::BlobController < Projects::ApplicationController end def viewer - conditionally_expand_blob(@blob) + @blob.expand! respond_to do |format| format.json do diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb index 46cb63fda0a..86afcc86aa0 100644 --- a/app/models/blob_viewer/server_side.rb +++ b/app/models/blob_viewer/server_side.rb @@ -5,7 +5,7 @@ module BlobViewer included do self.load_async = true self.collapse_limit = 2.megabytes - self.size_limit = 5.megabyte + self.size_limit = 5.megabytes end def prepare! diff --git a/spec/features/projects/tree/multi_file_blob_show_spec.rb b/spec/features/projects/tree/multi_file_blob_show_spec.rb new file mode 100644 index 00000000000..3d1e8d6d4a8 --- /dev/null +++ b/spec/features/projects/tree/multi_file_blob_show_spec.rb @@ -0,0 +1,357 @@ +require 'spec_helper' + +feature 'File blob', :js do + let(:project) { create(:project, :public, :repository) } + + def visit_blob(path, file: '', anchor: nil, ref: 'master') + visit project_tree_path(project, File.join(ref, path), anchor: anchor) + + wait_for_requests + + click_link file + + wait_for_requests + end + + before do + set_cookie('new_repo', 'true') + end + + context 'Ruby file' do + before do + visit_blob('files/ruby', file: 'popen.rb') + end + + it 'displays the blob' do + aggregate_failures do + # shows highlighted Ruby code + expect(page).to have_css(".js-syntax-highlight") + expect(page).to have_content("require 'fileutils'") + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_selector('a[data-original-title="Open raw"]') + end + end + end + + context 'Markdown file' do + context 'visiting directly' do + before do + visit_blob('files/markdown', file: 'ruby-style-guide.md') + end + + it 'displays the blob using the rich viewer' do + aggregate_failures do + # hides the simple viewer + expect(page).not_to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows rendered Markdown + expect(page).to have_link("PEP-8") + + # shows a viewer switcher + expect(page).to have_selector('.js-blob-viewer-switcher') + + # shows a disabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_selector('a[data-original-title="Open raw"]') + end + end + + context 'switching to the simple viewer' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=simple]').click + + wait_for_requests + end + + it 'displays the blob using the simple viewer' do + aggregate_failures do + # hides the rich viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]') + expect(page).not_to have_selector('.blob-viewer[data-type="rich"]') + + # shows highlighted Markdown code + expect(page).to have_css(".js-syntax-highlight") + expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + + context 'switching to the rich viewer again' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=rich]').click + + wait_for_requests + end + + it 'displays the blob using the rich viewer' do + aggregate_failures do + # hides the simple viewer + expect(page).not_to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + end + end + end + end + + context 'Markdown file (stored in LFS)' do + before do + project.add_master(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add Markdown in LFS", + file_path: 'files/lfs/file.md', + file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data + ).execute + end + + context 'when LFS is enabled on the project' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + project.update_attribute(:lfs_enabled, true) + + visit_blob('files/lfs', file: 'file.md') + end + + it 'displays an error' do + aggregate_failures do + # shows an error message + expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS. You can download it instead.') + + # shows a viewer switcher + expect(page).to have_selector('.js-blob-viewer-switcher') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_selector('a[data-original-title="Download"]') + end + end + + context 'switching to the simple viewer' do + before do + find('.js-blob-viewer-switcher .js-blob-viewer-switch-btn[data-viewer=simple]').click + + wait_for_requests + end + + it 'displays an error' do + aggregate_failures do + # shows an error message + expect(page).to have_content('The source could not be displayed because it is stored in LFS. You can download it instead.') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + end + end + end + end + + context 'when LFS is disabled on the project' do + before do + visit_blob('files/lfs', file: 'file.md') + end + + it 'displays the blob' do + aggregate_failures do + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_selector('a[data-original-title="Open raw"]') + end + end + end + end + + xcontext 'PDF file' do + before do + project.add_master(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add PDF", + file_path: 'files/test.pdf', + file_content: project.repository.blob_at('add-pdf-file', 'files/pdf/test.pdf').data + ).execute + + visit_blob('files', file: 'test.pdf') + end + + it 'displays the blob' do + aggregate_failures do + # shows rendered PDF + expect(page).to have_selector('.js-pdf-viewer') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_selector('a[data-original-title="Download"]') + end + end + end + + context 'ISO file (stored in LFS)' do + context 'when LFS is enabled on the project' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + project.update_attribute(:lfs_enabled, true) + + visit_blob('files/lfs', file: 'lfs_object.iso') + end + + it 'displays the blob' do + aggregate_failures do + # shows a download link + expect(page).to have_link('Download (1.5 MB)') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_selector('a[data-original-title="Download"]') + end + end + end + + context 'when LFS is disabled on the project' do + before do + visit_blob('files/lfs', file: 'lfs_object.iso') + end + + it 'displays the blob' do + aggregate_failures do + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_selector('a[data-original-title="Open raw"]') + end + end + end + end + + context 'ZIP file' do + before do + visit_blob('', file: 'Gemfile.zip') + end + + it 'displays the blob' do + aggregate_failures do + # shows a download link + expect(page).to have_link('Download (2.11 KB)') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_selector('a[data-original-title="Download"]') + end + end + end + + context 'empty file' do + before do + project.add_master(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add empty file", + file_path: 'files/empty.md', + file_content: '' + ).execute + + visit_blob('files', file: 'empty.md') + end + + it 'displays an error' do + aggregate_failures do + # shows an error message + expect(page).to have_content('Empty file') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # does not show a download or raw button + expect(page).not_to have_selector('a[data-original-title="Download"]') + end + end + end + + context 'binary file that appears to be text in the first 1024 bytes' do + before do + visit_blob('encoding', file: 'binary-1.bin', ref: 'binary-encoding') + end + + it 'displays the blob' do + aggregate_failures do + # shows a download link + expect(page).to have_link('Download (23.8 KB)') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # The specs below verify an arguably incorrect result, but since we only + # learn that the file is not actually text once the text viewer content + # is loaded asynchronously, there is no straightforward way to get these + # synchronously loaded elements to display correctly. + # + # Clicking the copy button will result in nothing being copied. + # Clicking the raw button will result in the binary file being downloaded, + # as expected. + + # shows an enabled copy button, incorrectly + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button, incorrectly + expect(page).to have_selector('a[data-original-title="Open raw"]') + end + end + end +end diff --git a/spec/javascripts/repo/components/viewers/error_spec.js b/spec/javascripts/repo/components/viewers/error_spec.js index 0d5dbd28509..073d153c63f 100644 --- a/spec/javascripts/repo/components/viewers/error_spec.js +++ b/spec/javascripts/repo/components/viewers/error_spec.js @@ -53,12 +53,12 @@ describe('Multi-file editor error viewer', () => { spyOn(vm, 'getFileHTML'); }); - it('calls getFileHTML with expanded when clicking load anyway link', () => { + it('calls getFileHTML with override when clicking load anyway link', () => { vm.$el.querySelector('a').click(); expect(vm.getFileHTML).toHaveBeenCalledWith({ file: f, - expanded: true, + override: true, }); }); }); diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js index 6473069f8d0..def29c0554f 100644 --- a/spec/javascripts/repo/stores/actions/file_spec.js +++ b/spec/javascripts/repo/stores/actions/file_spec.js @@ -509,10 +509,10 @@ describe('Multi-file store file actions', () => { }).catch(done.fail); }); - it('calls service if file has renderError and expanded is true', (done) => { + it('calls service if file has renderError and override is true', (done) => { localFile.rich.renderError = 'error'; - store.dispatch('getFileHTML', { file: localFile, expanded: true }) + store.dispatch('getFileHTML', { file: localFile, override: true }) .then(() => { expect(service.getFileHTML).toHaveBeenCalled(); @@ -520,10 +520,10 @@ describe('Multi-file store file actions', () => { }).catch(done.fail); }); - it('resets renderError if expanded is true', (done) => { + it('resets renderError if override is true', (done) => { localFile.rich.renderError = 'error'; - store.dispatch('getFileHTML', { file: localFile, expanded: true }) + store.dispatch('getFileHTML', { file: localFile, override: true }) .then(Vue.nextTick) .then(() => { expect(localFile.rich.renderError).not.toBe('error'); |