Merge remote-tracking branch 'upstream/master' into artifacts-from-ref-and-build-name
* upstream/master: (359 commits) Add new image to show the 'Reset template' button Refactor description templates documentation Remove index from pipeline toggles Hide `Create new list button` on Issues and MRs pages Remove params from build; general refactor Style build container box; add check mark to active build Display jobs as scrolling list in sidebar Move stages and jobs to build sidebar Removed vue assets Move skipped tests to end of array Remove unused data attributes Update changelog Style build dropdowns Populate dropdowns with current build on pageload Selecting stage updates builds dropdown Add data attributes to builds Change active state of list items; style dropdown items Hide dropdown if all tests fit on one line; add counter to dropdown Add overflow tests to dropdown Order by build status ...
This commit is contained in:
		
						commit
						62127dc95a
					
				
							
								
								
									
										11
									
								
								CHANGELOG
								
								
								
								
							
							
						
						
									
										11
									
								
								CHANGELOG
								
								
								
								
							| 
						 | 
				
			
			@ -5,10 +5,12 @@ v 8.11.0 (unreleased)
 | 
			
		|||
  - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
 | 
			
		||||
  - Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
 | 
			
		||||
  - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
 | 
			
		||||
  - Add delimiter to project stars and forks count (ClemMakesApps)
 | 
			
		||||
  - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
 | 
			
		||||
  - Fix the title of the toggle dropdown button. !5515 (herminiotorres)
 | 
			
		||||
  - Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
 | 
			
		||||
  - Update to Ruby 2.3.1. !4948
 | 
			
		||||
  - Add Issues Board !5548
 | 
			
		||||
  - Improve diff performance by eliminating redundant checks for text blobs
 | 
			
		||||
  - Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
 | 
			
		||||
  - Convert switch icon into icon font (ClemMakesApps)
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +18,7 @@ v 8.11.0 (unreleased)
 | 
			
		|||
  - API: List access requests, request access, approve, and deny access requests to a project or a group. !4833
 | 
			
		||||
  - Use long options for curl examples in documentation !5703 (winniehell)
 | 
			
		||||
  - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
 | 
			
		||||
  - GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository
 | 
			
		||||
  - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
 | 
			
		||||
  - Ignore URLs starting with // in Markdown links !5677 (winniehell)
 | 
			
		||||
  - Fix CI status icon link underline (ClemMakesApps)
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +35,7 @@ v 8.11.0 (unreleased)
 | 
			
		|||
  - Fix awardable button mutuality loading spinners (ClemMakesApps)
 | 
			
		||||
  - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
 | 
			
		||||
  - Optimize maximum user access level lookup in loading of notes
 | 
			
		||||
  - Send notification emails to users newly mentioned in issue and MR edits !5800
 | 
			
		||||
  - Add "No one can push" as an option for protected branches. !5081
 | 
			
		||||
  - Improve performance of AutolinkFilter#text_parse by using XPath
 | 
			
		||||
  - Add experimental Redis Sentinel support !1877
 | 
			
		||||
| 
						 | 
				
			
			@ -43,6 +47,7 @@ v 8.11.0 (unreleased)
 | 
			
		|||
  - Remove unused images (ClemMakesApps)
 | 
			
		||||
  - Get issue and merge request description templates from repositories
 | 
			
		||||
  - Add hover state to todos !5361 (winniehell)
 | 
			
		||||
  - Fix icon alignment of star and fork buttons !5451 (winniehell)
 | 
			
		||||
  - Limit git rev-list output count to one in forced push check
 | 
			
		||||
  - Show deployment status on merge requests with external URLs
 | 
			
		||||
  - Clean up unused routes (Josef Strzibny)
 | 
			
		||||
| 
						 | 
				
			
			@ -91,14 +96,18 @@ v 8.11.0 (unreleased)
 | 
			
		|||
  - Allow branch names ending with .json for graph and network page !5579 (winniehell)
 | 
			
		||||
  - Add the `sprockets-es6` gem
 | 
			
		||||
  - Improve OAuth2 client documentation (muteor)
 | 
			
		||||
  - Fix diff comments inverted toggle bug (ClemMakesApps)
 | 
			
		||||
  - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska)
 | 
			
		||||
  - Profile requests when a header is passed
 | 
			
		||||
  - Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab.
 | 
			
		||||
  - Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible
 | 
			
		||||
  - Add commit stats in commit api. !5517 (dixpac)
 | 
			
		||||
  - Add CI configuration button on project page
 | 
			
		||||
  - Fix merge request new view not changing code view rendering style
 | 
			
		||||
  - Make error pages responsive (Takuya Noguchi)
 | 
			
		||||
  - The performance of the project dropdown used for moving issues has been improved
 | 
			
		||||
  - Fix skip_repo parameter being ignored when destroying a namespace
 | 
			
		||||
  - Add all builds into stage/job dropdowns on builds page
 | 
			
		||||
  - Change requests_profiles resource constraint to catch virtually any file
 | 
			
		||||
  - Bump gitlab_git to lazy load compare commits
 | 
			
		||||
  - Reduce number of queries made for merge_requests/:id/diffs
 | 
			
		||||
| 
						 | 
				
			
			@ -111,6 +120,7 @@ v 8.11.0 (unreleased)
 | 
			
		|||
  - Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker
 | 
			
		||||
  - Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko)
 | 
			
		||||
  - Adds support for pending invitation project members importing projects
 | 
			
		||||
  - Add pipeline visualization/graph on pipeline page
 | 
			
		||||
  - Update devise initializer to turn on changed password notification emails. !5648 (tombell)
 | 
			
		||||
  - Avoid to show the original password field when password is automatically set. !5712 (duduribeiro)
 | 
			
		||||
  - Fix importing GitLab projects with an invalid MR source project
 | 
			
		||||
| 
						 | 
				
			
			@ -120,6 +130,7 @@ v 8.11.0 (unreleased)
 | 
			
		|||
  - Fix a memory leak caused by Banzai::Filter::SanitizationFilter
 | 
			
		||||
  - Speed up todos queries by limiting the projects set we join with
 | 
			
		||||
  - Ensure file editing in UI does not overwrite commited changes without warning user
 | 
			
		||||
  - Eliminate unneeded calls to Repository#blob_at when listing commits with no path
 | 
			
		||||
 | 
			
		||||
v 8.10.6
 | 
			
		||||
  - Upgrade Rails to 4.2.7.1 for security fixes. !5781
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										1
									
								
								Gemfile
								
								
								
								
							| 
						 | 
				
			
			@ -314,6 +314,7 @@ end
 | 
			
		|||
group :test do
 | 
			
		||||
  gem 'shoulda-matchers', '~> 2.8.0', require: false
 | 
			
		||||
  gem 'email_spec', '~> 1.6.0'
 | 
			
		||||
  gem 'json-schema', '~> 2.6.2'
 | 
			
		||||
  gem 'webmock', '~> 1.21.0'
 | 
			
		||||
  gem 'test_after_commit', '~> 0.4.2'
 | 
			
		||||
  gem 'sham_rack', '~> 1.3.6'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -356,6 +356,8 @@ GEM
 | 
			
		|||
    jquery-ui-rails (5.0.5)
 | 
			
		||||
      railties (>= 3.2.16)
 | 
			
		||||
    json (1.8.3)
 | 
			
		||||
    json-schema (2.6.2)
 | 
			
		||||
      addressable (~> 2.3.8)
 | 
			
		||||
    jwt (1.5.4)
 | 
			
		||||
    kaminari (0.17.0)
 | 
			
		||||
      actionpack (>= 3.0.0)
 | 
			
		||||
| 
						 | 
				
			
			@ -873,6 +875,7 @@ DEPENDENCIES
 | 
			
		|||
  jquery-rails (~> 4.1.0)
 | 
			
		||||
  jquery-turbolinks (~> 2.1.0)
 | 
			
		||||
  jquery-ui-rails (~> 5.0.0)
 | 
			
		||||
  json-schema (~> 2.6.2)
 | 
			
		||||
  jwt
 | 
			
		||||
  kaminari (~> 0.17.0)
 | 
			
		||||
  knapsack (~> 1.11.0)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -224,8 +224,11 @@
 | 
			
		|||
      return $('.navbar-toggle').toggleClass('active');
 | 
			
		||||
    });
 | 
			
		||||
    $body.on("click", ".js-toggle-diff-comments", function(e) {
 | 
			
		||||
      $(this).toggleClass('active');
 | 
			
		||||
      $(this).closest(".diff-file").find(".notes_holder").toggle();
 | 
			
		||||
      var $this = $(this);
 | 
			
		||||
      var showComments = $this.hasClass('active');
 | 
			
		||||
 | 
			
		||||
      $this.toggleClass('active');
 | 
			
		||||
      $this.closest(".diff-file").find(".notes_holder").toggle(showComments);
 | 
			
		||||
      return e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
    $document.off("click", '.js-confirm-danger');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
//= require vue
 | 
			
		||||
//= require vue-resource
 | 
			
		||||
//= require Sortable
 | 
			
		||||
//= require_tree ./models
 | 
			
		||||
//= require_tree ./stores
 | 
			
		||||
//= require_tree ./services
 | 
			
		||||
//= require_tree ./mixins
 | 
			
		||||
//= require ./components/board
 | 
			
		||||
//= require ./components/new_list_dropdown
 | 
			
		||||
//= require ./vue_resource_interceptor
 | 
			
		||||
 | 
			
		||||
$(() => {
 | 
			
		||||
  const $boardApp = document.getElementById('board-app'),
 | 
			
		||||
        Store = gl.issueBoards.BoardsStore;
 | 
			
		||||
 | 
			
		||||
  window.gl = window.gl || {};
 | 
			
		||||
 | 
			
		||||
  if (gl.IssueBoardsApp) {
 | 
			
		||||
    gl.IssueBoardsApp.$destroy(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  gl.IssueBoardsApp = new Vue({
 | 
			
		||||
    el: $boardApp,
 | 
			
		||||
    components: {
 | 
			
		||||
      'board': gl.issueBoards.Board
 | 
			
		||||
    },
 | 
			
		||||
    data: {
 | 
			
		||||
      state: Store.state,
 | 
			
		||||
      loading: true,
 | 
			
		||||
      endpoint: $boardApp.dataset.endpoint,
 | 
			
		||||
      disabled: $boardApp.dataset.disabled === 'true',
 | 
			
		||||
      issueLinkBase: $boardApp.dataset.issueLinkBase
 | 
			
		||||
    },
 | 
			
		||||
    init: Store.create.bind(Store),
 | 
			
		||||
    created () {
 | 
			
		||||
      gl.boardService = new BoardService(this.endpoint);
 | 
			
		||||
    },
 | 
			
		||||
    ready () {
 | 
			
		||||
      Store.disabled = this.disabled;
 | 
			
		||||
      gl.boardService.all()
 | 
			
		||||
        .then((resp) => {          
 | 
			
		||||
          resp.json().forEach((board) => {
 | 
			
		||||
            const list = Store.addList(board);
 | 
			
		||||
 | 
			
		||||
            if (list.type === 'done') {
 | 
			
		||||
              list.position = Infinity;
 | 
			
		||||
            } else if (list.type === 'backlog') {
 | 
			
		||||
              list.position = -1;
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          Store.addBlankState();
 | 
			
		||||
          this.loading = false;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,85 @@
 | 
			
		|||
//= require ./board_blank_state
 | 
			
		||||
//= require ./board_delete
 | 
			
		||||
//= require ./board_list
 | 
			
		||||
 | 
			
		||||
(() => {
 | 
			
		||||
  const Store = gl.issueBoards.BoardsStore;
 | 
			
		||||
 | 
			
		||||
  window.gl = window.gl || {};
 | 
			
		||||
  window.gl.issueBoards = window.gl.issueBoards || {};
 | 
			
		||||
 | 
			
		||||
  gl.issueBoards.Board = Vue.extend({
 | 
			
		||||
    components: {
 | 
			
		||||
      'board-list': gl.issueBoards.BoardList,
 | 
			
		||||
      'board-delete': gl.issueBoards.BoardDelete,
 | 
			
		||||
      'board-blank-state': gl.issueBoards.BoardBlankState
 | 
			
		||||
    },
 | 
			
		||||
    props: {
 | 
			
		||||
      list: Object,
 | 
			
		||||
      disabled: Boolean,
 | 
			
		||||
      issueLinkBase: String
 | 
			
		||||
    },
 | 
			
		||||
    data () {
 | 
			
		||||
      return {
 | 
			
		||||
        query: '',
 | 
			
		||||
        filters: Store.state.filters
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
      query () {
 | 
			
		||||
        this.list.filters = this.getFilterData();
 | 
			
		||||
        this.list.getIssues(true);
 | 
			
		||||
      },
 | 
			
		||||
      filters: {
 | 
			
		||||
        handler () {
 | 
			
		||||
          this.list.page = 1;
 | 
			
		||||
          this.list.getIssues(true);
 | 
			
		||||
        },
 | 
			
		||||
        deep: true
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
      getFilterData () {
 | 
			
		||||
        const filters = this.filters;
 | 
			
		||||
        let queryData = { search: this.query };
 | 
			
		||||
 | 
			
		||||
        Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; });
 | 
			
		||||
 | 
			
		||||
        return queryData;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    ready () {
 | 
			
		||||
      const options = gl.issueBoards.getBoardSortableDefaultOptions({
 | 
			
		||||
        disabled: this.disabled,
 | 
			
		||||
        group: 'boards',
 | 
			
		||||
        draggable: '.is-draggable',
 | 
			
		||||
        handle: '.js-board-handle',
 | 
			
		||||
        onEnd: (e) => {
 | 
			
		||||
          document.body.classList.remove('is-dragging');
 | 
			
		||||
 | 
			
		||||
          if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
 | 
			
		||||
            const order = this.sortable.toArray(),
 | 
			
		||||
                  $board = this.$parent.$refs.board[e.oldIndex + 1],
 | 
			
		||||
                  list = $board.list;
 | 
			
		||||
 | 
			
		||||
            $board.$destroy(true);
 | 
			
		||||
 | 
			
		||||
            this.$nextTick(() => {
 | 
			
		||||
              Store.state.lists.splice(e.newIndex, 0, list);
 | 
			
		||||
              Store.moveList(list, order);
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (bp.getBreakpointSize() === 'xs') {
 | 
			
		||||
        options.handle = '.js-board-drag-handle';
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.sortable = Sortable.create(this.$el.parentNode, options);
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy () {
 | 
			
		||||
      Store.state.lists.$remove(this.list);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,49 @@
 | 
			
		|||
(() => {
 | 
			
		||||
  const Store = gl.issueBoards.BoardsStore;
 | 
			
		||||
 | 
			
		||||
  window.gl = window.gl || {};
 | 
			
		||||
  window.gl.issueBoards = window.gl.issueBoards || {};
 | 
			
		||||
 | 
			
		||||
  gl.issueBoards.BoardBlankState = Vue.extend({
 | 
			
		||||
    data () {
 | 
			
		||||
      return {
 | 
			
		||||
        predefinedLabels: [
 | 
			
		||||
          new ListLabel({ title: 'Development', color: '#5CB85C' }),
 | 
			
		||||
          new ListLabel({ title: 'Testing', color: '#F0AD4E' }),
 | 
			
		||||
          new ListLabel({ title: 'Production', color: '#FF5F00' }),
 | 
			
		||||
          new ListLabel({ title: 'Ready', color: '#FF0000' })
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
      addDefaultLists () {
 | 
			
		||||
        this.clearBlankState();
 | 
			
		||||
 | 
			
		||||
        this.predefinedLabels.forEach((label, i) => {
 | 
			
		||||
          Store.addList({
 | 
			
		||||
            title: label.title,
 | 
			
		||||
            position: i,
 | 
			
		||||
            list_type: 'label',
 | 
			
		||||
            label: {
 | 
			
		||||
              title: label.title,
 | 
			
		||||
              color: label.color
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Save the labels
 | 
			
		||||
        gl.boardService.generateDefaultLists()
 | 
			
		||||
          .then((resp) => {
 | 
			
		||||
            resp.json().forEach((listObj) => {
 | 
			
		||||
              const list = Store.findList('title', listObj.title);
 | 
			
		||||
 | 
			
		||||
              list.id = listObj.id;
 | 
			
		||||
              list.label.id = listObj.label.id;
 | 
			
		||||
              list.getIssues();
 | 
			
		||||
            });
 | 
			
		||||
          });
 | 
			
		||||
      },
 | 
			
		||||
      clearBlankState: Store.removeBlankState.bind(Store)
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
(() => {
 | 
			
		||||
  const Store = gl.issueBoards.BoardsStore;
 | 
			
		||||
 | 
			
		||||
  window.gl = window.gl || {};
 | 
			
		||||
  window.gl.issueBoards = window.gl.issueBoards || {};
 | 
			
		||||
 | 
			
		||||
  gl.issueBoards.BoardCard = Vue.extend({
 | 
			
		||||
    props: {
 | 
			
		||||
      list: Object,
 | 
			
		||||
      issue: Object,
 | 
			
		||||
      issueLinkBase: String,
 | 
			
		||||
      disabled: Boolean,
 | 
			
		||||
      index: Number
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
      filterByLabel (label, e) {
 | 
			
		||||
        let labelToggleText = label.title;
 | 
			
		||||
        const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
 | 
			
		||||
        $(e.target).tooltip('hide');
 | 
			
		||||
 | 
			
		||||
        if (labelIndex === -1) {
 | 
			
		||||
          Store.state.filters['label_name'].push(label.title);
 | 
			
		||||
          $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
 | 
			
		||||
        } else {
 | 
			
		||||
          Store.state.filters['label_name'].splice(labelIndex, 1);
 | 
			
		||||
          labelToggleText = Store.state.filters['label_name'][0];
 | 
			
		||||
          $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const selectedLabels = Store.state.filters['label_name'];
 | 
			
		||||
        if (selectedLabels.length === 0) {
 | 
			
		||||
          labelToggleText = 'Label';
 | 
			
		||||
        } else if (selectedLabels.length > 1) {
 | 
			
		||||
          labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
 | 
			
		||||
 | 
			
		||||
        Store.updateFiltersUrl();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
(() => {
 | 
			
		||||
  window.gl = window.gl || {};
 | 
			
		||||
  window.gl.issueBoards = window.gl.issueBoards || {};
 | 
			
		||||
 | 
			
		||||
  gl.issueBoards.BoardDelete = Vue.extend({
 | 
			
		||||
    props: {
 | 
			
		||||
      list: Object
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
      deleteBoard () {
 | 
			
		||||
        $(this.$el).tooltip('hide');
 | 
			
		||||
 | 
			
		||||
        if (confirm('Are you sure you want to delete this list?')) {
 | 
			
		||||
          this.list.destroy();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,89 @@
 | 
			
		|||
//= require ./board_card
 | 
			
		||||
 | 
			
		||||
(() => {
 | 
			
		||||
  const Store = gl.issueBoards.BoardsStore;
 | 
			
		||||
 | 
			
		||||
  window.gl = window.gl || {};
 | 
			
		||||
  window.gl.issueBoards = window.gl.issueBoards || {};
 | 
			
		||||
 | 
			
		||||
  gl.issueBoards.BoardList = Vue.extend({
 | 
			
		||||
    components: {
 | 
			
		||||
      'board-card': gl.issueBoards.BoardCard
 | 
			
		||||
    },
 | 
			
		||||
    props: {
 | 
			
		||||
      disabled: Boolean,
 | 
			
		||||
      list: Object,
 | 
			
		||||
      issues: Array,
 | 
			
		||||
      loading: Boolean,
 | 
			
		||||
      issueLinkBase: String
 | 
			
		||||
    },
 | 
			
		||||
    data () {
 | 
			
		||||
      return {
 | 
			
		||||
        scrollOffset: 250,
 | 
			
		||||
        filters: Store.state.filters
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
      filters: {
 | 
			
		||||
        handler () {
 | 
			
		||||
          this.list.loadingMore = false;
 | 
			
		||||
          this.$els.list.scrollTop = 0;
 | 
			
		||||
        },
 | 
			
		||||
        deep: true
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
      listHeight () {
 | 
			
		||||
        return this.$els.list.getBoundingClientRect().height;
 | 
			
		||||
      },
 | 
			
		||||
      scrollHeight () {
 | 
			
		||||
        return this.$els.list.scrollHeight;
 | 
			
		||||
      },
 | 
			
		||||
      scrollTop () {
 | 
			
		||||
        return this.$els.list.scrollTop + this.listHeight();
 | 
			
		||||
      },
 | 
			
		||||
      loadNextPage () {
 | 
			
		||||
        const getIssues = this.list.nextPage();
 | 
			
		||||
 | 
			
		||||
        if (getIssues) {
 | 
			
		||||
          this.list.loadingMore = true;
 | 
			
		||||
          getIssues.then(() => {
 | 
			
		||||
            this.list.loadingMore = false;
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    ready () {
 | 
			
		||||
      const options = gl.issueBoards.getBoardSortableDefaultOptions({
 | 
			
		||||
        group: 'issues',
 | 
			
		||||
        sort: false,
 | 
			
		||||
        disabled: this.disabled,
 | 
			
		||||
        onStart: (e) => {
 | 
			
		||||
          const card = this.$refs.issue[e.oldIndex];
 | 
			
		||||
 | 
			
		||||
          Store.moving.issue = card.issue;
 | 
			
		||||
          Store.moving.list = card.list;
 | 
			
		||||
        },
 | 
			
		||||
        onAdd: (e) => {
 | 
			
		||||
          gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
 | 
			
		||||
        },
 | 
			
		||||
        onRemove: (e) => {
 | 
			
		||||
          this.$refs.issue[e.oldIndex].$destroy(true);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (bp.getBreakpointSize() === 'xs') {
 | 
			
		||||
        options.handle = '.js-card-drag-handle';
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.sortable = Sortable.create(this.$els.list, options);
 | 
			
		||||
 | 
			
		||||
      // Scroll event on list to load more
 | 
			
		||||
      this.$els.list.onscroll = () => {
 | 
			
		||||
        if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
 | 
			
		||||
          this.loadNextPage();
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
$(() => {
 | 
			
		||||
  const Store = gl.issueBoards.BoardsStore;
 | 
			
		||||
 | 
			
		||||
  $('.js-new-board-list').each(function () {
 | 
			
		||||
    const $this = $(this);
 | 
			
		||||
 | 
			
		||||
    new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
 | 
			
		||||
 | 
			
		||||
    $this.glDropdown({
 | 
			
		||||
      data(term, callback) {
 | 
			
		||||
        $.get($this.attr('data-labels'))
 | 
			
		||||
          .then((resp) => {
 | 
			
		||||
            callback(resp);
 | 
			
		||||
          });
 | 
			
		||||
      },
 | 
			
		||||
      renderRow (label) {
 | 
			
		||||
        const active = Store.findList('title', label.title),
 | 
			
		||||
              $li = $('<li />'),
 | 
			
		||||
              $a = $('<a />', {
 | 
			
		||||
                class: (active ? `is-active js-board-list-${active.id}` : ''),
 | 
			
		||||
                text: label.title,
 | 
			
		||||
                href: '#'
 | 
			
		||||
              }),
 | 
			
		||||
              $labelColor = $('<span />', {
 | 
			
		||||
                class: 'dropdown-label-box',
 | 
			
		||||
                style: `background-color: ${label.color}`
 | 
			
		||||
              });
 | 
			
		||||
 | 
			
		||||
        return $li.append($a.prepend($labelColor));
 | 
			
		||||
      },
 | 
			
		||||
			search: {
 | 
			
		||||
				fields: ['title']
 | 
			
		||||
			},
 | 
			
		||||
			filterable: true,
 | 
			
		||||
      selectable: true,
 | 
			
		||||
      clicked (label, $el, e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
 | 
			
		||||
        if (!Store.findList('title', label.title)) {
 | 
			
		||||
          Store.new({
 | 
			
		||||
            title: label.title,
 | 
			
		||||
            position: Store.state.lists.length - 2,
 | 
			
		||||
            list_type: 'label',
 | 
			
		||||
            label: {
 | 
			
		||||
              id: label.id,
 | 
			
		||||
              title: label.title,
 | 
			
		||||
              color: label.color
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
((w) => {
 | 
			
		||||
  window.gl = window.gl || {};
 | 
			
		||||
  window.gl.issueBoards = window.gl.issueBoards || {};
 | 
			
		||||
 | 
			
		||||
  gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
 | 
			
		||||
    let defaultSortOptions = {
 | 
			
		||||
      forceFallback: true,
 | 
			
		||||
      fallbackClass: 'is-dragging',
 | 
			
		||||
      fallbackOnBody: true,
 | 
			
		||||
      ghostClass: 'is-ghost',
 | 
			
		||||
      filter: '.has-tooltip',
 | 
			
		||||
      scrollSensitivity: 100,
 | 
			
		||||
      scrollSpeed: 20,
 | 
			
		||||
      onStart () {
 | 
			
		||||
        document.body.classList.add('is-dragging');
 | 
			
		||||
      },
 | 
			
		||||
      onEnd () {
 | 
			
		||||
        document.body.classList.remove('is-dragging');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
 | 
			
		||||
    return defaultSortOptions;
 | 
			
		||||
  };
 | 
			
		||||
})(window);
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
class ListIssue {
 | 
			
		||||
  constructor (obj) {
 | 
			
		||||
    this.id = obj.iid;
 | 
			
		||||
    this.title = obj.title;
 | 
			
		||||
    this.confidential = obj.confidential;
 | 
			
		||||
    this.labels = [];
 | 
			
		||||
 | 
			
		||||
    if (obj.assignee) {
 | 
			
		||||
      this.assignee = new ListUser(obj.assignee);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    obj.labels.forEach((label) => {
 | 
			
		||||
      this.labels.push(new ListLabel(label));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.priority = this.labels.reduce((max, label) => {
 | 
			
		||||
      return (label.priority < max) ? label.priority : max;
 | 
			
		||||
    }, Infinity);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addLabel (label) {
 | 
			
		||||
    if (!this.findLabel(label)) {
 | 
			
		||||
      this.labels.push(new ListLabel(label));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  findLabel (findLabel) {
 | 
			
		||||
    return this.labels.filter( label => label.title === findLabel.title )[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeLabel (removeLabel) {
 | 
			
		||||
    if (removeLabel) {
 | 
			
		||||
      this.labels = this.labels.filter( label => removeLabel.title !== label.title );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeLabels (labels) {
 | 
			
		||||
    labels.forEach(this.removeLabel.bind(this));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getLists () {
 | 
			
		||||
    return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
class ListLabel {
 | 
			
		||||
  constructor (obj) {
 | 
			
		||||
    this.id = obj.id;
 | 
			
		||||
    this.title = obj.title;
 | 
			
		||||
    this.color = obj.color;
 | 
			
		||||
    this.description = obj.description;
 | 
			
		||||
    this.priority = (obj.priority !== null) ? obj.priority : Infinity;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,125 @@
 | 
			
		|||
class List {
 | 
			
		||||
  constructor (obj) {
 | 
			
		||||
    this.id = obj.id;
 | 
			
		||||
    this._uid = this.guid();
 | 
			
		||||
    this.position = obj.position;
 | 
			
		||||
    this.title = obj.title;
 | 
			
		||||
    this.type = obj.list_type;
 | 
			
		||||
    this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1;
 | 
			
		||||
    this.filters = gl.issueBoards.BoardsStore.state.filters;
 | 
			
		||||
    this.page = 1;
 | 
			
		||||
    this.loading = true;
 | 
			
		||||
    this.loadingMore = false;
 | 
			
		||||
    this.issues = [];
 | 
			
		||||
 | 
			
		||||
    if (obj.label) {
 | 
			
		||||
      this.label = new ListLabel(obj.label);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.type !== 'blank' && this.id) {
 | 
			
		||||
      this.getIssues();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  guid() {
 | 
			
		||||
    const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
 | 
			
		||||
    return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  save () {
 | 
			
		||||
    return gl.boardService.createList(this.label.id)
 | 
			
		||||
      .then((resp) => {
 | 
			
		||||
        const data = resp.json();
 | 
			
		||||
 | 
			
		||||
        this.id = data.id;
 | 
			
		||||
        this.type = data.list_type;
 | 
			
		||||
        this.position = data.position;
 | 
			
		||||
 | 
			
		||||
        return this.getIssues();
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  destroy () {
 | 
			
		||||
    gl.issueBoards.BoardsStore.state.lists.$remove(this);
 | 
			
		||||
    gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
 | 
			
		||||
 | 
			
		||||
    gl.boardService.destroyList(this.id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  update () {
 | 
			
		||||
    gl.boardService.updateList(this.id, this.position);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  nextPage () {
 | 
			
		||||
    if (Math.floor(this.issues.length / 20) === this.page) {
 | 
			
		||||
      this.page++;
 | 
			
		||||
 | 
			
		||||
      return this.getIssues(false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  canSearch () {
 | 
			
		||||
    return this.type === 'backlog';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getIssues (emptyIssues = true) {
 | 
			
		||||
    const filters = this.filters;
 | 
			
		||||
    let data = { page: this.page };
 | 
			
		||||
 | 
			
		||||
    Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
 | 
			
		||||
 | 
			
		||||
    if (this.label) {
 | 
			
		||||
      data.label_name = data.label_name.filter( label => label !== this.label.title );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (emptyIssues) {
 | 
			
		||||
      this.loading = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return gl.boardService.getIssuesForList(this.id, data)
 | 
			
		||||
      .then((resp) => {
 | 
			
		||||
        const data = resp.json();
 | 
			
		||||
        this.loading = false;
 | 
			
		||||
 | 
			
		||||
        if (emptyIssues) {
 | 
			
		||||
          this.issues = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.createIssues(data);
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createIssues (data) {
 | 
			
		||||
    data.forEach((issueObj) => {
 | 
			
		||||
      this.addIssue(new ListIssue(issueObj));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addIssue (issue, listFrom) {
 | 
			
		||||
    this.issues.push(issue);
 | 
			
		||||
 | 
			
		||||
    if (this.label) {
 | 
			
		||||
      issue.addLabel(this.label);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (listFrom) {
 | 
			
		||||
      gl.boardService.moveIssue(issue.id, listFrom.id, this.id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  findIssue (id) {
 | 
			
		||||
    return this.issues.filter( issue => issue.id === id )[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeIssue (removeIssue) {
 | 
			
		||||
    this.issues = this.issues.filter((issue) => {
 | 
			
		||||
      const matchesRemove = removeIssue.id === issue.id;
 | 
			
		||||
 | 
			
		||||
      if (matchesRemove) {
 | 
			
		||||
        issue.removeLabel(this.label);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return !matchesRemove;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
class ListUser {
 | 
			
		||||
  constructor (user) {
 | 
			
		||||
    this.id = user.id;
 | 
			
		||||
    this.name = user.name;
 | 
			
		||||
    this.username = user.username;
 | 
			
		||||
    this.avatar = user.avatar_url;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
class BoardService {
 | 
			
		||||
  constructor (root) {
 | 
			
		||||
    Vue.http.options.root = root;
 | 
			
		||||
 | 
			
		||||
    this.lists = Vue.resource(`${root}/lists{/id}`, {}, {
 | 
			
		||||
      generate: {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        url: `${root}/lists/generate.json`
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this.issue = Vue.resource(`${root}/issues{/id}`, {});
 | 
			
		||||
    this.issues = Vue.resource(`${root}/lists{/id}/issues`, {});
 | 
			
		||||
 | 
			
		||||
    Vue.http.interceptors.push((request, next) => {
 | 
			
		||||
      request.headers['X-CSRF-Token'] = $.rails.csrfToken();
 | 
			
		||||
      next();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  all () {
 | 
			
		||||
    return this.lists.get();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generateDefaultLists () {
 | 
			
		||||
    return this.lists.generate({});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createList (label_id) {
 | 
			
		||||
    return this.lists.save({}, {
 | 
			
		||||
      list: {
 | 
			
		||||
        label_id
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateList (id, position) {
 | 
			
		||||
    return this.lists.update({ id }, {
 | 
			
		||||
      list: {
 | 
			
		||||
        position
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  destroyList (id) {
 | 
			
		||||
    return this.lists.delete({ id });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getIssuesForList (id, filter = {}) {
 | 
			
		||||
    let data = { id };
 | 
			
		||||
    Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
 | 
			
		||||
 | 
			
		||||
    return this.issues.get(data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  moveIssue (id, from_list_id, to_list_id) {
 | 
			
		||||
    return this.issue.update({ id }, {
 | 
			
		||||
      from_list_id,
 | 
			
		||||
      to_list_id
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,112 @@
 | 
			
		|||
(() => {
 | 
			
		||||
  window.gl = window.gl || {};
 | 
			
		||||
  window.gl.issueBoards = window.gl.issueBoards || {};
 | 
			
		||||
 | 
			
		||||
  gl.issueBoards.BoardsStore = {
 | 
			
		||||
    disabled: false,
 | 
			
		||||
    state: {},
 | 
			
		||||
    moving: {
 | 
			
		||||
      issue: {},
 | 
			
		||||
      list: {}
 | 
			
		||||
    },
 | 
			
		||||
    create () {
 | 
			
		||||
      this.state.lists = [];
 | 
			
		||||
      this.state.filters = {
 | 
			
		||||
        author_id: gl.utils.getParameterValues('author_id')[0],
 | 
			
		||||
        assignee_id: gl.utils.getParameterValues('assignee_id')[0],
 | 
			
		||||
        milestone_title: gl.utils.getParameterValues('milestone_title')[0],
 | 
			
		||||
        label_name: gl.utils.getParameterValues('label_name[]')
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    addList (listObj) {
 | 
			
		||||
      const list = new List(listObj);
 | 
			
		||||
      this.state.lists.push(list);
 | 
			
		||||
 | 
			
		||||
      return list;
 | 
			
		||||
    },
 | 
			
		||||
    new (listObj) {
 | 
			
		||||
      const list = this.addList(listObj),
 | 
			
		||||
            backlogList = this.findList('type', 'backlog', 'backlog');
 | 
			
		||||
 | 
			
		||||
      list
 | 
			
		||||
        .save()
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          // Remove any new issues from the backlog
 | 
			
		||||
          // as they will be visible in the new list
 | 
			
		||||
          list.issues.forEach(backlogList.removeIssue.bind(backlogList));
 | 
			
		||||
        });
 | 
			
		||||
      this.removeBlankState();
 | 
			
		||||
    },
 | 
			
		||||
    updateNewListDropdown (listId) {
 | 
			
		||||
      $(`.js-board-list-${listId}`).removeClass('is-active');
 | 
			
		||||
    },
 | 
			
		||||
    shouldAddBlankState () {
 | 
			
		||||
      // Decide whether to add the blank state
 | 
			
		||||
      return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]);
 | 
			
		||||
    },
 | 
			
		||||
    addBlankState () {
 | 
			
		||||
      if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
 | 
			
		||||
 | 
			
		||||
      this.addList({
 | 
			
		||||
        id: 'blank',
 | 
			
		||||
        list_type: 'blank',
 | 
			
		||||
        title: 'Welcome to your Issue Board!',
 | 
			
		||||
        position: 0
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    removeBlankState () {
 | 
			
		||||
      this.removeList('blank');
 | 
			
		||||
 | 
			
		||||
      $.cookie('issue_board_welcome_hidden', 'true', {
 | 
			
		||||
        expires: 365 * 10
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    welcomeIsHidden () {
 | 
			
		||||
      return $.cookie('issue_board_welcome_hidden') === 'true';
 | 
			
		||||
    },
 | 
			
		||||
    removeList (id, type = 'blank') {
 | 
			
		||||
      const list = this.findList('id', id, type);
 | 
			
		||||
 | 
			
		||||
      if (!list) return;
 | 
			
		||||
 | 
			
		||||
      this.state.lists = this.state.lists.filter( list => list.id !== id );
 | 
			
		||||
    },
 | 
			
		||||
    moveList (listFrom, orderLists) {
 | 
			
		||||
      orderLists.forEach((id, i) => {
 | 
			
		||||
        const list = this.findList('id', parseInt(id));
 | 
			
		||||
 | 
			
		||||
        list.position = i;
 | 
			
		||||
      });
 | 
			
		||||
      listFrom.update();
 | 
			
		||||
    },
 | 
			
		||||
    moveIssueToList (listFrom, listTo, issue) {
 | 
			
		||||
      const issueTo = listTo.findIssue(issue.id),
 | 
			
		||||
            issueLists = issue.getLists(),
 | 
			
		||||
            listLabels = issueLists.map( listIssue => listIssue.label );
 | 
			
		||||
 | 
			
		||||
      // Add to new lists issues if it doesn't already exist
 | 
			
		||||
      if (!issueTo) {
 | 
			
		||||
        listTo.addIssue(issue, listFrom);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (listTo.type === 'done' && listFrom.type !== 'backlog') {
 | 
			
		||||
        issueLists.forEach((list) => {
 | 
			
		||||
          list.removeIssue(issue);
 | 
			
		||||
        })
 | 
			
		||||
        issue.removeLabels(listLabels);
 | 
			
		||||
      } else {
 | 
			
		||||
        listFrom.removeIssue(issue);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    findList (key, val, type = 'label') {
 | 
			
		||||
      return this.state.lists.filter((list) => {
 | 
			
		||||
        const byType = type ? list['type'] === type : true;
 | 
			
		||||
 | 
			
		||||
        return list[key] === val && byType;
 | 
			
		||||
      })[0];
 | 
			
		||||
    },
 | 
			
		||||
    updateFiltersUrl () {
 | 
			
		||||
      history.pushState(null, null, `?${$.param(this.state.filters)}`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,119 @@
 | 
			
		|||
(function () {
 | 
			
		||||
	'use strict';
 | 
			
		||||
 | 
			
		||||
	function simulateEvent(el, type, options) {
 | 
			
		||||
		var event;
 | 
			
		||||
		if (!el) return;
 | 
			
		||||
		var ownerDocument = el.ownerDocument;
 | 
			
		||||
 | 
			
		||||
		options = options || {};
 | 
			
		||||
 | 
			
		||||
		if (/^mouse/.test(type)) {
 | 
			
		||||
			event = ownerDocument.createEvent('MouseEvents');
 | 
			
		||||
			event.initMouseEvent(type, true, true, ownerDocument.defaultView,
 | 
			
		||||
				options.button, options.screenX, options.screenY, options.clientX, options.clientY,
 | 
			
		||||
				options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
 | 
			
		||||
		} else {
 | 
			
		||||
			event = ownerDocument.createEvent('CustomEvent');
 | 
			
		||||
 | 
			
		||||
			event.initCustomEvent(type, true, true, ownerDocument.defaultView,
 | 
			
		||||
				options.button, options.screenX, options.screenY, options.clientX, options.clientY,
 | 
			
		||||
				options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
 | 
			
		||||
 | 
			
		||||
			event.dataTransfer = {
 | 
			
		||||
				data: {},
 | 
			
		||||
 | 
			
		||||
				setData: function (type, val) {
 | 
			
		||||
					this.data[type] = val;
 | 
			
		||||
				},
 | 
			
		||||
 | 
			
		||||
				getData: function (type) {
 | 
			
		||||
					return this.data[type];
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (el.dispatchEvent) {
 | 
			
		||||
			el.dispatchEvent(event);
 | 
			
		||||
		} else if (el.fireEvent) {
 | 
			
		||||
			el.fireEvent('on' + type, event);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return event;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function getTraget(target) {
 | 
			
		||||
		var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
 | 
			
		||||
		var children = el.children;
 | 
			
		||||
 | 
			
		||||
		return (
 | 
			
		||||
			children[target.index] ||
 | 
			
		||||
			children[target.index === 'first' ? 0 : -1] ||
 | 
			
		||||
			children[target.index === 'last' ? children.length - 1 : -1]
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function getRect(el) {
 | 
			
		||||
		var rect = el.getBoundingClientRect();
 | 
			
		||||
		var width = rect.right - rect.left;
 | 
			
		||||
		var height = rect.bottom - rect.top;
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			x: rect.left,
 | 
			
		||||
			y: rect.top,
 | 
			
		||||
			cx: rect.left + width / 2,
 | 
			
		||||
			cy: rect.top + height / 2,
 | 
			
		||||
			w: width,
 | 
			
		||||
			h: height,
 | 
			
		||||
			hw: width / 2,
 | 
			
		||||
			wh: height / 2
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function simulateDrag(options, callback) {
 | 
			
		||||
		options.to.el = options.to.el || options.from.el;
 | 
			
		||||
 | 
			
		||||
		var fromEl = getTraget(options.from);
 | 
			
		||||
		var toEl = getTraget(options.to);
 | 
			
		||||
    var scrollable = options.scrollable;
 | 
			
		||||
 | 
			
		||||
		var fromRect = getRect(fromEl);
 | 
			
		||||
		var toRect = getRect(toEl);
 | 
			
		||||
 | 
			
		||||
		var startTime = new Date().getTime();
 | 
			
		||||
		var duration = options.duration || 1000;
 | 
			
		||||
		simulateEvent(fromEl, 'mousedown', {button: 0});
 | 
			
		||||
		options.ontap && options.ontap();
 | 
			
		||||
		window.SIMULATE_DRAG_ACTIVE = 1;
 | 
			
		||||
 | 
			
		||||
		var dragInterval = setInterval(function loop() {
 | 
			
		||||
			var progress = (new Date().getTime() - startTime) / duration;
 | 
			
		||||
			var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
 | 
			
		||||
			var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
 | 
			
		||||
			var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
 | 
			
		||||
 | 
			
		||||
			simulateEvent(overEl, 'mousemove', {
 | 
			
		||||
				clientX: x,
 | 
			
		||||
				clientY: y
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (progress >= 1) {
 | 
			
		||||
				options.ondragend && options.ondragend();
 | 
			
		||||
				simulateEvent(toEl, 'mouseup');
 | 
			
		||||
				clearInterval(dragInterval);
 | 
			
		||||
				window.SIMULATE_DRAG_ACTIVE = 0;
 | 
			
		||||
			}
 | 
			
		||||
		}, 100);
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			target: fromEl,
 | 
			
		||||
			fromList: fromEl.parentNode,
 | 
			
		||||
			toList: toEl.parentNode
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	// Export
 | 
			
		||||
	window.simulateEvent = simulateEvent;
 | 
			
		||||
	window.simulateDrag = simulateDrag;
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
Vue.http.interceptors.push((request, next)  => {
 | 
			
		||||
  Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
 | 
			
		||||
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    Vue.activeResources--;
 | 
			
		||||
  }, 500);
 | 
			
		||||
  next();
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -6,19 +6,26 @@
 | 
			
		|||
 | 
			
		||||
    Build.state = null;
 | 
			
		||||
 | 
			
		||||
    function Build(page_url, build_url, build_status, state1) {
 | 
			
		||||
      this.page_url = page_url;
 | 
			
		||||
      this.build_url = build_url;
 | 
			
		||||
      this.build_status = build_status;
 | 
			
		||||
      this.state = state1;
 | 
			
		||||
    function Build(options) {
 | 
			
		||||
      this.page_url = options.page_url;
 | 
			
		||||
      this.build_url = options.build_url;
 | 
			
		||||
      this.build_status = options.build_status;
 | 
			
		||||
      this.state = options.state1;
 | 
			
		||||
      this.build_stage = options.build_stage;
 | 
			
		||||
      this.hideSidebar = bind(this.hideSidebar, this);
 | 
			
		||||
      this.toggleSidebar = bind(this.toggleSidebar, this);
 | 
			
		||||
      this.updateDropdown = bind(this.updateDropdown, this);
 | 
			
		||||
      clearInterval(Build.interval);
 | 
			
		||||
      this.bp = Breakpoints.get();
 | 
			
		||||
      this.hideSidebar();
 | 
			
		||||
      $('.js-build-sidebar').niceScroll();
 | 
			
		||||
 | 
			
		||||
      this.populateJobs(this.build_stage);
 | 
			
		||||
      this.updateStageDropdownText(this.build_stage);
 | 
			
		||||
      this.hideSidebar();
 | 
			
		||||
 | 
			
		||||
      $(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
 | 
			
		||||
      $(window).off('resize.build').on('resize.build', this.hideSidebar);
 | 
			
		||||
      $(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
 | 
			
		||||
      this.updateArtifactRemoveDate();
 | 
			
		||||
      if ($('#build-trace').length) {
 | 
			
		||||
        this.getInitialBuildTrace();
 | 
			
		||||
| 
						 | 
				
			
			@ -132,6 +139,22 @@
 | 
			
		|||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Build.prototype.populateJobs = function(stage) {
 | 
			
		||||
      $('.build-job').hide();
 | 
			
		||||
      $('.build-job[data-stage="' + stage + '"]').show();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Build.prototype.updateStageDropdownText = function(stage) {
 | 
			
		||||
      $('.stage-selection').text(stage);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Build.prototype.updateDropdown = function(e) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      var stage = e.currentTarget.text;
 | 
			
		||||
      this.updateStageDropdownText(stage);
 | 
			
		||||
      this.populateJobs(stage);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return Build;
 | 
			
		||||
 | 
			
		||||
  })();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,6 +34,7 @@
 | 
			
		|||
 | 
			
		||||
  $(function() {
 | 
			
		||||
    var clipboard;
 | 
			
		||||
 | 
			
		||||
    clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
 | 
			
		||||
    clipboard.on('success', genericSuccess);
 | 
			
		||||
    return clipboard.on('error', genericError);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,126 @@
 | 
			
		|||
(function (w) {
 | 
			
		||||
  class CreateLabelDropdown {
 | 
			
		||||
    constructor ($el, projectId) {
 | 
			
		||||
      this.$el = $el;
 | 
			
		||||
      this.projectId = projectId;
 | 
			
		||||
      this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
 | 
			
		||||
      this.$cancelButton = $('.js-cancel-label-btn', this.$el);
 | 
			
		||||
      this.$newLabelField = $('#new_label_name', this.$el);
 | 
			
		||||
      this.$newColorField = $('#new_label_color', this.$el);
 | 
			
		||||
      this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
 | 
			
		||||
      this.$newLabelError = $('.js-label-error', this.$el);
 | 
			
		||||
      this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
 | 
			
		||||
      this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
 | 
			
		||||
 | 
			
		||||
      this.$newLabelError.hide();
 | 
			
		||||
      this.$newLabelCreateButton.disable();
 | 
			
		||||
 | 
			
		||||
      this.cleanBinding();
 | 
			
		||||
      this.addBinding();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cleanBinding () {
 | 
			
		||||
      this.$colorSuggestions.off('click');
 | 
			
		||||
      this.$newLabelField.off('keyup change');
 | 
			
		||||
      this.$newColorField.off('keyup change');
 | 
			
		||||
      this.$dropdownBack.off('click');
 | 
			
		||||
      this.$cancelButton.off('click');
 | 
			
		||||
      this.$newLabelCreateButton.off('click');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addBinding () {
 | 
			
		||||
      const self = this;
 | 
			
		||||
 | 
			
		||||
      this.$colorSuggestions.on('click', function (e) {
 | 
			
		||||
        const $this = $(this);
 | 
			
		||||
        self.addColorValue(e, $this);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
 | 
			
		||||
      this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
 | 
			
		||||
 | 
			
		||||
      this.$dropdownBack.on('click', this.resetForm.bind(this));
 | 
			
		||||
 | 
			
		||||
      this.$cancelButton.on('click', function(e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        self.resetForm();
 | 
			
		||||
        self.$dropdownBack.trigger('click');
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addColorValue (e, $this) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
      this.$newColorField.val($this.data('color')).trigger('change');
 | 
			
		||||
      this.$colorPreview
 | 
			
		||||
        .css('background-color', $this.data('color'))
 | 
			
		||||
        .parent()
 | 
			
		||||
        .addClass('is-active');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    enableLabelCreateButton () {
 | 
			
		||||
      if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
 | 
			
		||||
        this.$newLabelError.hide();
 | 
			
		||||
        this.$newLabelCreateButton.enable();
 | 
			
		||||
      } else {
 | 
			
		||||
        this.$newLabelCreateButton.disable();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resetForm () {
 | 
			
		||||
      this.$newLabelField
 | 
			
		||||
        .val('')
 | 
			
		||||
        .trigger('change');
 | 
			
		||||
 | 
			
		||||
      this.$newColorField
 | 
			
		||||
        .val('')
 | 
			
		||||
        .trigger('change');
 | 
			
		||||
 | 
			
		||||
      this.$colorPreview
 | 
			
		||||
        .css('background-color', '')
 | 
			
		||||
        .parent()
 | 
			
		||||
        .removeClass('is-active');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    saveLabel (e) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
      Api.newLabel(this.projectId, {
 | 
			
		||||
        name: this.$newLabelField.val(),
 | 
			
		||||
        color: this.$newColorField.val()
 | 
			
		||||
      }, (label) => {
 | 
			
		||||
        this.$newLabelCreateButton.enable();
 | 
			
		||||
 | 
			
		||||
        if (label.message) {
 | 
			
		||||
          let errors;
 | 
			
		||||
 | 
			
		||||
          if (typeof label.message === 'string') {
 | 
			
		||||
            errors = label.message;
 | 
			
		||||
          } else {
 | 
			
		||||
            errors = label.message.map(function (value, key) {
 | 
			
		||||
              return key + " " + value[0];
 | 
			
		||||
            }).join("<br/>");
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.$newLabelError
 | 
			
		||||
            .html(errors)
 | 
			
		||||
            .show();
 | 
			
		||||
        } else {
 | 
			
		||||
          this.$dropdownBack.trigger('click');
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!w.gl) {
 | 
			
		||||
    w.gl = {};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  gl.CreateLabelDropdown = CreateLabelDropdown;
 | 
			
		||||
})(window);
 | 
			
		||||
| 
						 | 
				
			
			@ -88,6 +88,8 @@
 | 
			
		|||
          new ZenMode();
 | 
			
		||||
          new MergedButtons();
 | 
			
		||||
          break;
 | 
			
		||||
        case "projects:merge_requests:conflicts":
 | 
			
		||||
          window.mcui = new MergeConflictResolver()
 | 
			
		||||
        case 'projects:merge_requests:index':
 | 
			
		||||
          shortcut_handler = new ShortcutsNavigation();
 | 
			
		||||
          Issuable.init();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
      var _this;
 | 
			
		||||
      _this = this;
 | 
			
		||||
      $('.js-label-select').each(function(i, dropdown) {
 | 
			
		||||
        var $block, $colorPreview, $dropdown, $form, $loading, $newLabelCreateButton, $newLabelError, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, newColorField, newLabelField, projectId, resetForm, saveLabel, saveLabelData, selectedLabel, showAny, showNo;
 | 
			
		||||
        var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo;
 | 
			
		||||
        $dropdown = $(dropdown);
 | 
			
		||||
        projectId = $dropdown.data('project-id');
 | 
			
		||||
        labelUrl = $dropdown.data('labels');
 | 
			
		||||
| 
						 | 
				
			
			@ -13,8 +13,6 @@
 | 
			
		|||
        if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
 | 
			
		||||
          selectedLabel = selectedLabel.split(',');
 | 
			
		||||
        }
 | 
			
		||||
        newLabelField = $('#new_label_name');
 | 
			
		||||
        newColorField = $('#new_label_color');
 | 
			
		||||
        showNo = $dropdown.data('show-no');
 | 
			
		||||
        showAny = $dropdown.data('show-any');
 | 
			
		||||
        defaultLabel = $dropdown.data('default-label');
 | 
			
		||||
| 
						 | 
				
			
			@ -24,10 +22,6 @@
 | 
			
		|||
        $form = $dropdown.closest('form');
 | 
			
		||||
        $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
 | 
			
		||||
        $value = $block.find('.value');
 | 
			
		||||
        $newLabelError = $('.js-label-error');
 | 
			
		||||
        $colorPreview = $('.js-dropdown-label-color-preview');
 | 
			
		||||
        $newLabelCreateButton = $('.js-new-label-btn');
 | 
			
		||||
        $newLabelError.hide();
 | 
			
		||||
        $loading = $block.find('.block-loading').fadeOut();
 | 
			
		||||
        if (issueUpdateURL != null) {
 | 
			
		||||
          issueURLSplit = issueUpdateURL.split('/');
 | 
			
		||||
| 
						 | 
				
			
			@ -36,62 +30,9 @@
 | 
			
		|||
          labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
 | 
			
		||||
          labelNoneHTMLTemplate = '<span class="no-value">None</span>';
 | 
			
		||||
        }
 | 
			
		||||
        if (newLabelField.length) {
 | 
			
		||||
          $('.suggest-colors-dropdown a').on("click", function(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            newColorField.val($(this).data('color')).trigger('change');
 | 
			
		||||
            return $colorPreview.css('background-color', $(this).data('color')).parent().addClass('is-active');
 | 
			
		||||
          });
 | 
			
		||||
          resetForm = function() {
 | 
			
		||||
            newLabelField.val('').trigger('change');
 | 
			
		||||
            newColorField.val('').trigger('change');
 | 
			
		||||
            return $colorPreview.css('background-color', '').parent().removeClass('is-active');
 | 
			
		||||
          };
 | 
			
		||||
          $('.dropdown-menu-back').on('click', function() {
 | 
			
		||||
            return resetForm();
 | 
			
		||||
          });
 | 
			
		||||
          $('.js-cancel-label-btn').on('click', function(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            resetForm();
 | 
			
		||||
            return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
 | 
			
		||||
          });
 | 
			
		||||
          enableLabelCreateButton = function() {
 | 
			
		||||
            if (newLabelField.val() !== '' && newColorField.val() !== '') {
 | 
			
		||||
              $newLabelError.hide();
 | 
			
		||||
              return $newLabelCreateButton.enable();
 | 
			
		||||
            } else {
 | 
			
		||||
              return $newLabelCreateButton.disable();
 | 
			
		||||
            }
 | 
			
		||||
          };
 | 
			
		||||
          saveLabel = function() {
 | 
			
		||||
            return Api.newLabel(projectId, {
 | 
			
		||||
              name: newLabelField.val(),
 | 
			
		||||
              color: newColorField.val()
 | 
			
		||||
            }, function(label) {
 | 
			
		||||
              $newLabelCreateButton.enable();
 | 
			
		||||
              if (label.message != null) {
 | 
			
		||||
                var errorText = label.message;
 | 
			
		||||
                if (_.isObject(label.message)) {
 | 
			
		||||
                  errorText = _.map(label.message, function(value, key) {
 | 
			
		||||
                    return key + " " + value[0];
 | 
			
		||||
                  }).join('<br/>');
 | 
			
		||||
                }
 | 
			
		||||
                return $newLabelError.html(errorText).show();
 | 
			
		||||
              } else {
 | 
			
		||||
                return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          };
 | 
			
		||||
          newLabelField.on('keyup change', enableLabelCreateButton);
 | 
			
		||||
          newColorField.on('keyup change', enableLabelCreateButton);
 | 
			
		||||
          $newLabelCreateButton.disable().on('click', function(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            return saveLabel();
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
 | 
			
		||||
 | 
			
		||||
        saveLabelData = function() {
 | 
			
		||||
          var data, selected;
 | 
			
		||||
          selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
 | 
			
		||||
| 
						 | 
				
			
			@ -272,6 +213,9 @@
 | 
			
		|||
            isMRIndex = page === 'projects:merge_requests:index';
 | 
			
		||||
            $selectbox.hide();
 | 
			
		||||
            $value.removeAttr('style');
 | 
			
		||||
            if (page === 'projects:boards:show') {
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            if ($dropdown.hasClass('js-multiselect')) {
 | 
			
		||||
              if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
 | 
			
		||||
                selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
 | 
			
		||||
| 
						 | 
				
			
			@ -291,7 +235,7 @@
 | 
			
		|||
            }
 | 
			
		||||
          },
 | 
			
		||||
          multiSelect: $dropdown.hasClass('js-multiselect'),
 | 
			
		||||
          clicked: function(label) {
 | 
			
		||||
          clicked: function(label, $el, e) {
 | 
			
		||||
            var isIssueIndex, isMRIndex, page;
 | 
			
		||||
            _this.enableBulkLabelDropdown();
 | 
			
		||||
            if ($dropdown.hasClass('js-filter-bulk-update')) {
 | 
			
		||||
| 
						 | 
				
			
			@ -300,7 +244,23 @@
 | 
			
		|||
            page = $('body').data('page');
 | 
			
		||||
            isIssueIndex = page === 'projects:issues:index';
 | 
			
		||||
            isMRIndex = page === 'projects:merge_requests:index';
 | 
			
		||||
            if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
 | 
			
		||||
            if (page === 'projects:boards:show') {
 | 
			
		||||
              if (label.isAny) {
 | 
			
		||||
                gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
 | 
			
		||||
              } else if (label.title) {
 | 
			
		||||
                gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
 | 
			
		||||
              } else {
 | 
			
		||||
                var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
 | 
			
		||||
                filters = filters.filter(function (label) {
 | 
			
		||||
                  return label !== $el.text().trim();
 | 
			
		||||
                });
 | 
			
		||||
                gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              gl.issueBoards.BoardsStore.updateFiltersUrl();
 | 
			
		||||
              e.preventDefault();
 | 
			
		||||
              return;
 | 
			
		||||
            } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
 | 
			
		||||
              if (!$dropdown.hasClass('js-multiselect')) {
 | 
			
		||||
                selectedLabel = label.title;
 | 
			
		||||
                return Issuable.filterResults($dropdown.closest('form'));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,341 @@
 | 
			
		|||
const HEAD_HEADER_TEXT    = 'HEAD//our changes';
 | 
			
		||||
const ORIGIN_HEADER_TEXT  = 'origin//their changes';
 | 
			
		||||
const HEAD_BUTTON_TITLE   = 'Use ours';
 | 
			
		||||
const ORIGIN_BUTTON_TITLE = 'Use theirs';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MergeConflictDataProvider {
 | 
			
		||||
 | 
			
		||||
  getInitialData() {
 | 
			
		||||
    const diffViewType = $.cookie('diff_view');
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      isLoading      : true,
 | 
			
		||||
      hasError       : false,
 | 
			
		||||
      isParallel     : diffViewType === 'parallel',
 | 
			
		||||
      diffViewType   : diffViewType,
 | 
			
		||||
      isSubmitting   : false,
 | 
			
		||||
      conflictsData  : {},
 | 
			
		||||
      resolutionData : {}
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  decorateData(vueInstance, data) {
 | 
			
		||||
    this.vueInstance = vueInstance;
 | 
			
		||||
 | 
			
		||||
    if (data.type === 'error') {
 | 
			
		||||
      vueInstance.hasError = true;
 | 
			
		||||
      data.errorMessage = data.message;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      data.shortCommitSha = data.commit_sha.slice(0, 7);
 | 
			
		||||
      data.commitMessage  = data.commit_message;
 | 
			
		||||
 | 
			
		||||
      this.setParallelLines(data);
 | 
			
		||||
      this.setInlineLines(data);
 | 
			
		||||
      this.updateResolutionsData(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    vueInstance.conflictsData = data;
 | 
			
		||||
    vueInstance.isSubmitting = false;
 | 
			
		||||
 | 
			
		||||
    const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict';
 | 
			
		||||
    vueInstance.conflictsData.conflictsText = conflictsText;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  updateResolutionsData(data) {
 | 
			
		||||
    const vi = this.vueInstance;
 | 
			
		||||
 | 
			
		||||
    data.files.forEach( (file) => {
 | 
			
		||||
      file.sections.forEach( (section) => {
 | 
			
		||||
        if (section.conflict) {
 | 
			
		||||
          vi.$set(`resolutionData['${section.id}']`, false);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  setParallelLines(data) {
 | 
			
		||||
    data.files.forEach( (file) => {
 | 
			
		||||
      file.filePath  = this.getFilePath(file);
 | 
			
		||||
      file.iconClass = `fa-${file.blob_icon}`;
 | 
			
		||||
      file.blobPath  = file.blob_path;
 | 
			
		||||
      file.parallelLines = [];
 | 
			
		||||
      const linesObj = { left: [], right: [] };
 | 
			
		||||
 | 
			
		||||
      file.sections.forEach( (section) => {
 | 
			
		||||
        const { conflict, lines, id } = section;
 | 
			
		||||
 | 
			
		||||
        if (conflict) {
 | 
			
		||||
          linesObj.left.push(this.getOriginHeaderLine(id));
 | 
			
		||||
          linesObj.right.push(this.getHeadHeaderLine(id));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        lines.forEach( (line) => {
 | 
			
		||||
          const { type } = line;
 | 
			
		||||
 | 
			
		||||
          if (conflict) {
 | 
			
		||||
            if (type === 'old') {
 | 
			
		||||
              linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
 | 
			
		||||
            }
 | 
			
		||||
            else if (type === 'new') {
 | 
			
		||||
              linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            const lineType = type || 'context';
 | 
			
		||||
 | 
			
		||||
            linesObj.left.push (this.getLineForParallelView(line, id, lineType));
 | 
			
		||||
            linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.checkLineLengths(linesObj);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      for (let i = 0, len = linesObj.left.length; i < len; i++) {
 | 
			
		||||
        file.parallelLines.push([
 | 
			
		||||
          linesObj.right[i],
 | 
			
		||||
          linesObj.left[i]
 | 
			
		||||
        ]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  checkLineLengths(linesObj) {
 | 
			
		||||
    let { left, right } = linesObj;
 | 
			
		||||
 | 
			
		||||
    if (left.length !== right.length) {
 | 
			
		||||
      if (left.length > right.length) {
 | 
			
		||||
        const diff = left.length - right.length;
 | 
			
		||||
        for (let i = 0; i < diff; i++) {
 | 
			
		||||
          right.push({ lineType: 'emptyLine', richText: '' });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        const diff = right.length - left.length;
 | 
			
		||||
        for (let i = 0; i < diff; i++) {
 | 
			
		||||
          left.push({ lineType: 'emptyLine', richText: '' });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  setInlineLines(data) {
 | 
			
		||||
    data.files.forEach( (file) => {
 | 
			
		||||
      file.iconClass   = `fa-${file.blob_icon}`;
 | 
			
		||||
      file.blobPath    = file.blob_path;
 | 
			
		||||
      file.filePath    = this.getFilePath(file);
 | 
			
		||||
      file.inlineLines = []
 | 
			
		||||
 | 
			
		||||
      file.sections.forEach( (section) => {
 | 
			
		||||
        let currentLineType = 'new';
 | 
			
		||||
        const { conflict, lines, id } = section;
 | 
			
		||||
 | 
			
		||||
        if (conflict) {
 | 
			
		||||
          file.inlineLines.push(this.getHeadHeaderLine(id));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        lines.forEach( (line) => {
 | 
			
		||||
          const { type } = line;
 | 
			
		||||
 | 
			
		||||
          if ((type === 'new' || type === 'old') && currentLineType !== type) {
 | 
			
		||||
            currentLineType = type;
 | 
			
		||||
            file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.decorateLineForInlineView(line, id, conflict);
 | 
			
		||||
          file.inlineLines.push(line);
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (conflict) {
 | 
			
		||||
          file.inlineLines.push(this.getOriginHeaderLine(id));
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  handleSelected(sectionId, selection) {
 | 
			
		||||
    const vi = this.vueInstance;
 | 
			
		||||
 | 
			
		||||
    vi.resolutionData[sectionId] = selection;
 | 
			
		||||
    vi.conflictsData.files.forEach( (file) => {
 | 
			
		||||
      file.inlineLines.forEach( (line) => {
 | 
			
		||||
        if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
 | 
			
		||||
          this.markLine(line, selection);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      file.parallelLines.forEach( (lines) => {
 | 
			
		||||
        const left         = lines[0];
 | 
			
		||||
        const right        = lines[1];
 | 
			
		||||
        const hasSameId    = right.id === sectionId || left.id === sectionId;
 | 
			
		||||
        const isLeftMatch  = left.hasConflict || left.isHeader;
 | 
			
		||||
        const isRightMatch = right.hasConflict || right.isHeader;
 | 
			
		||||
 | 
			
		||||
        if (hasSameId && (isLeftMatch || isRightMatch)) {
 | 
			
		||||
          this.markLine(left, selection);
 | 
			
		||||
          this.markLine(right, selection);
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  updateViewType(newType) {
 | 
			
		||||
    const vi = this.vueInstance;
 | 
			
		||||
 | 
			
		||||
    if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    vi.diffView   = newType;
 | 
			
		||||
    vi.isParallel = newType === 'parallel';
 | 
			
		||||
    $.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
 | 
			
		||||
    $('.content-wrapper .container-fluid').toggleClass('container-limited');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  markLine(line, selection) {
 | 
			
		||||
    if (selection === 'head' && line.isHead) {
 | 
			
		||||
      line.isSelected   = true;
 | 
			
		||||
      line.isUnselected = false;
 | 
			
		||||
    }
 | 
			
		||||
    else if (selection === 'origin' && line.isOrigin) {
 | 
			
		||||
      line.isSelected   = true;
 | 
			
		||||
      line.isUnselected = false;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      line.isSelected   = false;
 | 
			
		||||
      line.isUnselected = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  getConflictsCount() {
 | 
			
		||||
    return Object.keys(this.vueInstance.resolutionData).length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  getResolvedCount() {
 | 
			
		||||
    let  count = 0;
 | 
			
		||||
    const data = this.vueInstance.resolutionData;
 | 
			
		||||
 | 
			
		||||
    for (const id in data) {
 | 
			
		||||
      const resolution = data[id];
 | 
			
		||||
      if (resolution) {
 | 
			
		||||
        count++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return count;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  isReadyToCommit() {
 | 
			
		||||
    const { conflictsData, isSubmitting } = this.vueInstance
 | 
			
		||||
    const allResolved = this.getConflictsCount() === this.getResolvedCount();
 | 
			
		||||
    const hasCommitMessage = $.trim(conflictsData.commitMessage).length;
 | 
			
		||||
 | 
			
		||||
    return !isSubmitting && hasCommitMessage && allResolved;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  getCommitButtonText() {
 | 
			
		||||
    const initial = 'Commit conflict resolution';
 | 
			
		||||
    const inProgress = 'Committing...';
 | 
			
		||||
    const vue = this.vueInstance;
 | 
			
		||||
 | 
			
		||||
    return vue ? vue.isSubmitting ? inProgress : initial : initial;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  decorateLineForInlineView(line, id, conflict) {
 | 
			
		||||
    const { type }    = line;
 | 
			
		||||
    line.id           = id;
 | 
			
		||||
    line.hasConflict  = conflict;
 | 
			
		||||
    line.isHead       = type === 'new';
 | 
			
		||||
    line.isOrigin     = type === 'old';
 | 
			
		||||
    line.hasMatch     = type === 'match';
 | 
			
		||||
    line.richText     = line.rich_text;
 | 
			
		||||
    line.isSelected   = false;
 | 
			
		||||
    line.isUnselected = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getLineForParallelView(line, id, lineType, isHead) {
 | 
			
		||||
    const { old_line, new_line, rich_text } = line;
 | 
			
		||||
    const hasConflict = lineType === 'conflict';
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      id,
 | 
			
		||||
      lineType,
 | 
			
		||||
      hasConflict,
 | 
			
		||||
      isHead       : hasConflict && isHead,
 | 
			
		||||
      isOrigin     : hasConflict && !isHead,
 | 
			
		||||
      hasMatch     : lineType === 'match',
 | 
			
		||||
      lineNumber   : isHead ? new_line : old_line,
 | 
			
		||||
      section      : isHead ? 'head' : 'origin',
 | 
			
		||||
      richText     : rich_text,
 | 
			
		||||
      isSelected   : false,
 | 
			
		||||
      isUnselected : false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  getHeadHeaderLine(id) {
 | 
			
		||||
    return {
 | 
			
		||||
      id          : id,
 | 
			
		||||
      richText    : HEAD_HEADER_TEXT,
 | 
			
		||||
      buttonTitle : HEAD_BUTTON_TITLE,
 | 
			
		||||
      type        : 'new',
 | 
			
		||||
      section     : 'head',
 | 
			
		||||
      isHeader    : true,
 | 
			
		||||
      isHead      : true,
 | 
			
		||||
      isSelected  : false,
 | 
			
		||||
      isUnselected: false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  getOriginHeaderLine(id) {
 | 
			
		||||
    return {
 | 
			
		||||
      id          : id,
 | 
			
		||||
      richText    : ORIGIN_HEADER_TEXT,
 | 
			
		||||
      buttonTitle : ORIGIN_BUTTON_TITLE,
 | 
			
		||||
      type        : 'old',
 | 
			
		||||
      section     : 'origin',
 | 
			
		||||
      isHeader    : true,
 | 
			
		||||
      isOrigin    : true,
 | 
			
		||||
      isSelected  : false,
 | 
			
		||||
      isUnselected: false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  handleFailedRequest(vueInstance, data) {
 | 
			
		||||
    vueInstance.hasError = true;
 | 
			
		||||
    vueInstance.conflictsData.errorMessage = 'Something went wrong!';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  getCommitData() {
 | 
			
		||||
    return {
 | 
			
		||||
      commit_message: this.vueInstance.conflictsData.commitMessage,
 | 
			
		||||
      sections: this.vueInstance.resolutionData
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  getFilePath(file) {
 | 
			
		||||
    const { old_path, new_path } = file;
 | 
			
		||||
    return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,85 @@
 | 
			
		|||
//= require vue
 | 
			
		||||
 | 
			
		||||
class MergeConflictResolver {
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.dataProvider = new MergeConflictDataProvider()
 | 
			
		||||
    this.initVue()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  initVue() {
 | 
			
		||||
    const that = this;
 | 
			
		||||
    this.vue   = new Vue({
 | 
			
		||||
      el       : '#conflicts',
 | 
			
		||||
      name     : 'MergeConflictResolver',
 | 
			
		||||
      data     : this.dataProvider.getInitialData(),
 | 
			
		||||
      created  : this.fetchData(),
 | 
			
		||||
      computed : this.setComputedProperties(),
 | 
			
		||||
      methods  : {
 | 
			
		||||
        handleSelected(sectionId, selection) {
 | 
			
		||||
          that.dataProvider.handleSelected(sectionId, selection);
 | 
			
		||||
        },
 | 
			
		||||
        handleViewTypeChange(newType) {
 | 
			
		||||
          that.dataProvider.updateViewType(newType);
 | 
			
		||||
        },
 | 
			
		||||
        commit() {
 | 
			
		||||
          that.commit();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  setComputedProperties() {
 | 
			
		||||
    const dp = this.dataProvider;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      conflictsCount() { return dp.getConflictsCount() },
 | 
			
		||||
      resolvedCount() { return dp.getResolvedCount() },
 | 
			
		||||
      readyToCommit() { return dp.isReadyToCommit() },
 | 
			
		||||
      commitButtonText() { return dp.getCommitButtonText() }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  fetchData() {
 | 
			
		||||
    const dp = this.dataProvider;
 | 
			
		||||
 | 
			
		||||
    $.get($('#conflicts').data('conflictsPath'))
 | 
			
		||||
      .done((data) => {
 | 
			
		||||
        dp.decorateData(this.vue, data);
 | 
			
		||||
      })
 | 
			
		||||
      .error((data) => {
 | 
			
		||||
        dp.handleFailedRequest(this.vue, data);
 | 
			
		||||
      })
 | 
			
		||||
      .always(() => {
 | 
			
		||||
        this.vue.isLoading = false;
 | 
			
		||||
 | 
			
		||||
        this.vue.$nextTick(() => {
 | 
			
		||||
          $('#conflicts .js-syntax-highlight').syntaxHighlight();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (this.vue.diffViewType === 'parallel') {
 | 
			
		||||
          $('.content-wrapper .container-fluid').removeClass('container-limited');
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  commit() {
 | 
			
		||||
    this.vue.isSubmitting = true;
 | 
			
		||||
 | 
			
		||||
    $.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
 | 
			
		||||
      .done((data) => {
 | 
			
		||||
        window.location.href = data.redirect_to;
 | 
			
		||||
      })
 | 
			
		||||
      .error(() => {
 | 
			
		||||
        new Flash('Something went wrong!');
 | 
			
		||||
      })
 | 
			
		||||
      .always(() => {
 | 
			
		||||
        this.vue.isSubmitting = false;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +9,8 @@
 | 
			
		|||
 | 
			
		||||
    MergeRequestTabs.prototype.buildsLoaded = false;
 | 
			
		||||
 | 
			
		||||
    MergeRequestTabs.prototype.pipelinesLoaded = false;
 | 
			
		||||
 | 
			
		||||
    MergeRequestTabs.prototype.commitsLoaded = false;
 | 
			
		||||
 | 
			
		||||
    function MergeRequestTabs(opts) {
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +52,9 @@
 | 
			
		|||
      } else if (action === 'builds') {
 | 
			
		||||
        this.loadBuilds($target.attr('href'));
 | 
			
		||||
        this.expandView();
 | 
			
		||||
      } else if (action === 'pipelines') {
 | 
			
		||||
        this.loadPipelines($target.attr('href'));
 | 
			
		||||
        this.expandView();
 | 
			
		||||
      } else {
 | 
			
		||||
        this.expandView();
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +86,7 @@
 | 
			
		|||
      if (action === 'show') {
 | 
			
		||||
        action = 'notes';
 | 
			
		||||
      }
 | 
			
		||||
      new_state = this._location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '');
 | 
			
		||||
      new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
 | 
			
		||||
      if (action !== 'notes') {
 | 
			
		||||
        new_state += "/" + action;
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -177,6 +182,21 @@
 | 
			
		|||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    MergeRequestTabs.prototype.loadPipelines = function(source) {
 | 
			
		||||
      if (this.pipelinesLoaded) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      return this._get({
 | 
			
		||||
        url: source + ".json",
 | 
			
		||||
        success: function(data) {
 | 
			
		||||
          $('#pipelines').html(data.html);
 | 
			
		||||
          gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
 | 
			
		||||
          this.pipelinesLoaded = true;
 | 
			
		||||
          return this.scrollToElement("#pipelines");
 | 
			
		||||
        }.bind(this)
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    MergeRequestTabs.prototype.toggleLoading = function(status) {
 | 
			
		||||
      return $('.mr-loading-status .loading').toggle(status);
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,7 @@
 | 
			
		|||
 | 
			
		||||
    MergeRequestWidget.prototype.addEventListeners = function() {
 | 
			
		||||
      var allowedPages;
 | 
			
		||||
      allowedPages = ['show', 'commits', 'builds', 'changes'];
 | 
			
		||||
      allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
 | 
			
		||||
      return $(document).on('page:change.merge_request', (function(_this) {
 | 
			
		||||
        return function() {
 | 
			
		||||
          var page;
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +53,7 @@
 | 
			
		|||
          return function(data) {
 | 
			
		||||
            var callback, urlSuffix;
 | 
			
		||||
            if (data.state === "merged") {
 | 
			
		||||
              urlSuffix = deleteSourceBranch ? '?delete_source=true' : '';
 | 
			
		||||
              urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
 | 
			
		||||
              return window.location.href = window.location.pathname + urlSuffix;
 | 
			
		||||
            } else if (data.merge_error) {
 | 
			
		||||
              return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -94,7 +94,7 @@
 | 
			
		|||
            $selectbox.hide();
 | 
			
		||||
            return $value.css('display', '');
 | 
			
		||||
          },
 | 
			
		||||
          clicked: function(selected) {
 | 
			
		||||
          clicked: function(selected, $el, e) {
 | 
			
		||||
            var data, isIssueIndex, isMRIndex, page;
 | 
			
		||||
            page = $('body').data('page');
 | 
			
		||||
            isIssueIndex = page === 'projects:issues:index';
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +102,11 @@
 | 
			
		|||
            if ($dropdown.hasClass('js-filter-bulk-update')) {
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
 | 
			
		||||
            if (page === 'projects:boards:show') {
 | 
			
		||||
              gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
 | 
			
		||||
              gl.issueBoards.BoardsStore.updateFiltersUrl();
 | 
			
		||||
              e.preventDefault();
 | 
			
		||||
            } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
 | 
			
		||||
              if (selected.name != null) {
 | 
			
		||||
                selectedMilestone = selected.name;
 | 
			
		||||
              } else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
(function() {
 | 
			
		||||
  function toggleGraph() {
 | 
			
		||||
    const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
 | 
			
		||||
    const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
 | 
			
		||||
    const $btnText = $(this).find('.toggle-btn-text');
 | 
			
		||||
 | 
			
		||||
    $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
 | 
			
		||||
 | 
			
		||||
    const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
 | 
			
		||||
 | 
			
		||||
    graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $(document).on('click', '.toggle-pipeline-btn', toggleGraph);
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			@ -141,7 +141,7 @@
 | 
			
		|||
              $selectbox.hide();
 | 
			
		||||
              return $value.css('display', '');
 | 
			
		||||
            },
 | 
			
		||||
            clicked: function(user) {
 | 
			
		||||
            clicked: function(user, $el, e) {
 | 
			
		||||
              var isIssueIndex, isMRIndex, page, selected;
 | 
			
		||||
              page = $('body').data('page');
 | 
			
		||||
              isIssueIndex = page === 'projects:issues:index';
 | 
			
		||||
| 
						 | 
				
			
			@ -149,7 +149,12 @@
 | 
			
		|||
              if ($dropdown.hasClass('js-filter-bulk-update')) {
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
              if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
 | 
			
		||||
              if (page === 'projects:boards:show') {
 | 
			
		||||
                selectedId = user.id;
 | 
			
		||||
                gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
 | 
			
		||||
                gl.issueBoards.BoardsStore.updateFiltersUrl();
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
              } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
 | 
			
		||||
                selectedId = user.id;
 | 
			
		||||
                return Issuable.filterResults($dropdown.closest('form'));
 | 
			
		||||
              } else if ($dropdown.hasClass('js-filter-submit')) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,3 +20,8 @@
 | 
			
		|||
    .turn-off { display: block; }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[v-cloak] {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -204,6 +204,10 @@
 | 
			
		|||
    position: relative;
 | 
			
		||||
    top: 2px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  svg, .fa {
 | 
			
		||||
    margin-right: 3px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-lg {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -123,4 +123,9 @@
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin dark-diff-match-line {
 | 
			
		||||
  color: rgba(255, 255, 255, 0.3);
 | 
			
		||||
  background: rgba(255, 255, 255, 0.1);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -222,3 +222,7 @@ header.header-pinned-nav {
 | 
			
		|||
    padding-right: $sidebar_collapsed_width;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.right-sidebar {
 | 
			
		||||
  border-left: 1px solid $border-color;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -276,3 +276,5 @@ $personal-access-tokens-disabled-label-color: #bbb;
 | 
			
		|||
 | 
			
		||||
$ci-output-bg: #1d1f21;
 | 
			
		||||
$ci-text-color: #c5c8c6;
 | 
			
		||||
 | 
			
		||||
$issue-boards-font-size: 15px;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,10 @@
 | 
			
		|||
 | 
			
		||||
  // Diff line
 | 
			
		||||
  .line_holder {
 | 
			
		||||
    &.match .line_content {
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    td.diff-line-num.hll:not(.empty-cell),
 | 
			
		||||
    td.line_content.hll:not(.empty-cell) {
 | 
			
		||||
      background-color: #557;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,8 +40,7 @@
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    .line_content.match {
 | 
			
		||||
      color: rgba(255, 255, 255, 0.3);
 | 
			
		||||
      background: rgba(255, 255, 255, 0.1);
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,10 @@
 | 
			
		|||
 | 
			
		||||
  // Diff line
 | 
			
		||||
  .line_holder {
 | 
			
		||||
    &.match .line_content {
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    td.diff-line-num.hll:not(.empty-cell),
 | 
			
		||||
    td.line_content.hll:not(.empty-cell) {
 | 
			
		||||
      background-color: #49483e;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,8 +40,7 @@
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    .line_content.match {
 | 
			
		||||
      color: rgba(255, 255, 255, 0.3);
 | 
			
		||||
      background: rgba(255, 255, 255, 0.1);
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,10 @@
 | 
			
		|||
 | 
			
		||||
  // Diff line
 | 
			
		||||
  .line_holder {
 | 
			
		||||
    &.match .line_content {
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    td.diff-line-num.hll:not(.empty-cell),
 | 
			
		||||
    td.line_content.hll:not(.empty-cell) {
 | 
			
		||||
      background-color: #174652;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,8 +40,7 @@
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    .line_content.match {
 | 
			
		||||
      color: rgba(255, 255, 255, 0.3);
 | 
			
		||||
      background: rgba(255, 255, 255, 0.1);
 | 
			
		||||
      @include dark-diff-match-line;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,10 @@
 | 
			
		|||
/* https://gist.github.com/qguv/7936275 */
 | 
			
		||||
 | 
			
		||||
@mixin matchLine {
 | 
			
		||||
  color: $black-transparent;
 | 
			
		||||
  background: rgba(255, 255, 255, 0.4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.code.solarized-light {
 | 
			
		||||
  // Line numbers
 | 
			
		||||
  .line-numbers, .diff-line-num {
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +27,10 @@
 | 
			
		|||
 | 
			
		||||
  // Diff line
 | 
			
		||||
  .line_holder {
 | 
			
		||||
    &.match .line_content {
 | 
			
		||||
      @include matchLine;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    td.diff-line-num.hll:not(.empty-cell),
 | 
			
		||||
    td.line_content.hll:not(.empty-cell) {
 | 
			
		||||
      background-color: #ddd8c5;
 | 
			
		||||
| 
						 | 
				
			
			@ -36,8 +46,7 @@
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    .line_content.match {
 | 
			
		||||
      color: $black-transparent;
 | 
			
		||||
      background: rgba(255, 255, 255, 0.4);
 | 
			
		||||
      @include matchLine;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,10 @@
 | 
			
		|||
/* https://github.com/aahan/pygments-github-style */
 | 
			
		||||
 | 
			
		||||
@mixin matchLine {
 | 
			
		||||
  color: $black-transparent;
 | 
			
		||||
  background-color: $match-line;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.code.white {
 | 
			
		||||
  // Line numbers
 | 
			
		||||
  .line-numbers, .diff-line-num {
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +28,10 @@
 | 
			
		|||
  // Diff line
 | 
			
		||||
  .line_holder {
 | 
			
		||||
 | 
			
		||||
    &.match .line_content {
 | 
			
		||||
      @include matchLine;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .diff-line-num {
 | 
			
		||||
      &.old {
 | 
			
		||||
        background-color: $line-number-old;
 | 
			
		||||
| 
						 | 
				
			
			@ -57,8 +67,7 @@
 | 
			
		|||
      }
 | 
			
		||||
 | 
			
		||||
      &.match {
 | 
			
		||||
        color: $black-transparent;
 | 
			
		||||
        background-color: $match-line;
 | 
			
		||||
        @include matchLine;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.hll:not(.empty-cell) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,329 @@
 | 
			
		|||
[v-cloak] {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.user-can-drag {
 | 
			
		||||
  cursor: -webkit-grab;
 | 
			
		||||
  cursor: grab;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.is-dragging {
 | 
			
		||||
  * {
 | 
			
		||||
    cursor: -webkit-grabbing;
 | 
			
		||||
    cursor: grabbing;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu-issues-board-new {
 | 
			
		||||
  width: 320px;
 | 
			
		||||
 | 
			
		||||
  .dropdown-content {
 | 
			
		||||
    max-height: 150px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.issue-board-dropdown-content {
 | 
			
		||||
  margin: 0 8px 10px;
 | 
			
		||||
  padding-bottom: 10px;
 | 
			
		||||
  border-bottom: 1px solid $dropdown-divider-color;
 | 
			
		||||
 | 
			
		||||
  > p {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    color: #9c9c9c;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.issue-boards-page {
 | 
			
		||||
  .content-wrapper {
 | 
			
		||||
    display: -webkit-flex;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    -webkit-flex-direction: column;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .sub-nav,
 | 
			
		||||
  .issues-filters {
 | 
			
		||||
    -webkit-flex: none;
 | 
			
		||||
    flex: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .page-with-sidebar {
 | 
			
		||||
    display: -webkit-flex;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    min-height: 100vh;
 | 
			
		||||
    max-height: 100vh;
 | 
			
		||||
    padding-bottom: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .issue-boards-content {
 | 
			
		||||
    display: -webkit-flex;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    -webkit-flex: 1;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
    .content {
 | 
			
		||||
      display: -webkit-flex;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      -webkit-flex-direction: column;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.boards-app-loading {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  font-size: 34px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.boards-list {
 | 
			
		||||
  display: -webkit-flex;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  -webkit-flex: 1;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  -webkit-flex-basis: 0;
 | 
			
		||||
  flex-basis: 0;
 | 
			
		||||
  min-height: calc(100vh - 152px);
 | 
			
		||||
  max-height: calc(100vh - 152px);
 | 
			
		||||
  padding-top: 25px;
 | 
			
		||||
  padding-right: ($gl-padding / 2);
 | 
			
		||||
  padding-left: ($gl-padding / 2);
 | 
			
		||||
  overflow-x: scroll;
 | 
			
		||||
 | 
			
		||||
  @media (min-width: $screen-sm-min) {
 | 
			
		||||
    min-height: 475px;
 | 
			
		||||
    max-height: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board {
 | 
			
		||||
  display: -webkit-flex;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  min-width: calc(100vw - 15px);
 | 
			
		||||
  max-width: calc(100vw - 15px);
 | 
			
		||||
  margin-bottom: 25px;
 | 
			
		||||
  padding-right: ($gl-padding / 2);
 | 
			
		||||
  padding-left: ($gl-padding / 2);
 | 
			
		||||
 | 
			
		||||
  @media (min-width: $screen-sm-min) {
 | 
			
		||||
    min-width: 400px;
 | 
			
		||||
    max-width: 400px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-inner {
 | 
			
		||||
  display: -webkit-flex;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  -webkit-flex-direction: column;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  font-size: $issue-boards-font-size;
 | 
			
		||||
  background: $background-color;
 | 
			
		||||
  border: 1px solid $border-color;
 | 
			
		||||
  border-radius: $border-radius-default;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-header {
 | 
			
		||||
  border-top-left-radius: $border-radius-default;
 | 
			
		||||
  border-top-right-radius: $border-radius-default;
 | 
			
		||||
 | 
			
		||||
  &.has-border {
 | 
			
		||||
    border-top: 3px solid;
 | 
			
		||||
 | 
			
		||||
    .board-title {
 | 
			
		||||
      padding-top: ($gl-padding - 3px);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-header-loading-spinner {
 | 
			
		||||
  margin-right: 10px;
 | 
			
		||||
  color: $gray-darkest;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-inner-container {
 | 
			
		||||
  border-bottom: 1px solid $border-color;
 | 
			
		||||
  padding: $gl-padding;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-title {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: $gl-padding;
 | 
			
		||||
  font-size: 1em;
 | 
			
		||||
  border-bottom: 1px solid $border-color;
 | 
			
		||||
 | 
			
		||||
  .board-mobile-handle {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    top: 1px;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-search-container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  background-color: #fff;
 | 
			
		||||
 | 
			
		||||
  .form-control {
 | 
			
		||||
    padding-right: 30px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-search-icon,
 | 
			
		||||
.board-search-clear-btn {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  right: $gl-padding + 10px;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  margin-top: -7px;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-search-icon {
 | 
			
		||||
  color: $gl-placeholder-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-search-clear-btn {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  outline: 0;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: $gl-link-color;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-delete {
 | 
			
		||||
  margin-right: 10px;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  color: $gray-darkest;
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  outline: 0;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: $gl-link-color;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-blank-state {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  padding: $gl-padding;
 | 
			
		||||
  background-color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-blank-state-list {
 | 
			
		||||
  list-style: none;
 | 
			
		||||
 | 
			
		||||
  > li:not(:last-child) {
 | 
			
		||||
    margin-bottom: 8px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .label-color {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    top: 2px;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    width: 16px;
 | 
			
		||||
    height: 16px;
 | 
			
		||||
    margin-right: 3px;
 | 
			
		||||
    border-radius: $border-radius-default;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-list {
 | 
			
		||||
  -webkit-flex: 1;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  height: 400px;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
  overflow-y: scroll;
 | 
			
		||||
  overflow-x: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-list-loading {
 | 
			
		||||
  margin-top: 10px;
 | 
			
		||||
  font-size: 26px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.is-ghost {
 | 
			
		||||
  opacity: 0.3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.is-dragging {
 | 
			
		||||
  // Important because plugin sets inline CSS
 | 
			
		||||
  opacity: 1!important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: 10px $gl-padding;
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  border-radius: $border-radius-default;
 | 
			
		||||
  box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
 | 
			
		||||
  list-style: none;
 | 
			
		||||
 | 
			
		||||
  &.user-can-drag {
 | 
			
		||||
    padding-left: ($gl-padding * 2);
 | 
			
		||||
 | 
			
		||||
    @media (min-width: $screen-sm-min) {
 | 
			
		||||
      padding-left: $gl-padding;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:not(:last-child) {
 | 
			
		||||
    margin-bottom: 5px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  a {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .label {
 | 
			
		||||
    border: 0;
 | 
			
		||||
    outline: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .confidential-icon {
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.board-mobile-handle {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 10px;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  margin-top: (-15px / 2);
 | 
			
		||||
 | 
			
		||||
  @media (min-width: $screen-sm-min) {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-title {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  font-size: 1em;
 | 
			
		||||
 | 
			
		||||
  a {
 | 
			
		||||
    color: inherit;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-footer {
 | 
			
		||||
  margin-top: 5px;
 | 
			
		||||
 | 
			
		||||
  .label {
 | 
			
		||||
    margin-right: 4px;
 | 
			
		||||
    font-size: (14px / $issue-boards-font-size) * 1em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-number {
 | 
			
		||||
  margin-right: 8px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -53,14 +53,6 @@
 | 
			
		|||
      left: 70px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nav-links {
 | 
			
		||||
    svg {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      top: 2px;
 | 
			
		||||
      margin-right: 3px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.build-header {
 | 
			
		||||
| 
						 | 
				
			
			@ -108,24 +100,98 @@
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.right-sidebar.build-sidebar {
 | 
			
		||||
  padding-top: $gl-padding;
 | 
			
		||||
  padding-bottom: $gl-padding;
 | 
			
		||||
  padding: $gl-padding 0;
 | 
			
		||||
 | 
			
		||||
  &.right-sidebar-collapsed {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .blocks-container {
 | 
			
		||||
    padding: $gl-padding;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .block {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .build-sidebar-header {
 | 
			
		||||
    padding-top: 0;
 | 
			
		||||
    padding: 0 $gl-padding $gl-padding;
 | 
			
		||||
 | 
			
		||||
    .gutter-toggle {
 | 
			
		||||
      margin-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .stage-item {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      color: $gl-text-color;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .build-dropdown {
 | 
			
		||||
    padding: 0 $gl-padding;
 | 
			
		||||
 | 
			
		||||
    .dropdown-menu-toggle {
 | 
			
		||||
      margin-top: 8px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropdown-menu {
 | 
			
		||||
      right: $gl-padding;
 | 
			
		||||
      left: $gl-padding;
 | 
			
		||||
      width: auto;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .builds-container {
 | 
			
		||||
    margin-top: $gl-padding;
 | 
			
		||||
    background-color: $white-light;
 | 
			
		||||
    border-top: 1px solid $border-color;
 | 
			
		||||
    border-bottom: 1px solid $border-color;
 | 
			
		||||
    max-height: 300px;
 | 
			
		||||
    overflow: scroll;
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      top: 2px;
 | 
			
		||||
      margin-right: 3px;
 | 
			
		||||
      height: 13px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    a {
 | 
			
		||||
      display: block;
 | 
			
		||||
      padding: $gl-padding 10px $gl-padding 40px;
 | 
			
		||||
      width: 270px;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: $row-hover;
 | 
			
		||||
        color: $gl-text-color;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .build-job {
 | 
			
		||||
      position: relative;
 | 
			
		||||
 | 
			
		||||
      .fa {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        left: 15px;
 | 
			
		||||
        top: 20px;
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.active {
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
 | 
			
		||||
        .fa {
 | 
			
		||||
          display: block;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.build-detail-row {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,238 @@
 | 
			
		|||
$colors: (
 | 
			
		||||
  white_header_head_neutral   : #e1fad7,
 | 
			
		||||
  white_line_head_neutral     : #effdec,
 | 
			
		||||
  white_button_head_neutral   : #9adb84,
 | 
			
		||||
 | 
			
		||||
  white_header_head_chosen    : #baf0a8,
 | 
			
		||||
  white_line_head_chosen      : #e1fad7,
 | 
			
		||||
  white_button_head_chosen    : #52c22d,
 | 
			
		||||
 | 
			
		||||
  white_header_origin_neutral : #e0f0ff,
 | 
			
		||||
  white_line_origin_neutral   : #f2f9ff,
 | 
			
		||||
  white_button_origin_neutral : #87c2fa,
 | 
			
		||||
 | 
			
		||||
  white_header_origin_chosen  : #add8ff,
 | 
			
		||||
  white_line_origin_chosen    : #e0f0ff,
 | 
			
		||||
  white_button_origin_chosen  : #268ced,
 | 
			
		||||
 | 
			
		||||
  white_header_not_chosen     : #f0f0f0,
 | 
			
		||||
  white_line_not_chosen       : #f9f9f9,
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  dark_header_head_neutral   : rgba(#3f3, .2),
 | 
			
		||||
  dark_line_head_neutral     : rgba(#3f3, .1),
 | 
			
		||||
  dark_button_head_neutral   : #40874f,
 | 
			
		||||
 | 
			
		||||
  dark_header_head_chosen    : rgba(#3f3, .33),
 | 
			
		||||
  dark_line_head_chosen      : rgba(#3f3, .2),
 | 
			
		||||
  dark_button_head_chosen    : #258537,
 | 
			
		||||
 | 
			
		||||
  dark_header_origin_neutral : rgba(#2878c9, .4),
 | 
			
		||||
  dark_line_origin_neutral   : rgba(#2878c9, .3),
 | 
			
		||||
  dark_button_origin_neutral : #2a5c8c,
 | 
			
		||||
 | 
			
		||||
  dark_header_origin_chosen  : rgba(#2878c9, .6),
 | 
			
		||||
  dark_line_origin_chosen    : rgba(#2878c9, .4),
 | 
			
		||||
  dark_button_origin_chosen  : #1d6cbf,
 | 
			
		||||
 | 
			
		||||
  dark_header_not_chosen     : rgba(#fff, .25),
 | 
			
		||||
  dark_line_not_chosen       : rgba(#fff, .1),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  monokai_header_head_neutral   : rgba(#a6e22e, .25),
 | 
			
		||||
  monokai_line_head_neutral     : rgba(#a6e22e, .1),
 | 
			
		||||
  monokai_button_head_neutral   : #376b20,
 | 
			
		||||
 | 
			
		||||
  monokai_header_head_chosen    : rgba(#a6e22e, .4),
 | 
			
		||||
  monokai_line_head_chosen      : rgba(#a6e22e, .25),
 | 
			
		||||
  monokai_button_head_chosen    : #39800d,
 | 
			
		||||
 | 
			
		||||
  monokai_header_origin_neutral : rgba(#60d9f1, .35),
 | 
			
		||||
  monokai_line_origin_neutral   : rgba(#60d9f1, .15),
 | 
			
		||||
  monokai_button_origin_neutral : #38848c,
 | 
			
		||||
 | 
			
		||||
  monokai_header_origin_chosen  : rgba(#60d9f1, .5),
 | 
			
		||||
  monokai_line_origin_chosen    : rgba(#60d9f1, .35),
 | 
			
		||||
  monokai_button_origin_chosen  : #3ea4b2,
 | 
			
		||||
 | 
			
		||||
  monokai_header_not_chosen     : rgba(#76715d, .24),
 | 
			
		||||
  monokai_line_not_chosen       : rgba(#76715d, .1),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  solarized_light_header_head_neutral   : rgba(#859900, .37),
 | 
			
		||||
  solarized_light_line_head_neutral     : rgba(#859900, .2),
 | 
			
		||||
  solarized_light_button_head_neutral   : #afb262,
 | 
			
		||||
 | 
			
		||||
  solarized_light_header_head_chosen    : rgba(#859900, .5),
 | 
			
		||||
  solarized_light_line_head_chosen      : rgba(#859900, .37),
 | 
			
		||||
  solarized_light_button_head_chosen    : #94993d,
 | 
			
		||||
 | 
			
		||||
  solarized_light_header_origin_neutral : rgba(#2878c9, .37),
 | 
			
		||||
  solarized_light_line_origin_neutral   : rgba(#2878c9, .15),
 | 
			
		||||
  solarized_light_button_origin_neutral : #60a1bf,
 | 
			
		||||
 | 
			
		||||
  solarized_light_header_origin_chosen  : rgba(#2878c9, .6),
 | 
			
		||||
  solarized_light_line_origin_chosen    : rgba(#2878c9, .37),
 | 
			
		||||
  solarized_light_button_origin_chosen  : #2482b2,
 | 
			
		||||
 | 
			
		||||
  solarized_light_header_not_chosen     : rgba(#839496, .37),
 | 
			
		||||
  solarized_light_line_not_chosen       : rgba(#839496, .2),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  solarized_dark_header_head_neutral   : rgba(#859900, .35),
 | 
			
		||||
  solarized_dark_line_head_neutral     : rgba(#859900, .15),
 | 
			
		||||
  solarized_dark_button_head_neutral   : #376b20,
 | 
			
		||||
 | 
			
		||||
  solarized_dark_header_head_chosen    : rgba(#859900, .5),
 | 
			
		||||
  solarized_dark_line_head_chosen      : rgba(#859900, .35),
 | 
			
		||||
  solarized_dark_button_head_chosen    : #39800d,
 | 
			
		||||
 | 
			
		||||
  solarized_dark_header_origin_neutral : rgba(#2878c9, .35),
 | 
			
		||||
  solarized_dark_line_origin_neutral   : rgba(#2878c9, .15),
 | 
			
		||||
  solarized_dark_button_origin_neutral : #086799,
 | 
			
		||||
 | 
			
		||||
  solarized_dark_header_origin_chosen  : rgba(#2878c9, .6),
 | 
			
		||||
  solarized_dark_line_origin_chosen    : rgba(#2878c9, .35),
 | 
			
		||||
  solarized_dark_button_origin_chosen  : #0082cc,
 | 
			
		||||
 | 
			
		||||
  solarized_dark_header_not_chosen     : rgba(#839496, .25),
 | 
			
		||||
  solarized_dark_line_not_chosen       : rgba(#839496, .15)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@mixin color-scheme($color) {
 | 
			
		||||
  .header.line_content, .diff-line-num {
 | 
			
		||||
    &.origin {
 | 
			
		||||
      background-color: map-get($colors, #{$color}_header_origin_neutral);
 | 
			
		||||
      border-color: map-get($colors, #{$color}_header_origin_neutral);
 | 
			
		||||
 | 
			
		||||
      button {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_button_origin_neutral);
 | 
			
		||||
        border-color: darken(map-get($colors, #{$color}_button_origin_neutral), 15);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.selected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_header_origin_chosen);
 | 
			
		||||
        border-color: map-get($colors, #{$color}_header_origin_chosen);
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
          background-color: map-get($colors, #{$color}_button_origin_chosen);
 | 
			
		||||
          border-color: darken(map-get($colors, #{$color}_button_origin_chosen), 15);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.unselected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_header_not_chosen);
 | 
			
		||||
        border-color: map-get($colors, #{$color}_header_not_chosen);
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
          background-color: lighten(map-get($colors, #{$color}_button_origin_neutral), 15);
 | 
			
		||||
          border-color: map-get($colors, #{$color}_button_origin_neutral);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &.head {
 | 
			
		||||
      background-color: map-get($colors, #{$color}_header_head_neutral);
 | 
			
		||||
      border-color: map-get($colors, #{$color}_header_head_neutral);
 | 
			
		||||
 | 
			
		||||
      button {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_button_head_neutral);
 | 
			
		||||
        border-color: darken(map-get($colors, #{$color}_button_head_neutral), 15);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.selected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_header_head_chosen);
 | 
			
		||||
        border-color: map-get($colors, #{$color}_header_head_chosen);
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
          background-color: map-get($colors, #{$color}_button_head_chosen);
 | 
			
		||||
          border-color: darken(map-get($colors, #{$color}_button_head_chosen), 15);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.unselected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_header_not_chosen);
 | 
			
		||||
        border-color: map-get($colors, #{$color}_header_not_chosen);
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
          background-color: lighten(map-get($colors, #{$color}_button_head_neutral), 15);
 | 
			
		||||
          border-color: map-get($colors, #{$color}_button_head_neutral);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .line_content {
 | 
			
		||||
    &.origin {
 | 
			
		||||
      background-color: map-get($colors, #{$color}_line_origin_neutral);
 | 
			
		||||
 | 
			
		||||
      &.selected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_line_origin_chosen);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.unselected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_line_not_chosen);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &.head {
 | 
			
		||||
      background-color: map-get($colors, #{$color}_line_head_neutral);
 | 
			
		||||
 | 
			
		||||
      &.selected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_line_head_chosen);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.unselected {
 | 
			
		||||
        background-color: map-get($colors, #{$color}_line_not_chosen);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#conflicts {
 | 
			
		||||
 | 
			
		||||
  .white {
 | 
			
		||||
    @include color-scheme('white')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .dark {
 | 
			
		||||
    @include color-scheme('dark')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .monokai {
 | 
			
		||||
    @include color-scheme('monokai')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .solarized-light {
 | 
			
		||||
    @include color-scheme('solarized_light')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .solarized-dark {
 | 
			
		||||
    @include color-scheme('solarized_dark')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .diff-wrap-lines .line_content {
 | 
			
		||||
    white-space: normal;
 | 
			
		||||
    min-height: 19px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .line_content.header {
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    button {
 | 
			
		||||
      border-radius: 2px;
 | 
			
		||||
      font-size: 10px;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: 10px;
 | 
			
		||||
      padding: 0;
 | 
			
		||||
      outline: none;
 | 
			
		||||
      color: #fff;
 | 
			
		||||
      width: 75px; // static width to make 2 buttons have same width
 | 
			
		||||
      height: 19px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .btn-success .fa-spinner {
 | 
			
		||||
    color: #fff;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -229,3 +229,196 @@
 | 
			
		|||
    box-shadow: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Pipeline visualization
 | 
			
		||||
 | 
			
		||||
.toggle-pipeline-btn {
 | 
			
		||||
  background-color: $gray-dark;
 | 
			
		||||
 | 
			
		||||
  .caret {
 | 
			
		||||
    border-top: none;
 | 
			
		||||
    border-bottom: 4px solid;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.graph-collapsed {
 | 
			
		||||
    background-color: $white-light;
 | 
			
		||||
 | 
			
		||||
    .caret {
 | 
			
		||||
      border-bottom: none;
 | 
			
		||||
      border-top: 4px solid;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pipeline-graph {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  max-height: 500px;
 | 
			
		||||
  transition: max-height 0.3s, padding 0.3s;
 | 
			
		||||
 | 
			
		||||
  &.graph-collapsed {
 | 
			
		||||
    max-height: 0;
 | 
			
		||||
    padding: 0 16px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pipeline-visualization {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  min-width: 1220px;
 | 
			
		||||
 | 
			
		||||
  ul {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stage-column {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  vertical-align: top;
 | 
			
		||||
  margin-right: 50px;
 | 
			
		||||
 | 
			
		||||
  li {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .stage-name {
 | 
			
		||||
    margin-bottom: 15px;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    width: 150px;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .build {
 | 
			
		||||
    border: 1px solid $border-color;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    padding: 6px 10px;
 | 
			
		||||
    border-radius: 30px;
 | 
			
		||||
    width: 150px;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
 | 
			
		||||
    &.playable {
 | 
			
		||||
      background-color: $gray-light;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .build-content {
 | 
			
		||||
      width: 130px;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
 | 
			
		||||
      a {
 | 
			
		||||
        color: $layout-link-gray;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      top: 2px;
 | 
			
		||||
      margin-right: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .fa {
 | 
			
		||||
      font-size: 13px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Connect first build in each stage with right horizontal line
 | 
			
		||||
    &:first-child {
 | 
			
		||||
      &::after {
 | 
			
		||||
        content: '';
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 50%;
 | 
			
		||||
        right: -54px;
 | 
			
		||||
        border-top: 2px solid $border-color;
 | 
			
		||||
        width: 54px;
 | 
			
		||||
        height: 1px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Connect each build (except for first) with curved lines
 | 
			
		||||
    &:not(:first-child) {
 | 
			
		||||
      &::after, &::before {
 | 
			
		||||
        content: '';
 | 
			
		||||
        top: -47px;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        border-bottom: 2px solid $border-color;
 | 
			
		||||
        width: 20px;
 | 
			
		||||
        height: 65px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Right connecting curves
 | 
			
		||||
      &::after {
 | 
			
		||||
        right: -20px;
 | 
			
		||||
        border-right: 2px solid $border-color;
 | 
			
		||||
        border-radius: 0 0 50px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Left connecting curves
 | 
			
		||||
      &::before {
 | 
			
		||||
        left: -20px;
 | 
			
		||||
        border-left: 2px solid $border-color;
 | 
			
		||||
        border-radius: 0 0 0 50px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Connect second build to first build with smaller curved line
 | 
			
		||||
    &:nth-child(2) {
 | 
			
		||||
      &::after, &::before {
 | 
			
		||||
        height: 45px;
 | 
			
		||||
        top: -26px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:last-child {
 | 
			
		||||
    .build {
 | 
			
		||||
      // Remove right connecting horizontal line from first build in last stage
 | 
			
		||||
      &:first-child {
 | 
			
		||||
        &::after, &::before {
 | 
			
		||||
          border: none;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // Remove right curved connectors from all builds in last stage
 | 
			
		||||
      &:not(:first-child) {
 | 
			
		||||
        &::after {
 | 
			
		||||
          border: none;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:first-child {
 | 
			
		||||
    .build {
 | 
			
		||||
      // Remove left curved connectors from all builds in first stage
 | 
			
		||||
      &:not(:first-child) {
 | 
			
		||||
        &::before {
 | 
			
		||||
          border: none;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pipeline-actions {
 | 
			
		||||
  border-bottom: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.toggle-pipeline-btn {
 | 
			
		||||
 | 
			
		||||
  .fa {
 | 
			
		||||
    color: $dropdown-header-color;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pipelines.tab-pane {
 | 
			
		||||
 | 
			
		||||
  .content-list.pipelines {
 | 
			
		||||
    overflow: scroll;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .stage {
 | 
			
		||||
    max-width: 60px;
 | 
			
		||||
    width: 60px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,19 +35,13 @@ class AutocompleteController < ApplicationController
 | 
			
		|||
 | 
			
		||||
  def projects
 | 
			
		||||
    project = Project.find_by_id(params[:project_id])
 | 
			
		||||
 | 
			
		||||
    projects = current_user.authorized_projects
 | 
			
		||||
    projects = projects.search(params[:search]) if params[:search].present?
 | 
			
		||||
    projects = projects.select do |project|
 | 
			
		||||
      current_user.can?(:admin_issue, project)
 | 
			
		||||
    end
 | 
			
		||||
    projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
 | 
			
		||||
 | 
			
		||||
    no_project = {
 | 
			
		||||
      id: 0,
 | 
			
		||||
      name_with_namespace: 'No project',
 | 
			
		||||
    }
 | 
			
		||||
    projects.unshift(no_project)
 | 
			
		||||
    projects.delete(project)
 | 
			
		||||
    projects.unshift(no_project) unless params[:offset_id].present?
 | 
			
		||||
 | 
			
		||||
    render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -79,4 +73,8 @@ class AutocompleteController < ApplicationController
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def projects_finder
 | 
			
		||||
    MoveToProjectFinder.new(current_user)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
class Projects::BoardListsController < Projects::ApplicationController
 | 
			
		||||
  respond_to :json
 | 
			
		||||
 | 
			
		||||
  before_action :authorize_admin_list!
 | 
			
		||||
 | 
			
		||||
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
 | 
			
		||||
 | 
			
		||||
  def create
 | 
			
		||||
    list = Boards::Lists::CreateService.new(project, current_user, list_params).execute
 | 
			
		||||
 | 
			
		||||
    if list.valid?
 | 
			
		||||
      render json: list.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
 | 
			
		||||
    else
 | 
			
		||||
      render json: list.errors, status: :unprocessable_entity
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update
 | 
			
		||||
    service = Boards::Lists::MoveService.new(project, current_user, move_params)
 | 
			
		||||
 | 
			
		||||
    if service.execute
 | 
			
		||||
      head :ok
 | 
			
		||||
    else
 | 
			
		||||
      head :unprocessable_entity
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def destroy
 | 
			
		||||
    service = Boards::Lists::DestroyService.new(project, current_user, params)
 | 
			
		||||
 | 
			
		||||
    if service.execute
 | 
			
		||||
      head :ok
 | 
			
		||||
    else
 | 
			
		||||
      head :unprocessable_entity
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def generate
 | 
			
		||||
    service = Boards::Lists::GenerateService.new(project, current_user)
 | 
			
		||||
 | 
			
		||||
    if service.execute
 | 
			
		||||
      render json: project.board.lists.label.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
 | 
			
		||||
    else
 | 
			
		||||
      head :unprocessable_entity
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def authorize_admin_list!
 | 
			
		||||
    return render_403 unless can?(current_user, :admin_list, project)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def list_params
 | 
			
		||||
    params.require(:list).permit(:label_id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def move_params
 | 
			
		||||
    params.require(:list).permit(:position).merge(id: params[:id])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def record_not_found(exception)
 | 
			
		||||
    render json: { error: exception.message }, status: :not_found
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
module Projects
 | 
			
		||||
  module Boards
 | 
			
		||||
    class ApplicationController < Projects::ApplicationController
 | 
			
		||||
      respond_to :json
 | 
			
		||||
 | 
			
		||||
      rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def record_not_found(exception)
 | 
			
		||||
        render json: { error: exception.message }, status: :not_found
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
module Projects
 | 
			
		||||
  module Boards
 | 
			
		||||
    class IssuesController < Boards::ApplicationController
 | 
			
		||||
      before_action :authorize_read_issue!, only: [:index]
 | 
			
		||||
      before_action :authorize_update_issue!, only: [:update]
 | 
			
		||||
 | 
			
		||||
      def index
 | 
			
		||||
        issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
 | 
			
		||||
        issues = issues.page(params[:page])
 | 
			
		||||
 | 
			
		||||
        render json: issues.as_json(
 | 
			
		||||
          only: [:iid, :title, :confidential],
 | 
			
		||||
          include: {
 | 
			
		||||
            assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
 | 
			
		||||
            labels:   { only: [:id, :title, :description, :color, :priority] }
 | 
			
		||||
          })
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def update
 | 
			
		||||
        service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
 | 
			
		||||
 | 
			
		||||
        if service.execute(issue)
 | 
			
		||||
          head :ok
 | 
			
		||||
        else
 | 
			
		||||
          head :unprocessable_entity
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def issue
 | 
			
		||||
        @issue ||=
 | 
			
		||||
          IssuesFinder.new(current_user, project_id: project.id, state: 'all')
 | 
			
		||||
                      .execute
 | 
			
		||||
                      .where(iid: params[:id])
 | 
			
		||||
                      .first!
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def authorize_read_issue!
 | 
			
		||||
        return render_403 unless can?(current_user, :read_issue, project)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def authorize_update_issue!
 | 
			
		||||
        return render_403 unless can?(current_user, :update_issue, issue)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def filter_params
 | 
			
		||||
        params.merge(id: params[:list_id])
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def move_params
 | 
			
		||||
        params.permit(:id, :from_list_id, :to_list_id)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
module Projects
 | 
			
		||||
  module Boards
 | 
			
		||||
    class ListsController < Boards::ApplicationController
 | 
			
		||||
      before_action :authorize_admin_list!, only: [:create, :update, :destroy, :generate]
 | 
			
		||||
      before_action :authorize_read_list!, only: [:index]
 | 
			
		||||
 | 
			
		||||
      def index
 | 
			
		||||
        render json: serialize_as_json(project.board.lists)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def create
 | 
			
		||||
        list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute
 | 
			
		||||
 | 
			
		||||
        if list.valid?
 | 
			
		||||
          render json: serialize_as_json(list)
 | 
			
		||||
        else
 | 
			
		||||
          render json: list.errors, status: :unprocessable_entity
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def update
 | 
			
		||||
        list = project.board.lists.movable.find(params[:id])
 | 
			
		||||
        service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
 | 
			
		||||
 | 
			
		||||
        if service.execute(list)
 | 
			
		||||
          head :ok
 | 
			
		||||
        else
 | 
			
		||||
          head :unprocessable_entity
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def destroy
 | 
			
		||||
        list = project.board.lists.destroyable.find(params[:id])
 | 
			
		||||
        service = ::Boards::Lists::DestroyService.new(project, current_user, params)
 | 
			
		||||
 | 
			
		||||
        if service.execute(list)
 | 
			
		||||
          head :ok
 | 
			
		||||
        else
 | 
			
		||||
          head :unprocessable_entity
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def generate
 | 
			
		||||
        service = ::Boards::Lists::GenerateService.new(project, current_user)
 | 
			
		||||
 | 
			
		||||
        if service.execute
 | 
			
		||||
          render json: serialize_as_json(project.board.lists.movable)
 | 
			
		||||
        else
 | 
			
		||||
          head :unprocessable_entity
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def authorize_admin_list!
 | 
			
		||||
        return render_403 unless can?(current_user, :admin_list, project)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def authorize_read_list!
 | 
			
		||||
        return render_403 unless can?(current_user, :read_list, project)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def list_params
 | 
			
		||||
        params.require(:list).permit(:label_id)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def move_params
 | 
			
		||||
        params.require(:list).permit(:position)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def serialize_as_json(resource)
 | 
			
		||||
        resource.as_json(
 | 
			
		||||
          only: [:id, :list_type, :position],
 | 
			
		||||
          methods: [:title],
 | 
			
		||||
          include: {
 | 
			
		||||
            label: { only: [:id, :title, :description, :color, :priority] }
 | 
			
		||||
          })
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
class Projects::BoardsController < Projects::ApplicationController
 | 
			
		||||
  respond_to :html
 | 
			
		||||
 | 
			
		||||
  before_action :authorize_read_board!, only: [:show]
 | 
			
		||||
 | 
			
		||||
  def show
 | 
			
		||||
    ::Boards::CreateService.new(project, current_user).execute
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def authorize_read_board!
 | 
			
		||||
    return access_denied! unless can?(current_user, :read_board, project)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -9,15 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
 | 
			
		||||
  before_action :module_enabled
 | 
			
		||||
  before_action :merge_request, only: [
 | 
			
		||||
    :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
 | 
			
		||||
    :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip
 | 
			
		||||
    :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check,
 | 
			
		||||
    :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts
 | 
			
		||||
  ]
 | 
			
		||||
  before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
 | 
			
		||||
  before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
 | 
			
		||||
  before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
 | 
			
		||||
  before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines]
 | 
			
		||||
  before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
 | 
			
		||||
  before_action :define_commit_vars, only: [:diffs]
 | 
			
		||||
  before_action :define_diff_comment_vars, only: [:diffs]
 | 
			
		||||
  before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
 | 
			
		||||
  before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
 | 
			
		||||
 | 
			
		||||
  # Allow read any merge_request
 | 
			
		||||
  before_action :authorize_read_merge_request!
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +28,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
  # Allow modify merge_request
 | 
			
		||||
  before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
 | 
			
		||||
 | 
			
		||||
  before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
 | 
			
		||||
 | 
			
		||||
  def index
 | 
			
		||||
    terms = params['issue_search']
 | 
			
		||||
    @merge_requests = merge_requests_collection
 | 
			
		||||
| 
						 | 
				
			
			@ -130,6 +132,47 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def conflicts
 | 
			
		||||
    respond_to do |format|
 | 
			
		||||
      format.html { define_discussion_vars }
 | 
			
		||||
 | 
			
		||||
      format.json do
 | 
			
		||||
        if @merge_request.conflicts_can_be_resolved_in_ui?
 | 
			
		||||
          render json: @merge_request.conflicts
 | 
			
		||||
        elsif @merge_request.can_be_merged?
 | 
			
		||||
          render json: {
 | 
			
		||||
            message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.',
 | 
			
		||||
            type: 'error'
 | 
			
		||||
          }
 | 
			
		||||
        else
 | 
			
		||||
          render json: {
 | 
			
		||||
            message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.',
 | 
			
		||||
            type: 'error'
 | 
			
		||||
          }
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def resolve_conflicts
 | 
			
		||||
    return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
 | 
			
		||||
 | 
			
		||||
    if @merge_request.can_be_merged?
 | 
			
		||||
      render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' }
 | 
			
		||||
      return
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      MergeRequests::ResolveService.new(@merge_request.source_project, current_user, params).execute(@merge_request)
 | 
			
		||||
 | 
			
		||||
      flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
 | 
			
		||||
 | 
			
		||||
      render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) }
 | 
			
		||||
    rescue Gitlab::Conflict::File::MissingResolution => e
 | 
			
		||||
      render status: :bad_request, json: { message: e.message }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def builds
 | 
			
		||||
    respond_to do |format|
 | 
			
		||||
      format.html do
 | 
			
		||||
| 
						 | 
				
			
			@ -141,7 +184,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pipelines
 | 
			
		||||
    @pipelines = @merge_request.all_pipelines
 | 
			
		||||
 | 
			
		||||
    respond_to do |format|
 | 
			
		||||
      format.html do
 | 
			
		||||
        define_discussion_vars
 | 
			
		||||
 | 
			
		||||
        render 'show'
 | 
			
		||||
      end
 | 
			
		||||
      format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_pipelines') } }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def new
 | 
			
		||||
    apply_diff_view_cookie!
 | 
			
		||||
 | 
			
		||||
    build_merge_request
 | 
			
		||||
    @noteable = @merge_request
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -338,6 +396,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
    return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def authorize_can_resolve_conflicts!
 | 
			
		||||
    return render_404 unless @merge_request.conflicts_can_be_resolved_by?(current_user)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def module_enabled
 | 
			
		||||
    return render_404 unless @project.merge_requests_enabled
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -412,7 +474,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 | 
			
		|||
      noteable_id: @merge_request.id
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @use_legacy_diff_notes = !@merge_request.support_new_diff_notes?
 | 
			
		||||
    @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
 | 
			
		||||
    @grouped_diff_discussions = @merge_request.notes.inc_author_project_award_emoji.grouped_diff_discussions
 | 
			
		||||
 | 
			
		||||
    Banzai::NoteRenderer.render(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
class MoveToProjectFinder
 | 
			
		||||
  def initialize(user)
 | 
			
		||||
    @user = user
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def execute(from_project, search: nil, offset_id: nil)
 | 
			
		||||
    projects = @user.projects_where_can_admin_issues
 | 
			
		||||
    projects = projects.search(search) if search.present?
 | 
			
		||||
    projects = projects.excluding_project(from_project)
 | 
			
		||||
 | 
			
		||||
    # to ask for Project#name_with_namespace
 | 
			
		||||
    projects.includes(namespace: :owner)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -320,4 +320,8 @@ module ApplicationHelper
 | 
			
		|||
      capture(&block)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def page_class
 | 
			
		||||
    "issue-boards-page" if current_controller?(:boards)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,10 @@ module CiStatusHelper
 | 
			
		|||
        'icon_status_pending'
 | 
			
		||||
      when 'running'
 | 
			
		||||
        'icon_status_running'
 | 
			
		||||
      when 'play'
 | 
			
		||||
        return icon('play fw')
 | 
			
		||||
      when 'created'
 | 
			
		||||
        'icon_status_pending'
 | 
			
		||||
      else
 | 
			
		||||
        'icon_status_cancel'
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			@ -53,13 +57,13 @@ module CiStatusHelper
 | 
			
		|||
  def render_commit_status(commit, tooltip_placement: 'auto left')
 | 
			
		||||
    project = commit.project
 | 
			
		||||
    path = builds_namespace_project_commit_path(project.namespace, project, commit)
 | 
			
		||||
    render_status_with_link('commit', commit.status, path, tooltip_placement)
 | 
			
		||||
    render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
 | 
			
		||||
    project = pipeline.project
 | 
			
		||||
    path = namespace_project_pipeline_path(project.namespace, project, pipeline)
 | 
			
		||||
    render_status_with_link('pipeline', pipeline.status, path, tooltip_placement)
 | 
			
		||||
    render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def no_runners_for_project?(project)
 | 
			
		||||
| 
						 | 
				
			
			@ -67,13 +71,17 @@ module CiStatusHelper
 | 
			
		|||
      Ci::Runner.shared.blank?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
  def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '')
 | 
			
		||||
    klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
 | 
			
		||||
    title = "#{type.titleize}: #{ci_label_for_status(status)}"
 | 
			
		||||
    data = { toggle: 'tooltip', placement: tooltip_placement }
 | 
			
		||||
 | 
			
		||||
  def render_status_with_link(type, status, path, tooltip_placement, cssclass: '')
 | 
			
		||||
    link_to ci_icon_for_status(status),
 | 
			
		||||
            path,
 | 
			
		||||
            class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}",
 | 
			
		||||
            title: "#{type.titleize}: #{ci_label_for_status(status)}",
 | 
			
		||||
            data: { toggle: 'tooltip', placement: tooltip_placement }
 | 
			
		||||
    if path
 | 
			
		||||
      link_to ci_icon_for_status(status), path,
 | 
			
		||||
              class: klass, title: title, data: data
 | 
			
		||||
    else
 | 
			
		||||
      content_tag :span, ci_icon_for_status(status),
 | 
			
		||||
              class: klass, title: title, data: data
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -98,28 +98,31 @@ module CommitsHelper
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def link_to_browse_code(project, commit)
 | 
			
		||||
    if current_controller?(:projects, :commits)
 | 
			
		||||
      if @repo.blob_at(commit.id, @path)
 | 
			
		||||
        return link_to(
 | 
			
		||||
          "Browse File",
 | 
			
		||||
          namespace_project_blob_path(project.namespace, project,
 | 
			
		||||
                                      tree_join(commit.id, @path)),
 | 
			
		||||
          class: "btn btn-default"
 | 
			
		||||
        )
 | 
			
		||||
      elsif @path.present?
 | 
			
		||||
        return link_to(
 | 
			
		||||
          "Browse Directory",
 | 
			
		||||
          namespace_project_tree_path(project.namespace, project,
 | 
			
		||||
                                      tree_join(commit.id, @path)),
 | 
			
		||||
          class: "btn btn-default"
 | 
			
		||||
        )
 | 
			
		||||
      end
 | 
			
		||||
    if @path.blank?
 | 
			
		||||
      return link_to(
 | 
			
		||||
        "Browse Files",
 | 
			
		||||
        namespace_project_tree_path(project.namespace, project, commit),
 | 
			
		||||
        class: "btn btn-default"
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return unless current_controller?(:projects, :commits)
 | 
			
		||||
 | 
			
		||||
    if @repo.blob_at(commit.id, @path)
 | 
			
		||||
      return link_to(
 | 
			
		||||
        "Browse File",
 | 
			
		||||
        namespace_project_blob_path(project.namespace, project,
 | 
			
		||||
                                    tree_join(commit.id, @path)),
 | 
			
		||||
        class: "btn btn-default"
 | 
			
		||||
      )
 | 
			
		||||
    elsif @path.present?
 | 
			
		||||
      return link_to(
 | 
			
		||||
        "Browse Directory",
 | 
			
		||||
        namespace_project_tree_path(project.namespace, project,
 | 
			
		||||
                                    tree_join(commit.id, @path)),
 | 
			
		||||
        class: "btn btn-default"
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
    link_to(
 | 
			
		||||
      "Browse Files",
 | 
			
		||||
      namespace_project_tree_path(project.namespace, project, commit),
 | 
			
		||||
      class: "btn btn-default"
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,7 @@ module NavHelper
 | 
			
		|||
      current_path?('merge_requests#diffs') ||
 | 
			
		||||
      current_path?('merge_requests#commits') ||
 | 
			
		||||
      current_path?('merge_requests#builds') ||
 | 
			
		||||
      current_path?('merge_requests#conflicts') ||
 | 
			
		||||
      current_path?('issues#show')
 | 
			
		||||
      if cookies[:collapsed_gutter] == 'true'
 | 
			
		||||
        "page-gutter right-sidebar-collapsed"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,11 @@ module Emails
 | 
			
		|||
      mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id)
 | 
			
		||||
      setup_issue_mail(issue_id, recipient_id)
 | 
			
		||||
      mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
 | 
			
		||||
      setup_issue_mail(issue_id, recipient_id)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,11 @@ module Emails
 | 
			
		|||
      mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
 | 
			
		||||
      setup_merge_request_mail(merge_request_id, recipient_id)
 | 
			
		||||
      mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
 | 
			
		||||
      setup_merge_request_mail(merge_request_id, recipient_id)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,6 +90,8 @@ class Ability
 | 
			
		|||
      if project && project.public?
 | 
			
		||||
        rules = [
 | 
			
		||||
          :read_project,
 | 
			
		||||
          :read_board,
 | 
			
		||||
          :read_list,
 | 
			
		||||
          :read_wiki,
 | 
			
		||||
          :read_label,
 | 
			
		||||
          :read_milestone,
 | 
			
		||||
| 
						 | 
				
			
			@ -228,6 +230,8 @@ class Ability
 | 
			
		|||
        :read_project,
 | 
			
		||||
        :read_wiki,
 | 
			
		||||
        :read_issue,
 | 
			
		||||
        :read_board,
 | 
			
		||||
        :read_list,
 | 
			
		||||
        :read_label,
 | 
			
		||||
        :read_milestone,
 | 
			
		||||
        :read_project_snippet,
 | 
			
		||||
| 
						 | 
				
			
			@ -249,6 +253,7 @@ class Ability
 | 
			
		|||
        :update_issue,
 | 
			
		||||
        :admin_issue,
 | 
			
		||||
        :admin_label,
 | 
			
		||||
        :admin_list,
 | 
			
		||||
        :read_commit_status,
 | 
			
		||||
        :read_build,
 | 
			
		||||
        :read_container_image,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
class Board < ActiveRecord::Base
 | 
			
		||||
  belongs_to :project
 | 
			
		||||
 | 
			
		||||
  has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
 | 
			
		||||
 | 
			
		||||
  validates :project, presence: true
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +97,7 @@ module Ci
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    def playable?
 | 
			
		||||
      project.builds_enabled? && commands.present? && manual?
 | 
			
		||||
      project.builds_enabled? && commands.present? && manual? && skipped?
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def play(current_user = nil)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -78,6 +78,10 @@ module Ci
 | 
			
		|||
      CommitStatus.where(pipeline: pluck(:id)).stages
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def stages_with_latest_statuses
 | 
			
		||||
      statuses.latest.order(:stage_idx).group_by(&:stage)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def project_id
 | 
			
		||||
      project.id
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,7 +75,7 @@ class DiffNote < Note
 | 
			
		|||
  private
 | 
			
		||||
 | 
			
		||||
  def supported?
 | 
			
		||||
    !self.for_merge_request? || self.noteable.support_new_diff_notes?
 | 
			
		||||
    !self.for_merge_request? || self.noteable.has_complete_diff_refs?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def noteable_diff_refs
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,8 @@ class Label < ActiveRecord::Base
 | 
			
		|||
  default_value_for :color, DEFAULT_COLOR
 | 
			
		||||
 | 
			
		||||
  belongs_to :project
 | 
			
		||||
 | 
			
		||||
  has_many :lists, dependent: :destroy
 | 
			
		||||
  has_many :label_links, dependent: :destroy
 | 
			
		||||
  has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
 | 
			
		||||
  has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
class List < ActiveRecord::Base
 | 
			
		||||
  belongs_to :board
 | 
			
		||||
  belongs_to :label
 | 
			
		||||
 | 
			
		||||
  enum list_type: { backlog: 0, label: 1, done: 2 }
 | 
			
		||||
 | 
			
		||||
  validates :board, :list_type, presence: true
 | 
			
		||||
  validates :label, :position, presence: true, if: :label?
 | 
			
		||||
  validates :label_id, uniqueness: { scope: :board_id }, if: :label?
 | 
			
		||||
  validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :label?
 | 
			
		||||
 | 
			
		||||
  before_destroy :can_be_destroyed
 | 
			
		||||
 | 
			
		||||
  scope :destroyable, -> { where(list_type: list_types[:label]) }
 | 
			
		||||
  scope :movable, -> { where(list_type: list_types[:label]) }
 | 
			
		||||
 | 
			
		||||
  def destroyable?
 | 
			
		||||
    label?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def movable?
 | 
			
		||||
    label?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def title
 | 
			
		||||
    label? ? label.name : list_type.humanize
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def can_be_destroyed
 | 
			
		||||
    destroyable?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -674,12 +674,23 @@ class MergeRequest < ActiveRecord::Base
 | 
			
		|||
    diverged_commits_count > 0
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def commits_sha
 | 
			
		||||
    commits.map(&:sha)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pipeline
 | 
			
		||||
    return unless diff_head_sha && source_project
 | 
			
		||||
 | 
			
		||||
    @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def all_pipelines
 | 
			
		||||
    @all_pipelines ||=
 | 
			
		||||
      if diff_head_sha && source_project
 | 
			
		||||
        source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch)
 | 
			
		||||
      end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def merge_commit
 | 
			
		||||
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -692,12 +703,12 @@ class MergeRequest < ActiveRecord::Base
 | 
			
		|||
    merge_commit
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def support_new_diff_notes?
 | 
			
		||||
  def has_complete_diff_refs?
 | 
			
		||||
    diff_sha_refs && diff_sha_refs.complete?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update_diff_notes_positions(old_diff_refs:, new_diff_refs:)
 | 
			
		||||
    return unless support_new_diff_notes?
 | 
			
		||||
    return unless has_complete_diff_refs?
 | 
			
		||||
    return if new_diff_refs == old_diff_refs
 | 
			
		||||
 | 
			
		||||
    active_diff_notes = self.notes.diff_notes.select do |note|
 | 
			
		||||
| 
						 | 
				
			
			@ -725,4 +736,26 @@ class MergeRequest < ActiveRecord::Base
 | 
			
		|||
  def keep_around_commit
 | 
			
		||||
    project.repository.keep_around(self.merge_commit_sha)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def conflicts
 | 
			
		||||
    @conflicts ||= Gitlab::Conflict::FileCollection.new(self)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def conflicts_can_be_resolved_by?(user)
 | 
			
		||||
    access = ::Gitlab::UserAccess.new(user, project: source_project)
 | 
			
		||||
    access.can_push_to_branch?(source_branch)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def conflicts_can_be_resolved_in_ui?
 | 
			
		||||
    return @conflicts_can_be_resolved_in_ui if defined?(@conflicts_can_be_resolved_in_ui)
 | 
			
		||||
 | 
			
		||||
    return @conflicts_can_be_resolved_in_ui = false unless cannot_be_merged?
 | 
			
		||||
    return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs?
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      @conflicts_can_be_resolved_in_ui = conflicts.files.each(&:lines)
 | 
			
		||||
    rescue Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
 | 
			
		||||
      @conflicts_can_be_resolved_in_ui = false
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,6 +62,8 @@ class Project < ActiveRecord::Base
 | 
			
		|||
  belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
 | 
			
		||||
  belongs_to :namespace
 | 
			
		||||
 | 
			
		||||
  has_one :board, dependent: :destroy
 | 
			
		||||
 | 
			
		||||
  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
 | 
			
		||||
 | 
			
		||||
  # Project services
 | 
			
		||||
| 
						 | 
				
			
			@ -197,6 +199,8 @@ class Project < ActiveRecord::Base
 | 
			
		|||
  scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
 | 
			
		||||
  scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
 | 
			
		||||
 | 
			
		||||
  scope :excluding_project, ->(project) { where.not(id: project) }
 | 
			
		||||
 | 
			
		||||
  state_machine :import_status, initial: :none do
 | 
			
		||||
    event :import_start do
 | 
			
		||||
      transition [:none, :finished] => :started
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -391,6 +391,8 @@ class Repository
 | 
			
		|||
    expire_exists_cache
 | 
			
		||||
    expire_root_ref_cache
 | 
			
		||||
    expire_emptiness_caches
 | 
			
		||||
 | 
			
		||||
    repository_event(:create_repository)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Runs code just before a repository is deleted.
 | 
			
		||||
| 
						 | 
				
			
			@ -407,6 +409,8 @@ class Repository
 | 
			
		|||
    expire_root_ref_cache
 | 
			
		||||
    expire_emptiness_caches
 | 
			
		||||
    expire_exists_cache
 | 
			
		||||
 | 
			
		||||
    repository_event(:remove_repository)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Runs code just before the HEAD of a repository is changed.
 | 
			
		||||
| 
						 | 
				
			
			@ -414,6 +418,8 @@ class Repository
 | 
			
		|||
    # Cached divergent commit counts are based on repository head
 | 
			
		||||
    expire_branch_cache
 | 
			
		||||
    expire_root_ref_cache
 | 
			
		||||
 | 
			
		||||
    repository_event(:change_default_branch)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Runs code before pushing (= creating or removing) a tag.
 | 
			
		||||
| 
						 | 
				
			
			@ -421,12 +427,16 @@ class Repository
 | 
			
		|||
    expire_cache
 | 
			
		||||
    expire_tags_cache
 | 
			
		||||
    expire_tag_count_cache
 | 
			
		||||
 | 
			
		||||
    repository_event(:push_tag)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Runs code before removing a tag.
 | 
			
		||||
  def before_remove_tag
 | 
			
		||||
    expire_tags_cache
 | 
			
		||||
    expire_tag_count_cache
 | 
			
		||||
 | 
			
		||||
    repository_event(:remove_tag)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def before_import
 | 
			
		||||
| 
						 | 
				
			
			@ -443,6 +453,8 @@ class Repository
 | 
			
		|||
  # Runs code after a new commit has been pushed.
 | 
			
		||||
  def after_push_commit(branch_name, revision)
 | 
			
		||||
    expire_cache(branch_name, revision)
 | 
			
		||||
 | 
			
		||||
    repository_event(:push_commit, branch: branch_name)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Runs code after a new branch has been created.
 | 
			
		||||
| 
						 | 
				
			
			@ -450,11 +462,15 @@ class Repository
 | 
			
		|||
    expire_branches_cache
 | 
			
		||||
    expire_has_visible_content_cache
 | 
			
		||||
    expire_branch_count_cache
 | 
			
		||||
 | 
			
		||||
    repository_event(:push_branch)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Runs code before removing an existing branch.
 | 
			
		||||
  def before_remove_branch
 | 
			
		||||
    expire_branches_cache
 | 
			
		||||
 | 
			
		||||
    repository_event(:remove_branch)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Runs code after an existing branch has been removed.
 | 
			
		||||
| 
						 | 
				
			
			@ -869,6 +885,14 @@ class Repository
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def resolve_conflicts(user, branch, params)
 | 
			
		||||
    commit_with_hooks(user, branch) do
 | 
			
		||||
      committer = user_to_committer(user)
 | 
			
		||||
 | 
			
		||||
      Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def check_revert_content(commit, base_branch)
 | 
			
		||||
    source_sha = find_branch(base_branch).target.sha
 | 
			
		||||
    args       = [commit.id, source_sha]
 | 
			
		||||
| 
						 | 
				
			
			@ -1059,4 +1083,8 @@ class Repository
 | 
			
		|||
  def keep_around_ref_name(sha)
 | 
			
		||||
    "refs/keep-around/#{sha}"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def repository_event(event, tags = {})
 | 
			
		||||
    Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -429,6 +429,13 @@ class User < ActiveRecord::Base
 | 
			
		|||
                    owned_groups.select(:id), namespace.id).joins(:namespace)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Returns projects which user can admin issues on (for example to move an issue to that project).
 | 
			
		||||
  #
 | 
			
		||||
  # This logic is duplicated from `Ability#project_abilities` into a SQL form.
 | 
			
		||||
  def projects_where_can_admin_issues
 | 
			
		||||
    authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def is_admin?
 | 
			
		||||
    admin
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
module Boards
 | 
			
		||||
  class BaseService < ::BaseService
 | 
			
		||||
    delegate :board, to: :project
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
module Boards
 | 
			
		||||
  class CreateService < Boards::BaseService
 | 
			
		||||
    def execute
 | 
			
		||||
      create_board! unless project.board.present?
 | 
			
		||||
      project.board
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def create_board!
 | 
			
		||||
      project.create_board
 | 
			
		||||
      project.board.lists.create(list_type: :backlog)
 | 
			
		||||
      project.board.lists.create(list_type: :done)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
module Boards
 | 
			
		||||
  module Issues
 | 
			
		||||
    class ListService < Boards::BaseService
 | 
			
		||||
      def execute
 | 
			
		||||
        issues = IssuesFinder.new(current_user, filter_params).execute
 | 
			
		||||
        issues = without_board_labels(issues) unless list.movable?
 | 
			
		||||
        issues = with_list_label(issues) if list.movable?
 | 
			
		||||
        issues
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def list
 | 
			
		||||
        @list ||= board.lists.find(params[:id])
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def filter_params
 | 
			
		||||
        set_default_scope
 | 
			
		||||
        set_default_sort
 | 
			
		||||
        set_project
 | 
			
		||||
        set_state
 | 
			
		||||
 | 
			
		||||
        params
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def set_default_scope
 | 
			
		||||
        params[:scope] = 'all'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def set_default_sort
 | 
			
		||||
        params[:sort] = 'priority'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def set_project
 | 
			
		||||
        params[:project_id] = project.id
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def set_state
 | 
			
		||||
        params[:state] = list.done? ? 'closed' : 'opened'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def board_label_ids
 | 
			
		||||
        @board_label_ids ||= board.lists.movable.pluck(:label_id)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def without_board_labels(issues)
 | 
			
		||||
        return issues unless board_label_ids.any?
 | 
			
		||||
 | 
			
		||||
        issues.where.not(
 | 
			
		||||
          LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
 | 
			
		||||
                   .where(label_id: board_label_ids).limit(1).arel.exists
 | 
			
		||||
        )
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def with_list_label(issues)
 | 
			
		||||
        issues.where(
 | 
			
		||||
          LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
 | 
			
		||||
                   .where("label_links.label_id = ?", list.label_id).limit(1).arel.exists
 | 
			
		||||
        )
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
module Boards
 | 
			
		||||
  module Issues
 | 
			
		||||
    class MoveService < Boards::BaseService
 | 
			
		||||
      def execute(issue)
 | 
			
		||||
        return false unless can?(current_user, :update_issue, issue)
 | 
			
		||||
        return false unless valid_move?
 | 
			
		||||
 | 
			
		||||
        update_service.execute(issue)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def valid_move?
 | 
			
		||||
        moving_from_list.present? && moving_to_list.present? &&
 | 
			
		||||
          moving_from_list != moving_to_list
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def moving_from_list
 | 
			
		||||
        @moving_from_list ||= board.lists.find_by(id: params[:from_list_id])
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def moving_to_list
 | 
			
		||||
        @moving_to_list ||= board.lists.find_by(id: params[:to_list_id])
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def update_service
 | 
			
		||||
        ::Issues::UpdateService.new(project, current_user, issue_params)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def issue_params
 | 
			
		||||
        {
 | 
			
		||||
          add_label_ids: add_label_ids,
 | 
			
		||||
          remove_label_ids: remove_label_ids,
 | 
			
		||||
          state_event: issue_state
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def issue_state
 | 
			
		||||
        return 'reopen' if moving_from_list.done?
 | 
			
		||||
        return 'close'  if moving_to_list.done?
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def add_label_ids
 | 
			
		||||
        [moving_to_list.label_id].compact
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def remove_label_ids
 | 
			
		||||
        label_ids =
 | 
			
		||||
          if moving_to_list.movable?
 | 
			
		||||
            moving_from_list.label_id
 | 
			
		||||
          else
 | 
			
		||||
            board.lists.movable.pluck(:label_id)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
        Array(label_ids).compact
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
module Boards
 | 
			
		||||
  module Lists
 | 
			
		||||
    class CreateService < Boards::BaseService
 | 
			
		||||
      def execute
 | 
			
		||||
        List.transaction do
 | 
			
		||||
          create_list_at(next_position)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def next_position
 | 
			
		||||
        max_position = board.lists.movable.maximum(:position)
 | 
			
		||||
        max_position.nil? ? 0 : max_position.succ
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def create_list_at(position)
 | 
			
		||||
        board.lists.create(params.merge(list_type: :label, position: position))
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
module Boards
 | 
			
		||||
  module Lists
 | 
			
		||||
    class DestroyService < Boards::BaseService
 | 
			
		||||
      def execute(list)
 | 
			
		||||
        return false unless list.destroyable?
 | 
			
		||||
 | 
			
		||||
        list.with_lock do
 | 
			
		||||
          decrement_higher_lists(list)
 | 
			
		||||
          remove_list(list)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def decrement_higher_lists(list)
 | 
			
		||||
        board.lists.movable.where('position > ?',  list.position)
 | 
			
		||||
                   .update_all('position = position - 1')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def remove_list(list)
 | 
			
		||||
        list.destroy
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
module Boards
 | 
			
		||||
  module Lists
 | 
			
		||||
    class GenerateService < Boards::BaseService
 | 
			
		||||
      def execute
 | 
			
		||||
        return false unless board.lists.movable.empty?
 | 
			
		||||
 | 
			
		||||
        List.transaction do
 | 
			
		||||
          label_params.each { |params| create_list(params) }
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def create_list(params)
 | 
			
		||||
        label = find_or_create_label(params)
 | 
			
		||||
        Lists::CreateService.new(project, current_user, label_id: label.id).execute
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def find_or_create_label(params)
 | 
			
		||||
        project.labels.create_with(color: params[:color])
 | 
			
		||||
                      .find_or_create_by(name: params[:name])
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def label_params
 | 
			
		||||
        [
 | 
			
		||||
          { name: 'Development', color: '#5CB85C' },
 | 
			
		||||
          { name: 'Testing',     color: '#F0AD4E' },
 | 
			
		||||
          { name: 'Production',  color: '#FF5F00' },
 | 
			
		||||
          { name: 'Ready',       color: '#FF0000' }
 | 
			
		||||
        ]
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
module Boards
 | 
			
		||||
  module Lists
 | 
			
		||||
    class MoveService < Boards::BaseService
 | 
			
		||||
      def execute(list)
 | 
			
		||||
        @old_position = list.position
 | 
			
		||||
        @new_position = params[:position]
 | 
			
		||||
 | 
			
		||||
        return false unless list.movable?
 | 
			
		||||
        return false unless valid_move?
 | 
			
		||||
 | 
			
		||||
        list.with_lock do
 | 
			
		||||
          reorder_intermediate_lists
 | 
			
		||||
          update_list_position(list)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      attr_reader :old_position, :new_position
 | 
			
		||||
 | 
			
		||||
      def valid_move?
 | 
			
		||||
        new_position.present? && new_position != old_position &&
 | 
			
		||||
          new_position >= 0 && new_position < board.lists.movable.size
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def reorder_intermediate_lists
 | 
			
		||||
        if old_position < new_position
 | 
			
		||||
          decrement_intermediate_lists
 | 
			
		||||
        else
 | 
			
		||||
          increment_intermediate_lists
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def decrement_intermediate_lists
 | 
			
		||||
        board.lists.movable.where('position > ?',  old_position)
 | 
			
		||||
                           .where('position <= ?', new_position)
 | 
			
		||||
                           .update_all('position = position - 1')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def increment_intermediate_lists
 | 
			
		||||
        board.lists.movable.where('position >= ?', new_position)
 | 
			
		||||
                           .where('position < ?',  old_position)
 | 
			
		||||
                           .update_all('position = position + 1')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def update_list_position(list)
 | 
			
		||||
        list.update_attribute(:position, new_position)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -104,11 +104,12 @@ class IssuableBaseService < BaseService
 | 
			
		|||
    change_subscription(issuable)
 | 
			
		||||
    filter_params
 | 
			
		||||
    old_labels = issuable.labels.to_a
 | 
			
		||||
    old_mentioned_users = issuable.mentioned_users.to_a
 | 
			
		||||
 | 
			
		||||
    if params.present? && update_issuable(issuable, params)
 | 
			
		||||
      issuable.reset_events_cache
 | 
			
		||||
      handle_common_system_notes(issuable, old_labels: old_labels)
 | 
			
		||||
      handle_changes(issuable, old_labels: old_labels)
 | 
			
		||||
      handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
 | 
			
		||||
      issuable.create_new_cross_references!(current_user)
 | 
			
		||||
      execute_hooks(issuable, 'update')
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ module Issues
 | 
			
		|||
      update(issue)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def handle_changes(issue, old_labels: [])
 | 
			
		||||
    def handle_changes(issue, old_labels: [], old_mentioned_users: [])
 | 
			
		||||
      if has_changes?(issue, old_labels: old_labels)
 | 
			
		||||
        todo_service.mark_pending_todos_as_done(issue, current_user)
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +32,11 @@ module Issues
 | 
			
		|||
      if added_labels.present?
 | 
			
		||||
        notification_service.relabeled_issue(issue, added_labels, current_user)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      added_mentions = issue.mentioned_users - old_mentioned_users
 | 
			
		||||
      if added_mentions.present?
 | 
			
		||||
        notification_service.new_mentions_in_issue(issue, added_mentions, current_user)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def reopen_service
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
module MergeRequests
 | 
			
		||||
  class ResolveService < MergeRequests::BaseService
 | 
			
		||||
    attr_accessor :conflicts, :rugged, :merge_index
 | 
			
		||||
 | 
			
		||||
    def execute(merge_request)
 | 
			
		||||
      @conflicts = merge_request.conflicts
 | 
			
		||||
      @rugged = project.repository.rugged
 | 
			
		||||
      @merge_index = conflicts.merge_index
 | 
			
		||||
 | 
			
		||||
      conflicts.files.each do |file|
 | 
			
		||||
        write_resolved_file_to_index(file, params[:sections])
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      commit_params = {
 | 
			
		||||
        message: params[:commit_message] || conflicts.default_commit_message,
 | 
			
		||||
        parents: [conflicts.our_commit, conflicts.their_commit].map(&:oid),
 | 
			
		||||
        tree: merge_index.write_tree(rugged)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def write_resolved_file_to_index(file, resolutions)
 | 
			
		||||
      new_file = file.resolve_lines(resolutions).map(&:text).join("\n")
 | 
			
		||||
      our_path = file.our_path
 | 
			
		||||
 | 
			
		||||
      merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
 | 
			
		||||
      merge_index.conflict_remove(our_path)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ module MergeRequests
 | 
			
		|||
      update(merge_request)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def handle_changes(merge_request, old_labels: [])
 | 
			
		||||
    def handle_changes(merge_request, old_labels: [], old_mentioned_users: [])
 | 
			
		||||
      if has_changes?(merge_request, old_labels: old_labels)
 | 
			
		||||
        todo_service.mark_pending_todos_as_done(merge_request, current_user)
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +55,15 @@ module MergeRequests
 | 
			
		|||
          current_user
 | 
			
		||||
        )
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      added_mentions = merge_request.mentioned_users - old_mentioned_users
 | 
			
		||||
      if added_mentions.present?
 | 
			
		||||
        notification_service.new_mentions_in_merge_request(
 | 
			
		||||
          merge_request,
 | 
			
		||||
          added_mentions,
 | 
			
		||||
          current_user
 | 
			
		||||
        )
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def reopen_service
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,20 @@ class NotificationService
 | 
			
		|||
    new_resource_email(issue, issue.project, :new_issue_email)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # When issue text is updated, we should send an email to:
 | 
			
		||||
  #
 | 
			
		||||
  #  * newly mentioned project team members with notification level higher than Participating
 | 
			
		||||
  #
 | 
			
		||||
  def new_mentions_in_issue(issue, new_mentioned_users, current_user)
 | 
			
		||||
    new_mentions_in_resource_email(
 | 
			
		||||
      issue,
 | 
			
		||||
      issue.project,
 | 
			
		||||
      new_mentioned_users,
 | 
			
		||||
      current_user,
 | 
			
		||||
      :new_mention_in_issue_email
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # When we close an issue we should send an email to:
 | 
			
		||||
  #
 | 
			
		||||
  #  * issue author if their notification level is not Disabled
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +89,20 @@ class NotificationService
 | 
			
		|||
    new_resource_email(merge_request, merge_request.target_project, :new_merge_request_email)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # When merge request text is updated, we should send an email to:
 | 
			
		||||
  #
 | 
			
		||||
  #  * newly mentioned project team members with notification level higher than Participating
 | 
			
		||||
  #
 | 
			
		||||
  def new_mentions_in_merge_request(merge_request, new_mentioned_users, current_user)
 | 
			
		||||
    new_mentions_in_resource_email(
 | 
			
		||||
      merge_request,
 | 
			
		||||
      merge_request.target_project,
 | 
			
		||||
      new_mentioned_users,
 | 
			
		||||
      current_user,
 | 
			
		||||
      :new_mention_in_merge_request_email
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # When we reassign a merge_request we should send an email to:
 | 
			
		||||
  #
 | 
			
		||||
  #  * merge_request old assignee if their notification level is not Disabled
 | 
			
		||||
| 
						 | 
				
			
			@ -177,7 +205,7 @@ class NotificationService
 | 
			
		|||
 | 
			
		||||
    # build notify method like 'note_commit_email'
 | 
			
		||||
    notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    recipients.each do |recipient|
 | 
			
		||||
      mailer.send(notify_method, recipient.id, note.id).deliver_later
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -471,6 +499,15 @@ class NotificationService
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
 | 
			
		||||
    recipients = build_recipients(target, project, current_user, action: "new")
 | 
			
		||||
    recipients = recipients & new_mentioned_users
 | 
			
		||||
 | 
			
		||||
    recipients.each do |recipient|
 | 
			
		||||
      mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def close_resource_email(target, project, current_user, method)
 | 
			
		||||
    action = method == :merged_merge_request_email ? "merge" : "close"
 | 
			
		||||
    recipients = build_recipients(target, project, current_user, action: action)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,6 @@
 | 
			
		|||
    = render "layouts/broadcast"
 | 
			
		||||
    = render "layouts/flash"
 | 
			
		||||
    = yield :flash_message
 | 
			
		||||
    %div{ class: (container_class unless @no_container) }
 | 
			
		||||
    %div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
 | 
			
		||||
      .content
 | 
			
		||||
        .clearfix
 | 
			
		||||
          = yield
 | 
			
		||||
        = yield
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
!!! 5
 | 
			
		||||
%html{ lang: "en"}
 | 
			
		||||
%html{ lang: "en", class: "#{page_class}" }
 | 
			
		||||
  = render "layouts/head"
 | 
			
		||||
  %body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}}
 | 
			
		||||
    = Gon::Base.render_data
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
%p
 | 
			
		||||
  You have been mentioned in an issue.
 | 
			
		||||
 | 
			
		||||
- if current_application_settings.email_author_in_body
 | 
			
		||||
  %div
 | 
			
		||||
    #{link_to @issue.author_name, user_url(@issue.author)} wrote:
 | 
			
		||||
-if @issue.description
 | 
			
		||||
  = markdown(@issue.description, pipeline: :email, author: @issue.author)
 | 
			
		||||
 | 
			
		||||
- if @issue.assignee_id.present?
 | 
			
		||||
  %p
 | 
			
		||||
    Assignee: #{@issue.assignee_name}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
You have been mentioned in an issue.
 | 
			
		||||
 | 
			
		||||
Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
 | 
			
		||||
Author:    <%= @issue.author_name %>
 | 
			
		||||
Assignee:  <%= @issue.assignee_name %>
 | 
			
		||||
 | 
			
		||||
<%= @issue.description %>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
%p
 | 
			
		||||
  You have been mentioned in Merge Request #{@merge_request.to_reference}
 | 
			
		||||
 | 
			
		||||
- if current_application_settings.email_author_in_body
 | 
			
		||||
  %div
 | 
			
		||||
    #{link_to @merge_request.author_name, user_url(@merge_request.author)} wrote:
 | 
			
		||||
%p.details
 | 
			
		||||
  != merge_path_description(@merge_request, '→')
 | 
			
		||||
 | 
			
		||||
- if @merge_request.assignee_id.present?
 | 
			
		||||
  %p
 | 
			
		||||
    Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name}
 | 
			
		||||
 | 
			
		||||
-if @merge_request.description
 | 
			
		||||
  = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
You have been mentioned in Merge Request <%= @merge_request.to_reference %>
 | 
			
		||||
 | 
			
		||||
<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %>
 | 
			
		||||
 | 
			
		||||
<%= merge_path_description(@merge_request, 'to') %>
 | 
			
		||||
Author:    <%= @merge_request.author_name %>
 | 
			
		||||
Assignee:  <%= @merge_request.assignee_name %>
 | 
			
		||||
 | 
			
		||||
<%= @merge_request.description %>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
%board-blank-state{ "inline-template" => true,
 | 
			
		||||
  "v-if" => "list.id == 'blank'" }
 | 
			
		||||
  .board-blank-state
 | 
			
		||||
    %p
 | 
			
		||||
      Add the following default lists to your Issue Board with one click:
 | 
			
		||||
    %ul.board-blank-state-list
 | 
			
		||||
      %li{ "v-for" => "label in predefinedLabels" }
 | 
			
		||||
        %span.label-color{ ":style" =>  "{ backgroundColor: label.color } " }
 | 
			
		||||
        {{ label.title }}
 | 
			
		||||
    %p
 | 
			
		||||
      Starting out with the default set of lists will get you right on the way to making the most of your board.
 | 
			
		||||
    %button.btn.btn-create.btn-inverted.btn-block{ type: "button", "@click.stop" => "addDefaultLists" }
 | 
			
		||||
      Add default lists
 | 
			
		||||
    %button.btn.btn-default.btn-block{ type: "button", "@click.stop" => "clearBlankState" }
 | 
			
		||||
      Nevermind, I'll use my own
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
%board{ "inline-template" => true,
 | 
			
		||||
  "v-cloak" => true,
 | 
			
		||||
  "v-for" => "list in state.lists | orderBy 'position'",
 | 
			
		||||
  "v-ref:board" => true,
 | 
			
		||||
  ":list" => "list",
 | 
			
		||||
  ":disabled" => "disabled",
 | 
			
		||||
  ":issue-link-base" => "issueLinkBase",
 | 
			
		||||
  "track-by" => "_uid" }
 | 
			
		||||
  .board{ ":class" => "{ 'is-draggable': !list.preset }",
 | 
			
		||||
    ":data-id" => "list.id" }
 | 
			
		||||
    .board-inner
 | 
			
		||||
      %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
 | 
			
		||||
        %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
 | 
			
		||||
          = icon("align-justify", class: "board-mobile-handle js-board-drag-handle", "v-if" => "(!disabled && !list.preset)")
 | 
			
		||||
          {{ list.title }}
 | 
			
		||||
          %span.pull-right{ "v-if" => "list.type !== 'blank'" }
 | 
			
		||||
            {{ list.issues.length }}
 | 
			
		||||
          - if can?(current_user, :admin_list, @project)
 | 
			
		||||
            %board-delete{ "inline-template" => true,
 | 
			
		||||
              ":list" => "list",
 | 
			
		||||
              "v-if" => "!list.preset && list.id" }
 | 
			
		||||
              %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
 | 
			
		||||
                = icon("trash")
 | 
			
		||||
          = icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore")
 | 
			
		||||
      .board-inner-container.board-search-container{ "v-if" => "list.canSearch()" }
 | 
			
		||||
        %input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" }
 | 
			
		||||
        = icon("search", class: "board-search-icon", "v-show" => "!query")
 | 
			
		||||
        %button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" }
 | 
			
		||||
          = icon("times", class: "board-search-clear")
 | 
			
		||||
      %board-list{ "inline-template" => true,
 | 
			
		||||
        "v-if" => "list.type !== 'blank'",
 | 
			
		||||
        ":list" => "list",
 | 
			
		||||
        ":issues" => "list.issues",
 | 
			
		||||
        ":loading" => "list.loading",
 | 
			
		||||
        ":disabled" => "disabled",
 | 
			
		||||
        ":issue-link-base" => "issueLinkBase" }
 | 
			
		||||
        .board-list-loading.text-center{ "v-if" => "loading" }
 | 
			
		||||
          = icon("spinner spin")
 | 
			
		||||
        %ul.board-list{ "v-el:list" => true,
 | 
			
		||||
          "v-show" => "!loading",
 | 
			
		||||
          ":data-board" => "list.id" }
 | 
			
		||||
          = render "projects/boards/components/card"
 | 
			
		||||
      - if can?(current_user, :admin_list, @project)
 | 
			
		||||
        = render "projects/boards/components/blank_state"
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
%board-card{ "inline-template" => true,
 | 
			
		||||
  "v-for" => "issue in issues | orderBy 'priority'",
 | 
			
		||||
  "v-ref:issue" => true,
 | 
			
		||||
  ":index" => "$index",
 | 
			
		||||
  ":list" => "list",
 | 
			
		||||
  ":issue" => "issue",
 | 
			
		||||
  ":issue-link-base" => "issueLinkBase",
 | 
			
		||||
  ":disabled" => "disabled",
 | 
			
		||||
  "track-by" => "id" }
 | 
			
		||||
  %li.card{ ":class" => "{ 'user-can-drag': !disabled }",
 | 
			
		||||
    ":index" => "index" }
 | 
			
		||||
    = icon("align-justify", class: "board-mobile-handle js-card-drag-handle", "v-if" => "!disabled")
 | 
			
		||||
    %h4.card-title
 | 
			
		||||
      = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
 | 
			
		||||
      %a{ ":href" => "issueLinkBase + '/' + issue.id",
 | 
			
		||||
        ":title" => "issue.title" }
 | 
			
		||||
        {{ issue.title }}
 | 
			
		||||
    .card-footer
 | 
			
		||||
      %span.card-number
 | 
			
		||||
        = precede '#' do
 | 
			
		||||
          {{ issue.id }}
 | 
			
		||||
      %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
 | 
			
		||||
        type: "button",
 | 
			
		||||
        "v-if" => "(!list.label || label.id !== list.label.id)",
 | 
			
		||||
        "@click" => "filterByLabel(label, $event)",
 | 
			
		||||
        ":style" => "{ backgroundColor: label.color, color: label.textColor }",
 | 
			
		||||
        ":title" => "label.description",
 | 
			
		||||
        data: { container: 'body' } }
 | 
			
		||||
        {{ label.title }}
 | 
			
		||||
      %a.has-tooltip{ ":href" => "'/u/' + issue.assignee.username",
 | 
			
		||||
        ":title" => "'Assigned to ' + issue.assignee.name",
 | 
			
		||||
        "v-if" => "issue.assignee",
 | 
			
		||||
        data: { container: 'body' } }
 | 
			
		||||
        %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
- @no_container = true
 | 
			
		||||
- @content_class = "issue-boards-content"
 | 
			
		||||
- page_title "Boards"
 | 
			
		||||
 | 
			
		||||
- content_for :page_specific_javascripts do
 | 
			
		||||
  = page_specific_javascript_tag('boards/boards_bundle.js')
 | 
			
		||||
  = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
 | 
			
		||||
 | 
			
		||||
= render "projects/issues/head"
 | 
			
		||||
 | 
			
		||||
= render 'shared/issuable/filter', type: :boards
 | 
			
		||||
 | 
			
		||||
.boards-list#board-app{ "v-cloak" => true,
 | 
			
		||||
  "data-endpoint" => "#{namespace_project_board_path(@project.namespace, @project)}",
 | 
			
		||||
  "data-disabled" => "#{!can?(current_user, :admin_list, @project)}",
 | 
			
		||||
  "data-issue-link-base" => "#{namespace_project_issues_path(@project.namespace, @project)}" }
 | 
			
		||||
  .boards-app-loading.text-center{ "v-if" => "loading" }
 | 
			
		||||
    = icon("spinner spin")
 | 
			
		||||
  = render "projects/boards/components/board"
 | 
			
		||||
| 
						 | 
				
			
			@ -11,98 +11,133 @@
 | 
			
		|||
      %p.build-detail-row
 | 
			
		||||
        #{@build.coverage}%
 | 
			
		||||
 | 
			
		||||
  - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
 | 
			
		||||
    .block{ class: ("block-first" if !@build.coverage) }
 | 
			
		||||
  - builds = @build.pipeline.builds.latest.to_a
 | 
			
		||||
  - statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
 | 
			
		||||
  - if builds.size > 1
 | 
			
		||||
    .dropdown.build-dropdown
 | 
			
		||||
      .build-light-text Stage
 | 
			
		||||
      %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
 | 
			
		||||
        %span.stage-selection More
 | 
			
		||||
        = icon('caret-down')
 | 
			
		||||
      %ul.dropdown-menu
 | 
			
		||||
        - builds.map(&:stage).uniq.each do |stage|
 | 
			
		||||
          %li
 | 
			
		||||
            %a.stage-item= stage
 | 
			
		||||
 | 
			
		||||
    .builds-container
 | 
			
		||||
      - statuses.each do |build_status|
 | 
			
		||||
        - builds.select{|build| build.status == build_status}.each do |build|
 | 
			
		||||
          .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
 | 
			
		||||
            = link_to namespace_project_build_path(@project.namespace, @project, build) do
 | 
			
		||||
              = icon('check')
 | 
			
		||||
              = ci_icon_for_status(build.status)
 | 
			
		||||
              %span
 | 
			
		||||
                - if build.name
 | 
			
		||||
                  = build.name
 | 
			
		||||
                - else
 | 
			
		||||
                  = build.id
 | 
			
		||||
 | 
			
		||||
        - if @build.retried?
 | 
			
		||||
          %li.active
 | 
			
		||||
            %a
 | 
			
		||||
              Build ##{@build.id}
 | 
			
		||||
              ·
 | 
			
		||||
              %i.fa.fa-warning
 | 
			
		||||
              This build was retried.
 | 
			
		||||
 | 
			
		||||
  .blocks-container
 | 
			
		||||
    - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
 | 
			
		||||
      .block{ class: ("block-first" if !@build.coverage) }
 | 
			
		||||
        .title
 | 
			
		||||
          Build artifacts
 | 
			
		||||
        - if @build.artifacts_expired?
 | 
			
		||||
          %p.build-detail-row
 | 
			
		||||
            The artifacts were removed
 | 
			
		||||
            #{time_ago_with_tooltip(@build.artifacts_expire_at)}
 | 
			
		||||
        - elsif @build.artifacts_expire_at
 | 
			
		||||
          %p.build-detail-row
 | 
			
		||||
            The artifacts will be removed in
 | 
			
		||||
            %span.js-artifacts-remove= @build.artifacts_expire_at
 | 
			
		||||
 | 
			
		||||
        - if @build.artifacts?
 | 
			
		||||
          .btn-group.btn-group-justified{ role: :group }
 | 
			
		||||
            - if @build.artifacts_expire_at
 | 
			
		||||
              = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
 | 
			
		||||
                Keep
 | 
			
		||||
 | 
			
		||||
            = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
 | 
			
		||||
              Download
 | 
			
		||||
 | 
			
		||||
            - if @build.artifacts_metadata?
 | 
			
		||||
              = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
 | 
			
		||||
                Browse
 | 
			
		||||
 | 
			
		||||
    .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
 | 
			
		||||
      .title
 | 
			
		||||
        Build artifacts
 | 
			
		||||
      - if @build.artifacts_expired?
 | 
			
		||||
        Build details
 | 
			
		||||
        - if can?(current_user, :update_build, @build) && @build.retryable?
 | 
			
		||||
          = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
 | 
			
		||||
      - if @build.merge_request
 | 
			
		||||
        %p.build-detail-row
 | 
			
		||||
          The artifacts were removed
 | 
			
		||||
          #{time_ago_with_tooltip(@build.artifacts_expire_at)}
 | 
			
		||||
      - elsif @build.artifacts_expire_at
 | 
			
		||||
          %span.build-light-text Merge Request:
 | 
			
		||||
          = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
 | 
			
		||||
      - if @build.duration
 | 
			
		||||
        %p.build-detail-row
 | 
			
		||||
          The artifacts will be removed in
 | 
			
		||||
          %span.js-artifacts-remove= @build.artifacts_expire_at
 | 
			
		||||
 | 
			
		||||
      - if @build.artifacts?
 | 
			
		||||
        .btn-group.btn-group-justified{ role: :group }
 | 
			
		||||
          - if @build.artifacts_expire_at
 | 
			
		||||
            = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
 | 
			
		||||
              Keep
 | 
			
		||||
 | 
			
		||||
          = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
 | 
			
		||||
            Download
 | 
			
		||||
 | 
			
		||||
          - if @build.artifacts_metadata?
 | 
			
		||||
            = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
 | 
			
		||||
              Browse
 | 
			
		||||
 | 
			
		||||
  .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
 | 
			
		||||
    .title
 | 
			
		||||
      Build details
 | 
			
		||||
      - if can?(current_user, :update_build, @build) && @build.retryable?
 | 
			
		||||
        = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
 | 
			
		||||
    - if @build.merge_request
 | 
			
		||||
          %span.build-light-text Duration:
 | 
			
		||||
          = time_interval_in_words(@build.duration)
 | 
			
		||||
      - if @build.finished_at
 | 
			
		||||
        %p.build-detail-row
 | 
			
		||||
          %span.build-light-text Finished:
 | 
			
		||||
          #{time_ago_with_tooltip(@build.finished_at)}
 | 
			
		||||
      - if @build.erased_at
 | 
			
		||||
        %p.build-detail-row
 | 
			
		||||
          %span.build-light-text Erased:
 | 
			
		||||
          #{time_ago_with_tooltip(@build.erased_at)}
 | 
			
		||||
      %p.build-detail-row
 | 
			
		||||
        %span.build-light-text Merge Request:
 | 
			
		||||
        = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
 | 
			
		||||
    - if @build.duration
 | 
			
		||||
      %p.build-detail-row
 | 
			
		||||
        %span.build-light-text Duration:
 | 
			
		||||
        = time_interval_in_words(@build.duration)
 | 
			
		||||
    - if @build.finished_at
 | 
			
		||||
      %p.build-detail-row
 | 
			
		||||
        %span.build-light-text Finished:
 | 
			
		||||
        #{time_ago_with_tooltip(@build.finished_at)}
 | 
			
		||||
    - if @build.erased_at
 | 
			
		||||
      %p.build-detail-row
 | 
			
		||||
        %span.build-light-text Erased:
 | 
			
		||||
        #{time_ago_with_tooltip(@build.erased_at)}
 | 
			
		||||
    %p.build-detail-row
 | 
			
		||||
      %span.build-light-text Runner:
 | 
			
		||||
      - if @build.runner && current_user && current_user.admin
 | 
			
		||||
        = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
 | 
			
		||||
      - elsif @build.runner
 | 
			
		||||
        \##{@build.runner.id}
 | 
			
		||||
    .btn-group.btn-group-justified{ role: :group }
 | 
			
		||||
      - if @build.has_trace?
 | 
			
		||||
        = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
 | 
			
		||||
      - if @build.active?
 | 
			
		||||
        = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
 | 
			
		||||
      - if can?(current_user, :update_build, @project) && @build.erasable?
 | 
			
		||||
        = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
 | 
			
		||||
                  class: "btn btn-sm btn-default", method: :post,
 | 
			
		||||
                  data: { confirm: "Are you sure you want to erase this build?" } do
 | 
			
		||||
          Erase
 | 
			
		||||
        %span.build-light-text Runner:
 | 
			
		||||
        - if @build.runner && current_user && current_user.admin
 | 
			
		||||
          = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
 | 
			
		||||
        - elsif @build.runner
 | 
			
		||||
          \##{@build.runner.id}
 | 
			
		||||
      .btn-group.btn-group-justified{ role: :group }
 | 
			
		||||
        - if @build.has_trace?
 | 
			
		||||
          = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
 | 
			
		||||
        - if @build.active?
 | 
			
		||||
          = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
 | 
			
		||||
        - if can?(current_user, :update_build, @project) && @build.erasable?
 | 
			
		||||
          = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
 | 
			
		||||
                    class: "btn btn-sm btn-default", method: :post,
 | 
			
		||||
                    data: { confirm: "Are you sure you want to erase this build?" } do
 | 
			
		||||
            Erase
 | 
			
		||||
 | 
			
		||||
  - if @build.trigger_request
 | 
			
		||||
    .build-widget
 | 
			
		||||
      %h4.title
 | 
			
		||||
        Trigger
 | 
			
		||||
    - if @build.trigger_request
 | 
			
		||||
      .build-widget
 | 
			
		||||
        %h4.title
 | 
			
		||||
          Trigger
 | 
			
		||||
 | 
			
		||||
      %p
 | 
			
		||||
        %span.build-light-text Token:
 | 
			
		||||
        #{@build.trigger_request.trigger.short_token}
 | 
			
		||||
 | 
			
		||||
      - if @build.trigger_request.variables
 | 
			
		||||
        %p
 | 
			
		||||
          %span.build-light-text Variables:
 | 
			
		||||
          %span.build-light-text Token:
 | 
			
		||||
          #{@build.trigger_request.trigger.short_token}
 | 
			
		||||
 | 
			
		||||
        - if @build.trigger_request.variables
 | 
			
		||||
          %p
 | 
			
		||||
            %span.build-light-text Variables:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        - @build.trigger_request.variables.each do |key, value|
 | 
			
		||||
          %code
 | 
			
		||||
            #{key}=#{value}
 | 
			
		||||
          - @build.trigger_request.variables.each do |key, value|
 | 
			
		||||
            %code
 | 
			
		||||
              #{key}=#{value}
 | 
			
		||||
 | 
			
		||||
  .block
 | 
			
		||||
    .title
 | 
			
		||||
      Commit title
 | 
			
		||||
    %p.build-light-text.append-bottom-0
 | 
			
		||||
      #{@build.pipeline.git_commit_title}
 | 
			
		||||
 | 
			
		||||
  - if @build.tags.any?
 | 
			
		||||
    .block
 | 
			
		||||
      .title
 | 
			
		||||
        Tags
 | 
			
		||||
      - @build.tag_list.each do |tag|
 | 
			
		||||
        %span.label.label-primary
 | 
			
		||||
          = tag
 | 
			
		||||
        Commit title
 | 
			
		||||
      %p.build-light-text.append-bottom-0
 | 
			
		||||
        #{@build.pipeline.git_commit_title}
 | 
			
		||||
 | 
			
		||||
    - if @build.tags.any?
 | 
			
		||||
      .block
 | 
			
		||||
        .title
 | 
			
		||||
          Tags
 | 
			
		||||
        - @build.tag_list.each do |tag|
 | 
			
		||||
          %span.label.label-primary
 | 
			
		||||
            = tag
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,26 +5,6 @@
 | 
			
		|||
.build-page
 | 
			
		||||
  = render "header"
 | 
			
		||||
 | 
			
		||||
  - builds = @build.pipeline.builds.latest.to_a
 | 
			
		||||
  - if builds.size > 1
 | 
			
		||||
    %ul.nav-links.no-top.no-bottom
 | 
			
		||||
      - builds.each do |build|
 | 
			
		||||
        %li{class: ('active' if build == @build) }
 | 
			
		||||
          = link_to namespace_project_build_path(@project.namespace, @project, build) do
 | 
			
		||||
            = ci_icon_for_status(build.status)
 | 
			
		||||
            %span
 | 
			
		||||
              - if build.name
 | 
			
		||||
                = build.name
 | 
			
		||||
              - else
 | 
			
		||||
                = build.id
 | 
			
		||||
 | 
			
		||||
      - if @build.retried?
 | 
			
		||||
        %li.active
 | 
			
		||||
          %a
 | 
			
		||||
            Build ##{@build.id}
 | 
			
		||||
            ·
 | 
			
		||||
            %i.fa.fa-warning
 | 
			
		||||
            This build was retried.
 | 
			
		||||
  - if @build.stuck?
 | 
			
		||||
    - unless @build.any_runners_online?
 | 
			
		||||
      .bs-callout.bs-callout-warning
 | 
			
		||||
| 
						 | 
				
			
			@ -67,4 +47,10 @@
 | 
			
		|||
= render "sidebar"
 | 
			
		||||
 | 
			
		||||
:javascript
 | 
			
		||||
  new Build("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}")
 | 
			
		||||
  new Build({
 | 
			
		||||
    page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}",
 | 
			
		||||
    build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}",
 | 
			
		||||
    build_status: "#{@build.status}",
 | 
			
		||||
    build_stage: "#{@build.stage}",
 | 
			
		||||
    state1: "#{trace_with_state[:state]}"
 | 
			
		||||
  })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
- if current_user
 | 
			
		||||
  = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do
 | 
			
		||||
    - if current_user.starred?(@project)
 | 
			
		||||
      = icon('star fw')
 | 
			
		||||
      = icon('star')
 | 
			
		||||
      %span.starred Unstar
 | 
			
		||||
    - else
 | 
			
		||||
      = icon('star-o fw')
 | 
			
		||||
      = icon('star-o')
 | 
			
		||||
      %span Star
 | 
			
		||||
  %div.count-with-arrow
 | 
			
		||||
    %span.arrow
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +13,7 @@
 | 
			
		|||
 | 
			
		||||
- else
 | 
			
		||||
  = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do
 | 
			
		||||
    = icon('star fw')
 | 
			
		||||
    = icon('star')
 | 
			
		||||
    Star
 | 
			
		||||
  %div.count-with-arrow
 | 
			
		||||
    %span.arrow
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
 | 
			
		||||
%li.build{class: ("playable" if is_playable)}
 | 
			
		||||
  .build-content
 | 
			
		||||
    - if is_playable
 | 
			
		||||
      = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
 | 
			
		||||
        = render_status_with_link('build', 'play')
 | 
			
		||||
        = subject.name
 | 
			
		||||
    - elsif can?(current_user, :read_build, @project)
 | 
			
		||||
      = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
 | 
			
		||||
        = render_status_with_link('build', subject.status)
 | 
			
		||||
        = subject.name
 | 
			
		||||
    - else
 | 
			
		||||
      = render_status_with_link('build', subject.status)
 | 
			
		||||
      = ci_icon_for_status(subject.status)
 | 
			
		||||
| 
						 | 
				
			
			@ -2,19 +2,21 @@
 | 
			
		|||
%tr.commit
 | 
			
		||||
  %td.commit-link
 | 
			
		||||
    = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do
 | 
			
		||||
      = ci_status_with_icon(status)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      - if defined?(status_icon_only) && status_icon_only
 | 
			
		||||
        = ci_icon_for_status(status)
 | 
			
		||||
      - else
 | 
			
		||||
        = ci_status_with_icon(status)
 | 
			
		||||
  %td
 | 
			
		||||
    .branch-commit
 | 
			
		||||
      = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do
 | 
			
		||||
        %span ##{pipeline.id}
 | 
			
		||||
      - if pipeline.ref
 | 
			
		||||
        .icon-container
 | 
			
		||||
          = pipeline.tag? ? icon('tag') : icon('code-fork')
 | 
			
		||||
        = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name"
 | 
			
		||||
        .icon-container
 | 
			
		||||
          = custom_icon("icon_commit")
 | 
			
		||||
        - unless defined?(hide_branch) && hide_branch
 | 
			
		||||
          .icon-container
 | 
			
		||||
            = pipeline.tag? ? icon('tag') : icon('code-fork')
 | 
			
		||||
          = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name"
 | 
			
		||||
      .icon-container
 | 
			
		||||
        = custom_icon("icon_commit")
 | 
			
		||||
      = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace"
 | 
			
		||||
      - if pipeline.latest?
 | 
			
		||||
        %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +55,7 @@
 | 
			
		|||
    - if pipeline.finished_at
 | 
			
		||||
      %p.finished-at
 | 
			
		||||
        = icon("calendar")
 | 
			
		||||
        #{time_ago_with_tooltip(pipeline.finished_at, short_format: true, skip_js: true)}
 | 
			
		||||
        #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
 | 
			
		||||
 | 
			
		||||
  %td.pipeline-actions
 | 
			
		||||
    .controls.hidden-xs.pull-right
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,9 @@
 | 
			
		|||
.row-content-block.build-content.middle-block
 | 
			
		||||
.row-content-block.build-content.middle-block.pipeline-actions
 | 
			
		||||
  .pull-right
 | 
			
		||||
    .btn.btn-grouped.btn-white.toggle-pipeline-btn
 | 
			
		||||
      %span.toggle-btn-text Hide
 | 
			
		||||
      %span pipeline graph
 | 
			
		||||
      %span.caret
 | 
			
		||||
    - if can?(current_user, :update_pipeline, pipeline.project)
 | 
			
		||||
      - if pipeline.builds.latest.failed.any?(&:retryable?)
 | 
			
		||||
        = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +27,22 @@
 | 
			
		|||
        in
 | 
			
		||||
        = time_interval_in_words pipeline.duration
 | 
			
		||||
 | 
			
		||||
.row-content-block.build-content.middle-block.pipeline-graph
 | 
			
		||||
  .pipeline-visualization
 | 
			
		||||
    %ul.stage-column-list
 | 
			
		||||
      - stages = pipeline.stages_with_latest_statuses
 | 
			
		||||
      - stages.each do |stage, statuses|
 | 
			
		||||
        %li.stage-column
 | 
			
		||||
          .stage-name
 | 
			
		||||
            %a{name: stage}
 | 
			
		||||
            - if stage
 | 
			
		||||
              = stage.titleize
 | 
			
		||||
          .builds-container
 | 
			
		||||
            %ul
 | 
			
		||||
              - statuses.each do |status|
 | 
			
		||||
                = render "projects/#{status.to_partial_path}_pipeline", subject: status
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
- if pipeline.yaml_errors.present?
 | 
			
		||||
  .bs-callout.bs-callout-danger
 | 
			
		||||
    %h4 Found errors in your .gitlab-ci.yml:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue