diff options
Diffstat (limited to 'app/assets/javascripts/merge_request_tabs.js')
-rw-r--r-- | app/assets/javascripts/merge_request_tabs.js | 653 |
1 files changed, 324 insertions, 329 deletions
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index de84e28f915..cacca35ca98 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -63,387 +63,382 @@ import syntaxHighlight from './syntax_highlight'; // /* eslint-enable max-len */ -(() => { - // Store the `location` object, allowing for easier stubbing in tests - let location = window.location; - - class MergeRequestTabs { - - constructor({ action, setUrl, stubLocation } = {}) { - const mergeRequestTabs = document.querySelector('.js-tabs-affix'); - const navbar = document.querySelector('.navbar-gitlab'); - const paddingTop = 16; - - this.diffsLoaded = false; - this.pipelinesLoaded = false; - this.commitsLoaded = false; - this.fixedLayoutPref = null; - - this.setUrl = setUrl !== undefined ? setUrl : true; - this.setCurrentAction = this.setCurrentAction.bind(this); - this.tabShown = this.tabShown.bind(this); - this.showTab = this.showTab.bind(this); - this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0; - - if (mergeRequestTabs) { - this.stickyTop += mergeRequestTabs.offsetHeight; - } +// Store the `location` object, allowing for easier stubbing in tests +let location = window.location; - if (stubLocation) { - location = stubLocation; - } +export default class MergeRequestTabs { - this.bindEvents(); - this.activateTab(action); - this.initAffix(); - } + constructor({ action, setUrl, stubLocation } = {}) { + const mergeRequestTabs = document.querySelector('.js-tabs-affix'); + const navbar = document.querySelector('.navbar-gitlab'); + const paddingTop = 16; - bindEvents() { - $(document) - .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) - .on('click', '.js-show-tab', this.showTab); + this.diffsLoaded = false; + this.pipelinesLoaded = false; + this.commitsLoaded = false; + this.fixedLayoutPref = null; - $('.merge-request-tabs a[data-toggle="tab"]') - .on('click', this.clickTab); - } + this.setUrl = setUrl !== undefined ? setUrl : true; + this.setCurrentAction = this.setCurrentAction.bind(this); + this.tabShown = this.tabShown.bind(this); + this.showTab = this.showTab.bind(this); + this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0; - // Used in tests - unbindEvents() { - $(document) - .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) - .off('click', '.js-show-tab', this.showTab); + if (mergeRequestTabs) { + this.stickyTop += mergeRequestTabs.offsetHeight; + } - $('.merge-request-tabs a[data-toggle="tab"]') - .off('click', this.clickTab); + if (stubLocation) { + location = stubLocation; } - destroyPipelinesView() { - if (this.commitPipelinesTable) { - this.commitPipelinesTable.$destroy(); - this.commitPipelinesTable = null; + this.bindEvents(); + this.activateTab(action); + this.initAffix(); + } - document.querySelector('#commit-pipeline-table-view').innerHTML = ''; - } - } + bindEvents() { + $(document) + .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .on('click', '.js-show-tab', this.showTab); - showTab(e) { - e.preventDefault(); - this.activateTab($(e.target).data('action')); - } + $('.merge-request-tabs a[data-toggle="tab"]') + .on('click', this.clickTab); + } - clickTab(e) { - if (e.currentTarget && isMetaClick(e)) { - const targetLink = e.currentTarget.getAttribute('href'); - e.stopImmediatePropagation(); - e.preventDefault(); - window.open(targetLink, '_blank'); - } + // Used in tests + unbindEvents() { + $(document) + .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .off('click', '.js-show-tab', this.showTab); + + $('.merge-request-tabs a[data-toggle="tab"]') + .off('click', this.clickTab); + } + + destroyPipelinesView() { + if (this.commitPipelinesTable) { + this.commitPipelinesTable.$destroy(); + this.commitPipelinesTable = null; + + document.querySelector('#commit-pipeline-table-view').innerHTML = ''; } + } - tabShown(e) { - const $target = $(e.target); - const action = $target.data('action'); + showTab(e) { + e.preventDefault(); + this.activateTab($(e.target).data('action')); + } - if (action === 'commits') { - this.loadCommits($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - this.destroyPipelinesView(); - } else if (this.isDiffAction(action)) { - this.loadDiff($target.attr('href')); - if (bp.getBreakpointSize() !== 'lg') { - this.shrinkView(); - } - if (this.diffViewType() === 'parallel') { - this.expandViewContainer(); - } - this.destroyPipelinesView(); - } else if (action === 'pipelines') { - this.resetViewContainer(); - this.mountPipelinesView(); - } else { - if (bp.getBreakpointSize() !== 'xs') { - this.expandView(); - } - this.resetViewContainer(); - this.destroyPipelinesView(); + clickTab(e) { + if (e.currentTarget && isMetaClick(e)) { + const targetLink = e.currentTarget.getAttribute('href'); + e.stopImmediatePropagation(); + e.preventDefault(); + window.open(targetLink, '_blank'); + } + } - initDiscussionTab(); + tabShown(e) { + const $target = $(e.target); + const action = $target.data('action'); + + if (action === 'commits') { + this.loadCommits($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + this.destroyPipelinesView(); + } else if (this.isDiffAction(action)) { + this.loadDiff($target.attr('href')); + if (bp.getBreakpointSize() !== 'lg') { + this.shrinkView(); } - if (this.setUrl) { - this.setCurrentAction(action); + if (this.diffViewType() === 'parallel') { + this.expandViewContainer(); } + this.destroyPipelinesView(); + } else if (action === 'pipelines') { + this.resetViewContainer(); + this.mountPipelinesView(); + } else { + if (bp.getBreakpointSize() !== 'xs') { + this.expandView(); + } + this.resetViewContainer(); + this.destroyPipelinesView(); + + initDiscussionTab(); + } + if (this.setUrl) { + this.setCurrentAction(action); } + } - scrollToElement(container) { - if (location.hash) { - const offset = 0 - ( - $('.navbar-gitlab').outerHeight() + - $('.js-tabs-affix').outerHeight() - ); - const $el = $(`${container} ${location.hash}:not(.match)`); - if ($el.length) { - $.scrollTo($el[0], { offset }); - } + scrollToElement(container) { + if (location.hash) { + const offset = 0 - ( + $('.navbar-gitlab').outerHeight() + + $('.js-tabs-affix').outerHeight() + ); + const $el = $(`${container} ${location.hash}:not(.match)`); + if ($el.length) { + $.scrollTo($el[0], { offset }); } } + } + + // Activate a tab based on the current action + activateTab(action) { + // important note: the .tab('show') method triggers 'shown.bs.tab' event itself + $(`.merge-request-tabs a[data-action='${action}']`).tab('show'); + } - // Activate a tab based on the current action - activateTab(action) { - // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(`.merge-request-tabs a[data-action='${action}']`).tab('show'); + // Replaces the current Merge Request-specific action in the URL with a new one + // + // If the action is "notes", the URL is reset to the standard + // `MergeRequests#show` route. + // + // Examples: + // + // location.pathname # => "/namespace/project/merge_requests/1" + // setCurrentAction('diffs') + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('show') + // location.pathname # => "/namespace/project/merge_requests/1" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('commits') + // location.pathname # => "/namespace/project/merge_requests/1/commits" + // + // Returns the new URL String + setCurrentAction(action) { + this.currentAction = action; + + // Remove a trailing '/commits' '/diffs' '/pipelines' + let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, ''); + + // Append the new action if we're on a tab other than 'notes' + if (this.currentAction !== 'show' && this.currentAction !== 'new') { + newState += `/${this.currentAction}`; } - // Replaces the current Merge Request-specific action in the URL with a new one - // - // If the action is "notes", the URL is reset to the standard - // `MergeRequests#show` route. - // - // Examples: - // - // location.pathname # => "/namespace/project/merge_requests/1" - // setCurrentAction('diffs') - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('show') - // location.pathname # => "/namespace/project/merge_requests/1" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('commits') - // location.pathname # => "/namespace/project/merge_requests/1/commits" - // - // Returns the new URL String - setCurrentAction(action) { - this.currentAction = action; + // Ensure parameters and hash come along for the ride + newState += location.search + location.hash; - // Remove a trailing '/commits' '/diffs' '/pipelines' - let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, ''); + // TODO: Consider refactoring in light of turbolinks removal. - // Append the new action if we're on a tab other than 'notes' - if (this.currentAction !== 'show' && this.currentAction !== 'new') { - newState += `/${this.currentAction}`; - } + // Replace the current history state with the new one without breaking + // Turbolinks' history. + // + // See https://github.com/rails/turbolinks/issues/363 + window.history.replaceState({ + url: newState, + }, document.title, newState); - // Ensure parameters and hash come along for the ride - newState += location.search + location.hash; + return newState; + } - // TODO: Consider refactoring in light of turbolinks removal. + loadCommits(source) { + if (this.commitsLoaded) { + return; + } + this.ajaxGet({ + url: `${source}.json`, + success: (data) => { + document.querySelector('div#commits').innerHTML = data.html; + localTimeAgo($('.js-timeago', 'div#commits')); + this.commitsLoaded = true; + this.scrollToElement('#commits'); + }, + }); + } - // Replace the current history state with the new one without breaking - // Turbolinks' history. - // - // See https://github.com/rails/turbolinks/issues/363 - window.history.replaceState({ - url: newState, - }, document.title, newState); + mountPipelinesView() { + const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); + const CommitPipelinesTable = gl.CommitPipelinesTable; + this.commitPipelinesTable = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, + errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, + autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, + }, + }).$mount(); + + // $mount(el) replaces the el with the new rendered component. We need it in order to mount + // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount + pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el); + } - return newState; + loadDiff(source) { + if (this.diffsLoaded) { + document.dispatchEvent(new CustomEvent('scroll')); + return; } - loadCommits(source) { - if (this.commitsLoaded) { - return; - } - this.ajaxGet({ - url: `${source}.json`, - success: (data) => { - document.querySelector('div#commits').innerHTML = data.html; - localTimeAgo($('.js-timeago', 'div#commits')); - this.commitsLoaded = true; - this.scrollToElement('#commits'); - }, - }); - } + // We extract pathname for the current Changes tab anchor href + // some pages like MergeRequestsController#new has query parameters on that anchor + const urlPathname = parseUrlPathname(source); - mountPipelinesView() { - const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - const CommitPipelinesTable = gl.CommitPipelinesTable; - this.commitPipelinesTable = new CommitPipelinesTable({ - propsData: { - endpoint: pipelineTableViewEl.dataset.endpoint, - helpPagePath: pipelineTableViewEl.dataset.helpPagePath, - emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, - errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, - autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, - }, - }).$mount(); + this.ajaxGet({ + url: `${urlPathname}.json${location.search}`, + success: (data) => { + const $container = $('#diffs'); + $container.html(data.html); - // $mount(el) replaces the el with the new rendered component. We need it in order to mount - // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount - pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el); - } + initChangesDropdown(this.stickyTop); - loadDiff(source) { - if (this.diffsLoaded) { - document.dispatchEvent(new CustomEvent('scroll')); - return; - } + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + + localTimeAgo($('.js-timeago', 'div#diffs')); + syntaxHighlight($('#diffs .js-syntax-highlight')); - // We extract pathname for the current Changes tab anchor href - // some pages like MergeRequestsController#new has query parameters on that anchor - const urlPathname = parseUrlPathname(source); - - this.ajaxGet({ - url: `${urlPathname}.json${location.search}`, - success: (data) => { - const $container = $('#diffs'); - $container.html(data.html); - - initChangesDropdown(this.stickyTop); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } - - localTimeAgo($('.js-timeago', 'div#diffs')); - syntaxHighlight($('#diffs .js-syntax-highlight')); - - if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { - this.expandViewContainer(); - } - this.diffsLoaded = true; - - new Diff(); - this.scrollToElement('#diffs'); - - $('.diff-file').each((i, el) => { - new BlobForkSuggestion({ - openButtons: $(el).find('.js-edit-blob-link-fork-toggler'), - forkButtons: $(el).find('.js-fork-suggestion-button'), - cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'), - suggestionSections: $(el).find('.js-file-fork-suggestion-section'), - actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'), - }) - .init(); + if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { + this.expandViewContainer(); + } + this.diffsLoaded = true; + + new Diff(); + this.scrollToElement('#diffs'); + + $('.diff-file').each((i, el) => { + new BlobForkSuggestion({ + openButtons: $(el).find('.js-edit-blob-link-fork-toggler'), + forkButtons: $(el).find('.js-fork-suggestion-button'), + cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'), + suggestionSections: $(el).find('.js-file-fork-suggestion-section'), + actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'), + }) + .init(); + }); + + // Scroll any linked note into view + // Similar to `toggler_behavior` in the discussion tab + const hash = getLocationHash(); + const anchor = hash && $container.find(`.note[id="${hash}"]`); + if (anchor && anchor.length > 0) { + const notesContent = anchor.closest('.notes_content'); + const lineType = notesContent.hasClass('new') ? 'new' : 'old'; + notes.toggleDiffNote({ + target: anchor, + lineType, + forceShow: true, }); + anchor[0].scrollIntoView(); + handleLocationHash(); + // We have multiple elements on the page with `#note_xxx` + // (discussion and diff tabs) and `:target` only applies to the first + anchor.addClass('target'); + } + }, + }); + } - // Scroll any linked note into view - // Similar to `toggler_behavior` in the discussion tab - const hash = getLocationHash(); - const anchor = hash && $container.find(`.note[id="${hash}"]`); - if (anchor && anchor.length > 0) { - const notesContent = anchor.closest('.notes_content'); - const lineType = notesContent.hasClass('new') ? 'new' : 'old'; - notes.toggleDiffNote({ - target: anchor, - lineType, - forceShow: true, - }); - anchor[0].scrollIntoView(); - handleLocationHash(); - // We have multiple elements on the page with `#note_xxx` - // (discussion and diff tabs) and `:target` only applies to the first - anchor.addClass('target'); - } - }, - }); - } + // Show or hide the loading spinner + // + // status - Boolean, true to show, false to hide + toggleLoading(status) { + $('.mr-loading-status .loading').toggle(status); + } - // Show or hide the loading spinner - // - // status - Boolean, true to show, false to hide - toggleLoading(status) { - $('.mr-loading-status .loading').toggle(status); - } + ajaxGet(options) { + const defaults = { + beforeSend: () => this.toggleLoading(true), + error: () => new Flash('An error occurred while fetching this tab.', 'alert'), + complete: () => this.toggleLoading(false), + dataType: 'json', + type: 'GET', + }; + $.ajax($.extend({}, defaults, options)); + } - ajaxGet(options) { - const defaults = { - beforeSend: () => this.toggleLoading(true), - error: () => new Flash('An error occurred while fetching this tab.', 'alert'), - complete: () => this.toggleLoading(false), - dataType: 'json', - type: 'GET', - }; - $.ajax($.extend({}, defaults, options)); - } + diffViewType() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } - diffViewType() { - return $('.inline-parallel-buttons a.active').data('view-type'); - } + isDiffAction(action) { + return action === 'diffs' || action === 'new/diffs'; + } - isDiffAction(action) { - return action === 'diffs' || action === 'new/diffs'; + expandViewContainer() { + const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs'); + if (this.fixedLayoutPref === null) { + this.fixedLayoutPref = $wrapper.hasClass('container-limited'); } + $wrapper.removeClass('container-limited'); + } - expandViewContainer() { - const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs'); - if (this.fixedLayoutPref === null) { - this.fixedLayoutPref = $wrapper.hasClass('container-limited'); - } - $wrapper.removeClass('container-limited'); + resetViewContainer() { + if (this.fixedLayoutPref !== null) { + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', this.fixedLayoutPref); } + } - resetViewContainer() { - if (this.fixedLayoutPref !== null) { - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', this.fixedLayoutPref); - } - } + shrinkView() { + const $gutterIcon = $('.js-sidebar-toggle i:visible'); - shrinkView() { - const $gutterIcon = $('.js-sidebar-toggle i:visible'); + // Wait until listeners are set + setTimeout(() => { + // Only when sidebar is expanded + if ($gutterIcon.is('.fa-angle-double-right')) { + $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + } - // Wait until listeners are set - setTimeout(() => { - // Only when sidebar is expanded - if ($gutterIcon.is('.fa-angle-double-right')) { - $gutterIcon.closest('a').trigger('click', [true]); - } - }, 0); + // Expand the issuable sidebar unless the user explicitly collapsed it + expandView() { + if (Cookies.get('collapsed_gutter') === 'true') { + return; } + const $gutterIcon = $('.js-sidebar-toggle i:visible'); - // Expand the issuable sidebar unless the user explicitly collapsed it - expandView() { - if (Cookies.get('collapsed_gutter') === 'true') { - return; + // Wait until listeners are set + setTimeout(() => { + // Only when sidebar is collapsed + if ($gutterIcon.is('.fa-angle-double-left')) { + $gutterIcon.closest('a').trigger('click', [true]); } - const $gutterIcon = $('.js-sidebar-toggle i:visible'); + }, 0); + } - // Wait until listeners are set - setTimeout(() => { - // Only when sidebar is collapsed - if ($gutterIcon.is('.fa-angle-double-left')) { - $gutterIcon.closest('a').trigger('click', [true]); - } - }, 0); - } + initAffix() { + const $tabs = $('.js-tabs-affix'); + const $fixedNav = $('.navbar-gitlab'); + + // Screen space on small screens is usually very sparse + // So we dont affix the tabs on these + if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return; + + /** + If the browser does not support position sticky, it returns the position as static. + If the browser does support sticky, then we allow the browser to handle it, if not + then we default back to Bootstraps affix + **/ + if ($tabs.css('position') !== 'static') return; + + const $diffTabs = $('#diff-notes-app'); + + $tabs.off('affix.bs.affix affix-top.bs.affix') + .affix({ + offset: { + top: () => ( + $diffTabs.offset().top - $tabs.height() - $fixedNav.height() + ), + }, + }) + .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) + .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' })); - initAffix() { - const $tabs = $('.js-tabs-affix'); - const $fixedNav = $('.navbar-gitlab'); - - // Screen space on small screens is usually very sparse - // So we dont affix the tabs on these - if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return; - - /** - If the browser does not support position sticky, it returns the position as static. - If the browser does support sticky, then we allow the browser to handle it, if not - then we default back to Bootstraps affix - **/ - if ($tabs.css('position') !== 'static') return; - - const $diffTabs = $('#diff-notes-app'); - - $tabs.off('affix.bs.affix affix-top.bs.affix') - .affix({ - offset: { - top: () => ( - $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - ), - }, - }) - .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) - .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' })); - - // Fix bug when reloading the page already scrolling - if ($tabs.hasClass('affix')) { - $tabs.trigger('affix.bs.affix'); - } + // Fix bug when reloading the page already scrolling + if ($tabs.hasClass('affix')) { + $tabs.trigger('affix.bs.affix'); } } - - window.gl = window.gl || {}; - window.gl.MergeRequestTabs = MergeRequestTabs; -})(); +} |