merge_request_tabs.js.es6 10.8 KB
Newer Older
1
/* eslint-disable no-new, class-methods-use-this */
2 3 4 5
/* global Breakpoints */
/* global Cookies */
/* global DiffNotesApp */
/* global Flash */
6 7 8 9 10

/*= require js.cookie */
/*= require breakpoints */

/* eslint-disable max-len */
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
// MergeRequestTabs
//
// Handles persisting and restoring the current tab selection and lazily-loading
// content on the MergeRequests#show page.
//
// ### Example Markup
//
//   <ul class="nav-links merge-request-tabs">
//     <li class="notes-tab active">
//       <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
//         Discussion
//       </a>
//     </li>
//     <li class="commits-tab">
//       <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
//         Commits
//       </a>
//     </li>
//     <li class="diffs-tab">
//       <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
//         Diffs
//       </a>
//     </li>
//   </ul>
//
//   <div class="tab-content">
//     <div class="notes tab-pane active" id="notes">
//       Notes Content
//     </div>
//     <div class="commits tab-pane" id="commits">
//       Commits Content
//     </div>
//     <div class="diffs tab-pane" id="diffs">
//       Diffs Content
//     </div>
//   </div>
//
//   <div class="mr-loading-status">
//     <div class="loading">
//       Loading Animation
//     </div>
//   </div>
//
54 55
/* eslint-enable max-len */

56
(() => {
57 58
  // Store the `location` object, allowing for easier stubbing in tests
  let location = window.location;
F
Fatih Acet 已提交
59

60
  class MergeRequestTabs {
F
Fatih Acet 已提交
61

62
    constructor({ action, setUrl, stubLocation } = {}) {
63 64 65 66
      this.diffsLoaded = false;
      this.pipelinesLoaded = false;
      this.commitsLoaded = false;
      this.fixedLayoutPref = null;
F
Fatih Acet 已提交
67

68 69 70 71
      this.setUrl = setUrl !== undefined ? setUrl : true;
      this.setCurrentAction = this.setCurrentAction.bind(this);
      this.tabShown = this.tabShown.bind(this);
      this.showTab = this.showTab.bind(this);
F
Fatih Acet 已提交
72

73 74 75 76
      if (stubLocation) {
        location = stubLocation;
      }

F
Fatih Acet 已提交
77
      this.bindEvents();
78
      this.activateTab(action);
79
      this.initAffix();
F
Fatih Acet 已提交
80 81
    }

82
    bindEvents() {
83 84 85 86
      $(document)
        .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
        .on('click', '.js-show-tab', this.showTab);
    }
87

88
    unbindEvents() {
89 90 91 92
      $(document)
        .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
        .off('click', '.js-show-tab', this.showTab);
    }
F
Fatih Acet 已提交
93

94 95 96
    showTab(e) {
      e.preventDefault();
      this.activateTab($(e.target).data('action'));
97
    }
F
Fatih Acet 已提交
98

99 100
    tabShown(e) {
      const $target = $(e.target);
101 102
      const action = $target.data('action');

F
Fatih Acet 已提交
103 104 105
      if (action === 'commits') {
        this.loadCommits($target.attr('href'));
        this.expandView();
106
        this.resetViewContainer();
107
      } else if (this.isDiffAction(action)) {
F
Fatih Acet 已提交
108
        this.loadDiff($target.attr('href'));
109
        if (Breakpoints.get().getBreakpointSize() !== 'lg') {
F
Fatih Acet 已提交
110 111
          this.shrinkView();
        }
112 113 114
        if (this.diffViewType() === 'parallel') {
          this.expandViewContainer();
        }
115
        const navBarHeight = $('.navbar-gitlab').outerHeight();
116 117
        $.scrollTo('.merge-request-details .merge-request-tabs', {
          offset: -navBarHeight,
F
Fatih Acet 已提交
118
        });
A
Annabel Dunstone 已提交
119 120 121
      } else if (action === 'pipelines') {
        this.loadPipelines($target.attr('href'));
        this.expandView();
122
        this.resetViewContainer();
F
Fatih Acet 已提交
123 124
      } else {
        this.expandView();
125
        this.resetViewContainer();
F
Fatih Acet 已提交
126
      }
127
      if (this.setUrl) {
128 129
        this.setCurrentAction(action);
      }
130
    }
F
Fatih Acet 已提交
131

132
    scrollToElement(container) {
133
      if (location.hash) {
134 135 136 137 138
        const offset = 0 - (
          $('.navbar-gitlab').outerHeight() +
          $('.layout-nav').outerHeight() +
          $('.js-tabs-affix').outerHeight()
        );
139
        const $el = $(`${container} ${location.hash}:not(.match)`);
140
        if ($el.length) {
141
          $.scrollTo($el[0], { offset });
F
Fatih Acet 已提交
142 143
        }
      }
144
    }
F
Fatih Acet 已提交
145

146
    // Activate a tab based on the current action
147
    activateTab(action) {
148
      const activate = action === 'show' ? 'notes' : action;
149
      // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
150
      $(`.merge-request-tabs a[data-action='${activate}']`).tab('show');
151
    }
F
Fatih Acet 已提交
152

153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
    // 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('notes')
    //   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
173
    setCurrentAction(action) {
174
      this.currentAction = action === 'show' ? 'notes' : action;
175

176 177
      // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs'
      let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
178

179
      // Append the new action if we're on a tab other than 'notes'
180 181
      if (this.currentAction !== 'notes') {
        newState += `/${this.currentAction}`;
F
Fatih Acet 已提交
182
      }
183

184
      // Ensure parameters and hash come along for the ride
185
      newState += location.search + location.hash;
186

187 188 189 190
      // Replace the current history state with the new one without breaking
      // Turbolinks' history.
      //
      // See https://github.com/rails/turbolinks/issues/363
191
      window.history.replaceState({
192
        turbolinks: true,
193 194
        url: newState,
      }, document.title, newState);
195

196
      return newState;
197
    }
F
Fatih Acet 已提交
198

199
    loadCommits(source) {
F
Fatih Acet 已提交
200 201 202
      if (this.commitsLoaded) {
        return;
      }
203
      this.ajaxGet({
204
        url: `${source}.json`,
205
        success: (data) => {
206
          document.querySelector('div#commits').innerHTML = data.html;
207 208
          gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
          this.commitsLoaded = true;
209 210
          this.scrollToElement('#commits');
        },
F
Fatih Acet 已提交
211
      });
212
    }
F
Fatih Acet 已提交
213

214
    loadDiff(source) {
F
Fatih Acet 已提交
215 216 217
      if (this.diffsLoaded) {
        return;
      }
218 219 220

      // We extract pathname for the current Changes tab anchor href
      // some pages like MergeRequestsController#new has query parameters on that anchor
S
Steffen Rauh 已提交
221
      const urlPathname = gl.utils.parseUrlPathname(source);
222

223
      this.ajaxGet({
224
        url: `${urlPathname}.json${location.search}`,
225 226 227 228 229 230 231 232 233 234
        success: (data) => {
          $('#diffs').html(data.html);

          if (typeof gl.diffNotesCompileComponents !== 'undefined') {
            gl.diffNotesCompileComponents();
          }

          gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
          $('#diffs .js-syntax-highlight').syntaxHighlight();

235
          if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
236 237 238 239
            this.expandViewContainer();
          }
          this.diffsLoaded = true;

240 241
          new gl.Diff();
          this.scrollToElement('#diffs');
242
        },
F
Fatih Acet 已提交
243
      });
244
    }
F
Fatih Acet 已提交
245

246
    loadPipelines(source) {
A
Annabel Dunstone 已提交
247 248 249
      if (this.pipelinesLoaded) {
        return;
      }
250
      this.ajaxGet({
251
        url: `${source}.json`,
252
        success: (data) => {
A
Annabel Dunstone 已提交
253 254 255
          $('#pipelines').html(data.html);
          gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
          this.pipelinesLoaded = true;
256
          this.scrollToElement('#pipelines');
257 258 259 260

          new gl.MiniPipelineGraph({
            container: '.js-pipeline-table',
          });
261
        },
A
Annabel Dunstone 已提交
262
      });
263
    }
A
Annabel Dunstone 已提交
264

265 266 267
    // Show or hide the loading spinner
    //
    // status - Boolean, true to show, false to hide
268 269 270
    toggleLoading(status) {
      $('.mr-loading-status .loading').toggle(status);
    }
F
Fatih Acet 已提交
271

272
    ajaxGet(options) {
273
      const defaults = {
274
        beforeSend: () => this.toggleLoading(true),
275
        error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
276
        complete: () => this.toggleLoading(false),
F
Fatih Acet 已提交
277
        dataType: 'json',
278
        type: 'GET',
F
Fatih Acet 已提交
279
      };
280
      $.ajax($.extend({}, defaults, options));
281
    }
F
Fatih Acet 已提交
282

283
    diffViewType() {
F
Fatih Acet 已提交
284
      return $('.inline-parallel-buttons a.active').data('view-type');
285
    }
F
Fatih Acet 已提交
286

287
    isDiffAction(action) {
288
      return action === 'diffs' || action === 'new/diffs';
289
    }
290

291
    expandViewContainer() {
292
      const $wrapper = $('.content-wrapper .container-fluid');
293 294 295 296
      if (this.fixedLayoutPref === null) {
        this.fixedLayoutPref = $wrapper.hasClass('container-limited');
      }
      $wrapper.removeClass('container-limited');
297
    }
298

299
    resetViewContainer() {
300 301 302 303
      if (this.fixedLayoutPref !== null) {
        $('.content-wrapper .container-fluid')
          .toggleClass('container-limited', this.fixedLayoutPref);
      }
304
    }
F
Fatih Acet 已提交
305

306
    shrinkView() {
307
      const $gutterIcon = $('.js-sidebar-toggle i:visible');
308 309

      // Wait until listeners are set
310
      setTimeout(() => {
311
        // Only when sidebar is expanded
F
Fatih Acet 已提交
312
        if ($gutterIcon.is('.fa-angle-double-right')) {
313
          $gutterIcon.closest('a').trigger('click', [true]);
F
Fatih Acet 已提交
314 315
        }
      }, 0);
316
    }
F
Fatih Acet 已提交
317

318 319
    // Expand the issuable sidebar unless the user explicitly collapsed it
    expandView() {
320
      if (Cookies.get('collapsed_gutter') === 'true') {
F
Fatih Acet 已提交
321 322
        return;
      }
323
      const $gutterIcon = $('.js-sidebar-toggle i:visible');
324 325

      // Wait until listeners are set
326
      setTimeout(() => {
327
        // Only when sidebar is collapsed
F
Fatih Acet 已提交
328
        if ($gutterIcon.is('.fa-angle-double-left')) {
329
          $gutterIcon.closest('a').trigger('click', [true]);
F
Fatih Acet 已提交
330 331
        }
      }, 0);
332
    }
F
Fatih Acet 已提交
333

334
    initAffix() {
335
      const $tabs = $('.js-tabs-affix');
P
Phil Hughes 已提交
336

337 338
      // Screen space on small screens is usually very sparse
      // So we dont affix the tabs on these
P
Phil Hughes 已提交
339
      if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
340

341 342 343
      const $diffTabs = $('#diff-notes-app');
      const $fixedNav = $('.navbar-fixed-top');
      const $layoutNav = $('.layout-nav');
344 345

      $tabs.off('affix.bs.affix affix-top.bs.affix')
346 347 348 349 350 351 352
        .affix({
          offset: {
            top: () => (
              $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height()
            ),
          },
        })
353 354
        .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
        .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' }));
355 356 357 358 359

      // Fix bug when reloading the page already scrolling
      if ($tabs.hasClass('affix')) {
        $tabs.trigger('affix.bs.affix');
      }
360 361
    }
  }
F
Fatih Acet 已提交
362

363 364 365
  window.gl = window.gl || {};
  window.gl.MergeRequestTabs = MergeRequestTabs;
})();