Merge branch 'master' into 22643-manual-job-page
* master: (200 commits)
  Fix issue boards scroll config.
  Added multi editor setting on the profile preferences page
  fix missing import of timeWeek which would cause errors in prometheus graphs with deployments
  Remove downcase from special path helper
  Rename db:seed_fu-{pg,mysql} to gitlab:setup-{pg,mysql}
  Fix error when viewing diffs without blobs
  Moves prettier to dev dependency
  Eager load event target authors whenever possible
  Do not run ee_compat_check on security branches
  Include integration tests in CE/EE testing documentation
  41054-Disallow creation of new Kubernetes integrations
  Resolve "Resizable file list and commit panel"
  Make tooltip placement bottom by default as per design guidelines
  Fix groups list icon, timestamp alignment and row height
  Avoid leaving a push event empty if payload cannot be created
  Move git operations for UpdateRemoteMirrorService into Gitlab::Git
  Move delete_remote_branches from Gitlab::Shell to Gitlab::Git::Repository
  Move push_remote_branches from Gitlab::Shell to Gitlab::Git::Repository
  Update Kubernetes service documentation
  fix issue #37843
  ...
			
			
This commit is contained in:
		
						commit
						a29a91f02d
					
				| 
						 | 
				
			
			@ -431,6 +431,7 @@ ee_compat_check:
 | 
			
		|||
    - master
 | 
			
		||||
    - tags
 | 
			
		||||
    - /^[\d-]+-stable(-ee)?/
 | 
			
		||||
    - /^security-/
 | 
			
		||||
    - branches@gitlab-org/gitlab-ee
 | 
			
		||||
    - branches@gitlab/gitlab-ee
 | 
			
		||||
  retry: 0
 | 
			
		||||
| 
						 | 
				
			
			@ -508,7 +509,7 @@ db:rollback-mysql:
 | 
			
		|||
  <<: *db-rollback
 | 
			
		||||
  <<: *use-mysql
 | 
			
		||||
 | 
			
		||||
.db-seed_fu: &db-seed_fu
 | 
			
		||||
.gitlab-setup: &gitlab-setup
 | 
			
		||||
  <<: *dedicated-runner
 | 
			
		||||
  <<: *except-docs-and-qa
 | 
			
		||||
  <<: *pull-cache
 | 
			
		||||
| 
						 | 
				
			
			@ -517,22 +518,24 @@ db:rollback-mysql:
 | 
			
		|||
    SIZE: "1"
 | 
			
		||||
    SETUP_DB: "false"
 | 
			
		||||
    CREATE_DB_USER: "true"
 | 
			
		||||
    FIXTURE_PATH: db/fixtures/development
 | 
			
		||||
  script:
 | 
			
		||||
    - git clone https://gitlab.com/gitlab-org/gitlab-test.git
 | 
			
		||||
       /home/git/repositories/gitlab-org/gitlab-test.git
 | 
			
		||||
    - bundle exec rake db:setup db:seed_fu
 | 
			
		||||
    - scripts/gitaly-test-spawn
 | 
			
		||||
    - force=yes bundle exec rake gitlab:setup
 | 
			
		||||
  artifacts:
 | 
			
		||||
    when: on_failure
 | 
			
		||||
    expire_in: 1d
 | 
			
		||||
    paths:
 | 
			
		||||
      - log/development.log
 | 
			
		||||
 | 
			
		||||
db:seed_fu-pg:
 | 
			
		||||
  <<: *db-seed_fu
 | 
			
		||||
gitlab:setup-pg:
 | 
			
		||||
  <<: *gitlab-setup
 | 
			
		||||
  <<: *use-pg
 | 
			
		||||
 | 
			
		||||
db:seed_fu-mysql:
 | 
			
		||||
  <<: *db-seed_fu
 | 
			
		||||
gitlab:setup-mysql:
 | 
			
		||||
  <<: *gitlab-setup
 | 
			
		||||
  <<: *use-mysql
 | 
			
		||||
 | 
			
		||||
# Frontend-related jobs
 | 
			
		||||
| 
						 | 
				
			
			@ -600,6 +603,14 @@ codequality:
 | 
			
		|||
  artifacts:
 | 
			
		||||
    paths: [codeclimate.json]
 | 
			
		||||
 | 
			
		||||
sast:
 | 
			
		||||
  image: registry.gitlab.com/gitlab-org/gl-sast:latest
 | 
			
		||||
  before_script: []
 | 
			
		||||
  script:
 | 
			
		||||
    - /app/bin/run .
 | 
			
		||||
  artifacts:
 | 
			
		||||
    paths: [gl-sast-report.json]
 | 
			
		||||
 | 
			
		||||
qa:internal:
 | 
			
		||||
  <<: *dedicated-runner
 | 
			
		||||
  <<: *except-docs
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ inherit_gem:
 | 
			
		|||
    - rubocop-default.yml
 | 
			
		||||
 | 
			
		||||
inherit_from: .rubocop_todo.yml
 | 
			
		||||
require: ./rubocop/rubocop
 | 
			
		||||
 | 
			
		||||
AllCops:
 | 
			
		||||
  TargetRailsVersion: 4.2
 | 
			
		||||
| 
						 | 
				
			
			@ -24,8 +25,10 @@ Gitlab/ModuleWithInstanceVariables:
 | 
			
		|||
  Exclude:
 | 
			
		||||
    # We ignore Rails helpers right now because it's hard to workaround it
 | 
			
		||||
    - app/helpers/**/*_helper.rb
 | 
			
		||||
    - ee/app/helpers/**/*_helper.rb
 | 
			
		||||
    # We ignore Rails mailers right now because it's hard to workaround it
 | 
			
		||||
    - app/mailers/emails/**/*.rb
 | 
			
		||||
    - ee/**/emails/**/*.rb
 | 
			
		||||
    # We ignore spec helpers because it usually doesn't matter
 | 
			
		||||
    - spec/support/**/*.rb
 | 
			
		||||
    - features/steps/**/*.rb
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										29
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										29
									
								
								CHANGELOG.md
								
								
								
								
							| 
						 | 
				
			
			@ -2,6 +2,35 @@
 | 
			
		|||
documentation](doc/development/changelog.md) for instructions on adding your own
 | 
			
		||||
entry.
 | 
			
		||||
 | 
			
		||||
## 10.3.3 (2018-01-02)
 | 
			
		||||
 | 
			
		||||
### Fixed (3 changes)
 | 
			
		||||
 | 
			
		||||
- Fix links to old commits in merge request comments.
 | 
			
		||||
- Fix 404 errors after a user edits an issue description and solves the reCAPTCHA.
 | 
			
		||||
- Gracefully handle orphaned write deploy keys in /internal/post_receive.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## 10.3.2 (2017-12-28)
 | 
			
		||||
 | 
			
		||||
### Fixed (1 change)
 | 
			
		||||
 | 
			
		||||
- Fix migration for removing orphaned issues.moved_to_id values in MySQL and PostgreSQL.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## 10.3.1 (2017-12-27)
 | 
			
		||||
 | 
			
		||||
### Fixed (3 changes)
 | 
			
		||||
 | 
			
		||||
- Don't link LFS objects to a project when unlinking forks when they were already linked. !16006
 | 
			
		||||
- Execute project hooks and services after commit when moving an issue.
 | 
			
		||||
- Fix Error 500s with anonymous clones for a project that has moved.
 | 
			
		||||
 | 
			
		||||
### Changed (1 change)
 | 
			
		||||
 | 
			
		||||
- Reduce the number of buckets in gitlab_cache_operation_duration_seconds metric. !15881
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## 10.3.0 (2017-12-22)
 | 
			
		||||
 | 
			
		||||
### Security (1 change, 1 of them is from the community)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -553,7 +553,7 @@ the feature you contribute through all of these steps.
 | 
			
		|||
 | 
			
		||||
1. Description explaining the relevancy (see following item)
 | 
			
		||||
1. Working and clean code that is commented where needed
 | 
			
		||||
1. [Unit and system tests][testing] that pass on the CI server
 | 
			
		||||
1. [Unit, integration, and system tests][testing] that pass on the CI server
 | 
			
		||||
1. Performance/scalability implications have been considered, addressed, and tested
 | 
			
		||||
1. [Documented][doc-styleguide] in the `/doc` directory
 | 
			
		||||
1. [Changelog entry added][changelog], if necessary
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
0.60.0
 | 
			
		||||
0.65.0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										6
									
								
								Gemfile
								
								
								
								
							| 
						 | 
				
			
			@ -12,7 +12,7 @@ gem 'sprockets', '~> 3.7.0'
 | 
			
		|||
gem 'default_value_for', '~> 3.0.0'
 | 
			
		||||
 | 
			
		||||
# Supported DBs
 | 
			
		||||
gem 'mysql2', '~> 0.4.5', group: :mysql
 | 
			
		||||
gem 'mysql2', '~> 0.4.10', group: :mysql
 | 
			
		||||
gem 'pg', '~> 0.18.2', group: :postgres
 | 
			
		||||
 | 
			
		||||
gem 'rugged', '~> 0.26.0'
 | 
			
		||||
| 
						 | 
				
			
			@ -283,7 +283,7 @@ group :metrics do
 | 
			
		|||
  gem 'influxdb', '~> 0.2', require: false
 | 
			
		||||
 | 
			
		||||
  # Prometheus
 | 
			
		||||
  gem 'prometheus-client-mmap', '~> 0.7.0.beta43'
 | 
			
		||||
  gem 'prometheus-client-mmap', '~> 0.7.0.beta44'
 | 
			
		||||
  gem 'raindrops', '~> 0.18'
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -402,7 +402,7 @@ group :ed25519 do
 | 
			
		|||
end
 | 
			
		||||
 | 
			
		||||
# Gitaly GRPC client
 | 
			
		||||
gem 'gitaly-proto', '~> 0.61.0', require: 'gitaly'
 | 
			
		||||
gem 'gitaly-proto', '~> 0.64.0', require: 'gitaly'
 | 
			
		||||
 | 
			
		||||
gem 'toml-rb', '~> 0.3.15', require: false
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								Gemfile.lock
								
								
								
								
							
							
						
						
									
										18
									
								
								Gemfile.lock
								
								
								
								
							| 
						 | 
				
			
			@ -284,7 +284,7 @@ GEM
 | 
			
		|||
      po_to_json (>= 1.0.0)
 | 
			
		||||
      rails (>= 3.2.0)
 | 
			
		||||
    gherkin-ruby (0.3.2)
 | 
			
		||||
    gitaly-proto (0.61.0)
 | 
			
		||||
    gitaly-proto (0.64.0)
 | 
			
		||||
      google-protobuf (~> 3.1)
 | 
			
		||||
      grpc (~> 1.0)
 | 
			
		||||
    github-linguist (4.7.6)
 | 
			
		||||
| 
						 | 
				
			
			@ -505,7 +505,7 @@ GEM
 | 
			
		|||
    mustermann (1.0.0)
 | 
			
		||||
    mustermann-grape (1.0.0)
 | 
			
		||||
      mustermann (~> 1.0.0)
 | 
			
		||||
    mysql2 (0.4.5)
 | 
			
		||||
    mysql2 (0.4.10)
 | 
			
		||||
    net-ldap (0.16.0)
 | 
			
		||||
    net-ssh (4.1.0)
 | 
			
		||||
    netrc (0.11.0)
 | 
			
		||||
| 
						 | 
				
			
			@ -634,7 +634,7 @@ GEM
 | 
			
		|||
      parser
 | 
			
		||||
      unparser
 | 
			
		||||
    procto (0.0.3)
 | 
			
		||||
    prometheus-client-mmap (0.7.0.beta43)
 | 
			
		||||
    prometheus-client-mmap (0.7.0.beta44)
 | 
			
		||||
    pry (0.10.4)
 | 
			
		||||
      coderay (~> 1.1.0)
 | 
			
		||||
      method_source (~> 0.8.1)
 | 
			
		||||
| 
						 | 
				
			
			@ -708,7 +708,7 @@ GEM
 | 
			
		|||
      json
 | 
			
		||||
    recursive-open-struct (1.0.0)
 | 
			
		||||
    redcarpet (3.4.0)
 | 
			
		||||
    redis (3.3.3)
 | 
			
		||||
    redis (3.3.5)
 | 
			
		||||
    redis-actionpack (5.0.2)
 | 
			
		||||
      actionpack (>= 4.0, < 6)
 | 
			
		||||
      redis-rack (>= 1, < 3)
 | 
			
		||||
| 
						 | 
				
			
			@ -839,11 +839,11 @@ GEM
 | 
			
		|||
      rack
 | 
			
		||||
    shoulda-matchers (3.1.2)
 | 
			
		||||
      activesupport (>= 4.0.0)
 | 
			
		||||
    sidekiq (5.0.4)
 | 
			
		||||
    sidekiq (5.0.5)
 | 
			
		||||
      concurrent-ruby (~> 1.0)
 | 
			
		||||
      connection_pool (~> 2.2, >= 2.2.0)
 | 
			
		||||
      rack-protection (>= 1.5.0)
 | 
			
		||||
      redis (~> 3.3, >= 3.3.3)
 | 
			
		||||
      redis (>= 3.3.4, < 5)
 | 
			
		||||
    sidekiq-cron (0.6.0)
 | 
			
		||||
      rufus-scheduler (>= 3.3.0)
 | 
			
		||||
      sidekiq (>= 4.2.1)
 | 
			
		||||
| 
						 | 
				
			
			@ -1046,7 +1046,7 @@ DEPENDENCIES
 | 
			
		|||
  gettext (~> 3.2.2)
 | 
			
		||||
  gettext_i18n_rails (~> 1.8.0)
 | 
			
		||||
  gettext_i18n_rails_js (~> 1.2.0)
 | 
			
		||||
  gitaly-proto (~> 0.61.0)
 | 
			
		||||
  gitaly-proto (~> 0.64.0)
 | 
			
		||||
  github-linguist (~> 4.7.0)
 | 
			
		||||
  gitlab-flowdock-git-hook (~> 1.0.1)
 | 
			
		||||
  gitlab-markup (~> 1.6.2)
 | 
			
		||||
| 
						 | 
				
			
			@ -1087,7 +1087,7 @@ DEPENDENCIES
 | 
			
		|||
  method_source (~> 0.8)
 | 
			
		||||
  minitest (~> 5.7.0)
 | 
			
		||||
  mousetrap-rails (~> 1.4.6)
 | 
			
		||||
  mysql2 (~> 0.4.5)
 | 
			
		||||
  mysql2 (~> 0.4.10)
 | 
			
		||||
  net-ldap
 | 
			
		||||
  net-ssh (~> 4.1.0)
 | 
			
		||||
  nokogiri (~> 1.8.1)
 | 
			
		||||
| 
						 | 
				
			
			@ -1122,7 +1122,7 @@ DEPENDENCIES
 | 
			
		|||
  peek-sidekiq (~> 1.0.3)
 | 
			
		||||
  pg (~> 0.18.2)
 | 
			
		||||
  premailer-rails (~> 1.9.7)
 | 
			
		||||
  prometheus-client-mmap (~> 0.7.0.beta43)
 | 
			
		||||
  prometheus-client-mmap (~> 0.7.0.beta44)
 | 
			
		||||
  pry-byebug (~> 3.4.1)
 | 
			
		||||
  pry-rails (~> 0.3.4)
 | 
			
		||||
  rack-attack (~> 4.4.1)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 388 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.8 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 5.3 KiB  | 
| 
						 | 
				
			
			@ -1,10 +1,8 @@
 | 
			
		|||
/* eslint-disable no-new */
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import VueResource from 'vue-resource';
 | 
			
		||||
import axios from '../../lib/utils/axios_utils';
 | 
			
		||||
import notebookLab from '../../notebook/index.vue';
 | 
			
		||||
 | 
			
		||||
Vue.use(VueResource);
 | 
			
		||||
 | 
			
		||||
export default () => {
 | 
			
		||||
  const el = document.getElementById('js-notebook-viewer');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,14 +48,14 @@ export default () => {
 | 
			
		|||
    `,
 | 
			
		||||
    methods: {
 | 
			
		||||
      loadFile() {
 | 
			
		||||
        this.$http.get(el.dataset.endpoint)
 | 
			
		||||
          .then(response => response.json())
 | 
			
		||||
          .then((res) => {
 | 
			
		||||
            this.json = res;
 | 
			
		||||
        axios.get(el.dataset.endpoint)
 | 
			
		||||
          .then(res => res.data)
 | 
			
		||||
          .then((data) => {
 | 
			
		||||
            this.json = data;
 | 
			
		||||
            this.loading = false;
 | 
			
		||||
          })
 | 
			
		||||
          .catch((e) => {
 | 
			
		||||
            if (e.status) {
 | 
			
		||||
            if (e.status !== 200) {
 | 
			
		||||
              this.loadError = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@
 | 
			
		|||
 | 
			
		||||
import _ from 'underscore';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import VueResource from 'vue-resource';
 | 
			
		||||
import Flash from '../flash';
 | 
			
		||||
import { __ } from '../locale';
 | 
			
		||||
import FilteredSearchBoards from './filtered_search_boards';
 | 
			
		||||
| 
						 | 
				
			
			@ -25,8 +24,6 @@ import './components/new_list_dropdown';
 | 
			
		|||
import './components/modal/index';
 | 
			
		||||
import '../vue_shared/vue_resource_interceptor';
 | 
			
		||||
 | 
			
		||||
Vue.use(VueResource);
 | 
			
		||||
 | 
			
		||||
$(() => {
 | 
			
		||||
  const $boardApp = document.getElementById('board-app');
 | 
			
		||||
  const Store = gl.issueBoards.BoardsStore;
 | 
			
		||||
| 
						 | 
				
			
			@ -95,14 +92,13 @@ $(() => {
 | 
			
		|||
 | 
			
		||||
      Store.disabled = this.disabled;
 | 
			
		||||
      gl.boardService.all()
 | 
			
		||||
        .then(response => response.json())
 | 
			
		||||
        .then((resp) => {
 | 
			
		||||
          resp.forEach((board) => {
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          data.forEach((board) => {
 | 
			
		||||
            const list = Store.addList(board, this.defaultAvatar);
 | 
			
		||||
 | 
			
		||||
            if (list.type === 'closed') {
 | 
			
		||||
              list.position = Infinity;
 | 
			
		||||
              list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
 | 
			
		||||
            } else if (list.type === 'backlog') {
 | 
			
		||||
              list.position = -1;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -113,7 +109,9 @@ $(() => {
 | 
			
		|||
          Store.addBlankState();
 | 
			
		||||
          this.loading = false;
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => new Flash('An error occurred. Please try again.'));
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
          Flash('An error occurred while fetching the board lists. Please try again.');
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
      updateTokens() {
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +122,7 @@ $(() => {
 | 
			
		|||
        if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
 | 
			
		||||
          newIssue.setFetchingState('subscriptions', true);
 | 
			
		||||
          BoardService.getIssueInfo(sidebarInfoEndpoint)
 | 
			
		||||
            .then(res => res.json())
 | 
			
		||||
            .then(res => res.data)
 | 
			
		||||
            .then((data) => {
 | 
			
		||||
              newIssue.setFetchingState('subscriptions', false);
 | 
			
		||||
              newIssue.updateData({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,7 +65,7 @@ export default {
 | 
			
		|||
 | 
			
		||||
      // Save the labels
 | 
			
		||||
      gl.boardService.generateDefaultLists()
 | 
			
		||||
        .then(resp => resp.json())
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          data.forEach((listObj) => {
 | 
			
		||||
            const list = Store.findList('title', listObj.title);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -115,7 +115,7 @@ export default {
 | 
			
		|||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    const options = gl.issueBoards.getBoardSortableDefaultOptions({
 | 
			
		||||
      scroll: document.querySelectorAll('.boards-list')[0],
 | 
			
		||||
      scroll: true,
 | 
			
		||||
      group: 'issues',
 | 
			
		||||
      disabled: this.disabled,
 | 
			
		||||
      filter: '.board-list-count, .is-disabled',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,4 @@
 | 
			
		|||
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
 | 
			
		||||
/* global MilestoneSelect */
 | 
			
		||||
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import Flash from '../../flash';
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +11,7 @@ import './sidebar/remove_issue';
 | 
			
		|||
import IssuableContext from '../../issuable_context';
 | 
			
		||||
import LabelsSelect from '../../labels_select';
 | 
			
		||||
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
 | 
			
		||||
import MilestoneSelect from '../../milestone_select';
 | 
			
		||||
 | 
			
		||||
const Store = gl.issueBoards.BoardsStore;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -89,7 +89,7 @@ gl.issueBoards.IssuesModal = Vue.extend({
 | 
			
		|||
        page: this.page,
 | 
			
		||||
        per: this.perPage,
 | 
			
		||||
      }))
 | 
			
		||||
      .then(resp => resp.json())
 | 
			
		||||
      .then(res => res.data)
 | 
			
		||||
      .then((data) => {
 | 
			
		||||
        if (clearIssues) {
 | 
			
		||||
          this.issues = [];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,7 +40,7 @@ class List {
 | 
			
		|||
 | 
			
		||||
  save () {
 | 
			
		||||
    return gl.boardService.createList(this.label.id)
 | 
			
		||||
      .then(resp => resp.json())
 | 
			
		||||
      .then(res => res.data)
 | 
			
		||||
      .then((data) => {
 | 
			
		||||
        this.id = data.id;
 | 
			
		||||
        this.type = data.list_type;
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +90,7 @@ class List {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    return gl.boardService.getIssuesForList(this.id, data)
 | 
			
		||||
      .then(resp => resp.json())
 | 
			
		||||
      .then(res => res.data)
 | 
			
		||||
      .then((data) => {
 | 
			
		||||
        this.loading = false;
 | 
			
		||||
        this.issuesSize = data.size;
 | 
			
		||||
| 
						 | 
				
			
			@ -108,7 +108,7 @@ class List {
 | 
			
		|||
    this.issuesSize += 1;
 | 
			
		||||
 | 
			
		||||
    return gl.boardService.newIssue(this.id, issue)
 | 
			
		||||
      .then(resp => resp.json())
 | 
			
		||||
      .then(res => res.data)
 | 
			
		||||
      .then((data) => {
 | 
			
		||||
        issue.id = data.id;
 | 
			
		||||
        issue.iid = data.iid;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,82 +1,79 @@
 | 
			
		|||
/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
 | 
			
		||||
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import axios from '../../lib/utils/axios_utils';
 | 
			
		||||
import { mergeUrlParams } from '../../lib/utils/url_utility';
 | 
			
		||||
 | 
			
		||||
export default class BoardService {
 | 
			
		||||
  constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
 | 
			
		||||
    this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
 | 
			
		||||
      issues: {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`,
 | 
			
		||||
  constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
 | 
			
		||||
    this.boardsEndpoint = boardsEndpoint;
 | 
			
		||||
    this.boardId = boardId;
 | 
			
		||||
    this.listsEndpoint = listsEndpoint;
 | 
			
		||||
    this.listsEndpointGenerate = `${listsEndpoint}/generate.json`;
 | 
			
		||||
    this.bulkUpdatePath = bulkUpdatePath;
 | 
			
		||||
  }
 | 
			
		||||
    });
 | 
			
		||||
    this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
 | 
			
		||||
      generate: {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        url: `${listsEndpoint}/generate.json`
 | 
			
		||||
 | 
			
		||||
  generateBoardsPath(id) {
 | 
			
		||||
    return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`;
 | 
			
		||||
  }
 | 
			
		||||
    });
 | 
			
		||||
    this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {});
 | 
			
		||||
    this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
 | 
			
		||||
      bulkUpdate: {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        url: bulkUpdatePath,
 | 
			
		||||
 | 
			
		||||
  generateIssuesPath(id) {
 | 
			
		||||
    return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static generateIssuePath(boardId, id) {
 | 
			
		||||
    return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  all() {
 | 
			
		||||
    return axios.get(this.listsEndpoint);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generateDefaultLists() {
 | 
			
		||||
    return axios.post(this.listsEndpointGenerate, {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createList(labelId) {
 | 
			
		||||
    return axios.post(this.listsEndpoint, {
 | 
			
		||||
      list: {
 | 
			
		||||
        label_id: labelId,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  all () {
 | 
			
		||||
    return this.lists.get();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generateDefaultLists () {
 | 
			
		||||
    return this.lists.generate({});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createList (label_id) {
 | 
			
		||||
    return this.lists.save({}, {
 | 
			
		||||
  updateList(id, position) {
 | 
			
		||||
    return axios.put(`${this.listsEndpoint}/${id}`, {
 | 
			
		||||
      list: {
 | 
			
		||||
        label_id
 | 
			
		||||
      }
 | 
			
		||||
        position,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateList (id, position) {
 | 
			
		||||
    return this.lists.update({ id }, {
 | 
			
		||||
      list: {
 | 
			
		||||
        position
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  destroyList(id) {
 | 
			
		||||
    return axios.delete(`${this.listsEndpoint}/${id}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  destroyList (id) {
 | 
			
		||||
    return this.lists.delete({ id });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getIssuesForList (id, filter = {}) {
 | 
			
		||||
  getIssuesForList(id, filter = {}) {
 | 
			
		||||
    const data = { id };
 | 
			
		||||
    Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
 | 
			
		||||
 | 
			
		||||
    return this.issues.get(data);
 | 
			
		||||
    return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  moveIssue (id, from_list_id = null, to_list_id = null, move_before_id = null, move_after_id = null) {
 | 
			
		||||
    return this.issue.update({ id }, {
 | 
			
		||||
      from_list_id,
 | 
			
		||||
      to_list_id,
 | 
			
		||||
      move_before_id,
 | 
			
		||||
      move_after_id,
 | 
			
		||||
  moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
 | 
			
		||||
    return axios.put(BoardService.generateIssuePath(this.boardId, id), {
 | 
			
		||||
      from_list_id: fromListId,
 | 
			
		||||
      to_list_id: toListId,
 | 
			
		||||
      move_before_id: moveBeforeId,
 | 
			
		||||
      move_after_id: moveAfterId,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  newIssue (id, issue) {
 | 
			
		||||
    return this.issues.save({ id }, {
 | 
			
		||||
      issue
 | 
			
		||||
  newIssue(id, issue) {
 | 
			
		||||
    return axios.post(this.generateIssuesPath(id), {
 | 
			
		||||
      issue,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBacklog(data) {
 | 
			
		||||
    return this.boards.issues(data);
 | 
			
		||||
    return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bulkUpdate(issueIds, extraData = {}) {
 | 
			
		||||
| 
						 | 
				
			
			@ -86,15 +83,15 @@ export default class BoardService {
 | 
			
		|||
      }),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return this.issues.bulkUpdate(data);
 | 
			
		||||
    return axios.post(this.bulkUpdatePath, data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getIssueInfo(endpoint) {
 | 
			
		||||
    return Vue.http.get(endpoint);
 | 
			
		||||
    return axios.get(endpoint);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static toggleIssueSubscription(endpoint) {
 | 
			
		||||
    return Vue.http.post(endpoint);
 | 
			
		||||
    return axios.post(endpoint);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ import IssuableIndex from './issuable_index';
 | 
			
		|||
import Milestone from './milestone';
 | 
			
		||||
import IssuableForm from './issuable_form';
 | 
			
		||||
import LabelsSelect from './labels_select';
 | 
			
		||||
/* global MilestoneSelect */
 | 
			
		||||
import MilestoneSelect from './milestone_select';
 | 
			
		||||
import NewBranchForm from './new_branch_form';
 | 
			
		||||
import NotificationsForm from './notifications_form';
 | 
			
		||||
import notificationsDropdown from './notifications_dropdown';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -127,7 +127,7 @@ class FilteredSearchManager {
 | 
			
		|||
    this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
 | 
			
		||||
    this.checkForEnterWrapper = this.checkForEnter.bind(this);
 | 
			
		||||
    this.onClearSearchWrapper = this.onClearSearch.bind(this);
 | 
			
		||||
    this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
 | 
			
		||||
    this.checkForBackspaceWrapper = this.checkForBackspace.call(this);
 | 
			
		||||
    this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
 | 
			
		||||
    this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
 | 
			
		||||
    this.editTokenWrapper = this.editToken.bind(this);
 | 
			
		||||
| 
						 | 
				
			
			@ -180,22 +180,34 @@ class FilteredSearchManager {
 | 
			
		|||
    this.unbindStateEvents();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  checkForBackspace(e) {
 | 
			
		||||
  checkForBackspace() {
 | 
			
		||||
    let backspaceCount = 0;
 | 
			
		||||
 | 
			
		||||
    // closure for keeping track of the number of backspace keystrokes
 | 
			
		||||
    return (e) => {
 | 
			
		||||
      // 8 = Backspace Key
 | 
			
		||||
      // 46 = Delete Key
 | 
			
		||||
      if (e.keyCode === 8 || e.keyCode === 46) {
 | 
			
		||||
        const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
 | 
			
		||||
 | 
			
		||||
        const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
 | 
			
		||||
        const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
 | 
			
		||||
 | 
			
		||||
        if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
 | 
			
		||||
          backspaceCount += 1;
 | 
			
		||||
 | 
			
		||||
          if (backspaceCount === 2) {
 | 
			
		||||
            backspaceCount = 0;
 | 
			
		||||
            this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
 | 
			
		||||
            gl.FilteredSearchVisualTokens.removeLastTokenPartial();
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Reposition dropdown so that it is aligned with cursor
 | 
			
		||||
        this.dropdownManager.updateCurrentDropdownOffset();
 | 
			
		||||
      } else {
 | 
			
		||||
        backspaceCount = 0;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  checkForEnter(e) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,7 +77,8 @@ export default {
 | 
			
		|||
    class="group-row"
 | 
			
		||||
    >
 | 
			
		||||
    <div
 | 
			
		||||
      class="group-row-contents">
 | 
			
		||||
      class="group-row-contents"
 | 
			
		||||
      :class="{ 'project-row-contents': !isGroup }">
 | 
			
		||||
      <item-actions
 | 
			
		||||
        v-if="isGroup"
 | 
			
		||||
        :group="group"
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +98,7 @@ export default {
 | 
			
		|||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div
 | 
			
		||||
        class="avatar-container s40 hidden-xs"
 | 
			
		||||
        class="avatar-container prepend-top-8 prepend-left-5 s24 hidden-xs"
 | 
			
		||||
        :class="{ 'content-loading': group.isChildrenLoading }"
 | 
			
		||||
      >
 | 
			
		||||
        <a
 | 
			
		||||
| 
						 | 
				
			
			@ -106,11 +107,12 @@ export default {
 | 
			
		|||
        >
 | 
			
		||||
          <img
 | 
			
		||||
            v-if="hasAvatar"
 | 
			
		||||
            class="avatar s40"
 | 
			
		||||
            class="avatar s24"
 | 
			
		||||
            :src="group.avatarUrl"
 | 
			
		||||
          />
 | 
			
		||||
          <identicon
 | 
			
		||||
            v-else
 | 
			
		||||
            size-class="s24"
 | 
			
		||||
            :entity-id=group.id
 | 
			
		||||
            :entity-name="group.name"
 | 
			
		||||
          />
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +125,7 @@ export default {
 | 
			
		|||
          :href="group.relativePath"
 | 
			
		||||
          :title="group.fullName"
 | 
			
		||||
          class="no-expand"
 | 
			
		||||
          data-placement="top"
 | 
			
		||||
          data-placement="bottom"
 | 
			
		||||
        >{{
 | 
			
		||||
          // ending bracket must be by closing tag to prevent
 | 
			
		||||
          // link hover text-decoration from over-extending
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,14 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { s__ } from '../../locale';
 | 
			
		||||
import tooltip from '../../vue_shared/directives/tooltip';
 | 
			
		||||
import modal from '../../vue_shared/components/modal.vue';
 | 
			
		||||
import { s__ } from '~/locale';
 | 
			
		||||
import tooltip from '~/vue_shared/directives/tooltip';
 | 
			
		||||
import icon from '~/vue_shared/components/icon.vue';
 | 
			
		||||
import modal from '~/vue_shared/components/modal.vue';
 | 
			
		||||
import eventHub from '../event_hub';
 | 
			
		||||
import { COMMON_STR } from '../constants';
 | 
			
		||||
import Icon from '../../vue_shared/components/icon.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  components: {
 | 
			
		||||
    Icon,
 | 
			
		||||
    icon,
 | 
			
		||||
    modal,
 | 
			
		||||
  },
 | 
			
		||||
  directives: {
 | 
			
		||||
| 
						 | 
				
			
			@ -64,10 +64,9 @@ export default {
 | 
			
		|||
      :title="editBtnTitle"
 | 
			
		||||
      :aria-label="editBtnTitle"
 | 
			
		||||
      data-container="body"
 | 
			
		||||
      data-placement="bottom"
 | 
			
		||||
      class="edit-group btn no-expand">
 | 
			
		||||
      <icon
 | 
			
		||||
        name="settings">
 | 
			
		||||
      </icon>
 | 
			
		||||
      <icon name="settings"/>
 | 
			
		||||
    </a>
 | 
			
		||||
    <a
 | 
			
		||||
      v-tooltip
 | 
			
		||||
| 
						 | 
				
			
			@ -77,10 +76,9 @@ export default {
 | 
			
		|||
      :title="leaveBtnTitle"
 | 
			
		||||
      :aria-label="leaveBtnTitle"
 | 
			
		||||
      data-container="body"
 | 
			
		||||
      data-placement="bottom"
 | 
			
		||||
      class="leave-group btn no-expand">
 | 
			
		||||
      <i
 | 
			
		||||
        class="fa fa-sign-out"
 | 
			
		||||
        aria-hidden="true"/>
 | 
			
		||||
      <icon name="leave"/>
 | 
			
		||||
    </a>
 | 
			
		||||
    <modal
 | 
			
		||||
      v-show="modalStatus"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
<script>
 | 
			
		||||
import icon from '~/vue_shared/components/icon.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    isGroupOpen: {
 | 
			
		||||
| 
						 | 
				
			
			@ -7,9 +9,12 @@ export default {
 | 
			
		|||
      default: false,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  components: {
 | 
			
		||||
    icon,
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    iconClass() {
 | 
			
		||||
      return this.isGroupOpen ? 'fa-caret-down' : 'fa-caret-right';
 | 
			
		||||
      return this.isGroupOpen ? 'angle-down' : 'angle-right';
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -17,9 +22,9 @@ export default {
 | 
			
		|||
 | 
			
		||||
<template>
 | 
			
		||||
  <span class="folder-caret">
 | 
			
		||||
    <i
 | 
			
		||||
      :class="iconClass"
 | 
			
		||||
      class="fa"
 | 
			
		||||
      aria-hidden="true"/>
 | 
			
		||||
    <icon
 | 
			
		||||
      :size="12"
 | 
			
		||||
      :name="iconClass"
 | 
			
		||||
    />
 | 
			
		||||
  </span>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,14 @@
 | 
			
		|||
<script>
 | 
			
		||||
import tooltip from '../../vue_shared/directives/tooltip';
 | 
			
		||||
import icon from '~/vue_shared/components/icon.vue';
 | 
			
		||||
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
 | 
			
		||||
import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants';
 | 
			
		||||
import itemStatsValue from './item_stats_value.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  directives: {
 | 
			
		||||
    tooltip,
 | 
			
		||||
  components: {
 | 
			
		||||
    icon,
 | 
			
		||||
    timeAgoTooltip,
 | 
			
		||||
    itemStatsValue,
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    item: {
 | 
			
		||||
| 
						 | 
				
			
			@ -34,65 +38,47 @@ export default {
 | 
			
		|||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="stats">
 | 
			
		||||
    <span
 | 
			
		||||
      v-tooltip
 | 
			
		||||
    <item-stats-value
 | 
			
		||||
      v-if="isGroup"
 | 
			
		||||
      css-class="number-subgroups"
 | 
			
		||||
      icon-name="folder"
 | 
			
		||||
      :title="s__('Subgroups')"
 | 
			
		||||
      class="number-subgroups"
 | 
			
		||||
      data-placement="top"
 | 
			
		||||
      data-container="body">
 | 
			
		||||
      <i
 | 
			
		||||
        class="fa fa-folder"
 | 
			
		||||
        aria-hidden="true"
 | 
			
		||||
      :value=item.subgroupCount
 | 
			
		||||
    />
 | 
			
		||||
      {{item.subgroupCount}}
 | 
			
		||||
    </span>
 | 
			
		||||
    <span
 | 
			
		||||
      v-tooltip
 | 
			
		||||
    <item-stats-value
 | 
			
		||||
      v-if="isGroup"
 | 
			
		||||
      css-class="number-projects"
 | 
			
		||||
      icon-name="bookmark"
 | 
			
		||||
      :title="s__('Projects')"
 | 
			
		||||
      class="number-projects"
 | 
			
		||||
      data-placement="top"
 | 
			
		||||
      data-container="body">
 | 
			
		||||
      <i
 | 
			
		||||
        class="fa fa-bookmark"
 | 
			
		||||
        aria-hidden="true"
 | 
			
		||||
      :value=item.projectCount
 | 
			
		||||
    />
 | 
			
		||||
      {{item.projectCount}}
 | 
			
		||||
    </span>
 | 
			
		||||
    <span
 | 
			
		||||
      v-tooltip
 | 
			
		||||
    <item-stats-value
 | 
			
		||||
      v-if="isGroup"
 | 
			
		||||
      css-class="number-users"
 | 
			
		||||
      icon-name="users"
 | 
			
		||||
      :title="s__('Members')"
 | 
			
		||||
      class="number-users"
 | 
			
		||||
      data-placement="top"
 | 
			
		||||
      data-container="body">
 | 
			
		||||
      <i
 | 
			
		||||
        class="fa fa-users"
 | 
			
		||||
        aria-hidden="true"
 | 
			
		||||
      :value=item.memberCount
 | 
			
		||||
    />
 | 
			
		||||
      {{item.memberCount}}
 | 
			
		||||
    </span>
 | 
			
		||||
    <span
 | 
			
		||||
    <item-stats-value
 | 
			
		||||
      v-if="isProject"
 | 
			
		||||
      class="project-stars">
 | 
			
		||||
      <i
 | 
			
		||||
        class="fa fa-star"
 | 
			
		||||
        aria-hidden="true"
 | 
			
		||||
      css-class="project-stars"
 | 
			
		||||
      icon-name="star"
 | 
			
		||||
      :value=item.starCount
 | 
			
		||||
    />
 | 
			
		||||
      {{item.starCount}}
 | 
			
		||||
    </span>
 | 
			
		||||
    <span
 | 
			
		||||
      v-tooltip
 | 
			
		||||
    <item-stats-value
 | 
			
		||||
      css-class="item-visibility"
 | 
			
		||||
      tooltip-placement="left"
 | 
			
		||||
      :icon-name="visibilityIcon"
 | 
			
		||||
      :title="visibilityTooltip"
 | 
			
		||||
      data-placement="left"
 | 
			
		||||
      data-container="body"
 | 
			
		||||
      class="item-visibility">
 | 
			
		||||
      <i
 | 
			
		||||
        :class="visibilityIcon"
 | 
			
		||||
        class="fa"
 | 
			
		||||
        aria-hidden="true"
 | 
			
		||||
    />
 | 
			
		||||
    </span>
 | 
			
		||||
    <div
 | 
			
		||||
      class="last-updated"
 | 
			
		||||
      v-if="isProject"
 | 
			
		||||
    >
 | 
			
		||||
      <time-ago-tooltip
 | 
			
		||||
        tooltip-placement="bottom"
 | 
			
		||||
        :time="item.updatedAt"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,68 @@
 | 
			
		|||
<script>
 | 
			
		||||
import tooltip from '~/vue_shared/directives/tooltip';
 | 
			
		||||
import icon from '~/vue_shared/components/icon.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    title: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
    cssClass: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
    iconName: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    tooltipPlacement: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: 'bottom',
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * value could either be number or string
 | 
			
		||||
     * as `memberCount` is always passed as string
 | 
			
		||||
     * while `subgroupCount` & `projectCount`
 | 
			
		||||
     * are always number
 | 
			
		||||
     */
 | 
			
		||||
    value: {
 | 
			
		||||
      type: [Number, String],
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  directives: {
 | 
			
		||||
    tooltip,
 | 
			
		||||
  },
 | 
			
		||||
  components: {
 | 
			
		||||
    icon,
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    isValuePresent() {
 | 
			
		||||
      return this.value !== '';
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <span
 | 
			
		||||
    v-tooltip
 | 
			
		||||
    data-container="body"
 | 
			
		||||
    :data-placement="tooltipPlacement"
 | 
			
		||||
    :class="cssClass"
 | 
			
		||||
    :title="title"
 | 
			
		||||
  >
 | 
			
		||||
    <icon :name="iconName"/>
 | 
			
		||||
    <span
 | 
			
		||||
      v-if="isValuePresent"
 | 
			
		||||
      class="stat-value"
 | 
			
		||||
    >
 | 
			
		||||
      {{value}}
 | 
			
		||||
    </span>
 | 
			
		||||
  </span>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,11 @@
 | 
			
		|||
<script>
 | 
			
		||||
import icon from '~/vue_shared/components/icon.vue';
 | 
			
		||||
import { ITEM_TYPE } from '../constants';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  components: {
 | 
			
		||||
    icon,
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    itemType: {
 | 
			
		||||
      type: String,
 | 
			
		||||
| 
						 | 
				
			
			@ -16,9 +20,9 @@ export default {
 | 
			
		|||
  computed: {
 | 
			
		||||
    iconClass() {
 | 
			
		||||
      if (this.itemType === ITEM_TYPE.GROUP) {
 | 
			
		||||
        return this.isGroupOpen ? 'fa-folder-open' : 'fa-folder';
 | 
			
		||||
        return this.isGroupOpen ? 'folder-open' : 'folder';
 | 
			
		||||
      }
 | 
			
		||||
      return 'fa-bookmark';
 | 
			
		||||
      return 'bookmark';
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -26,9 +30,6 @@ export default {
 | 
			
		|||
 | 
			
		||||
<template>
 | 
			
		||||
  <span class="item-type-icon">
 | 
			
		||||
    <i
 | 
			
		||||
      :class="iconClass"
 | 
			
		||||
      class="fa"
 | 
			
		||||
      aria-hidden="true"/>
 | 
			
		||||
    <icon :name="iconClass"/>
 | 
			
		||||
  </span>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ export const PROJECT_VISIBILITY_TYPE = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export const VISIBILITY_TYPE_ICON = {
 | 
			
		||||
  public: 'fa-globe',
 | 
			
		||||
  internal: 'fa-shield',
 | 
			
		||||
  private: 'fa-lock',
 | 
			
		||||
  public: 'earth',
 | 
			
		||||
  internal: 'shield',
 | 
			
		||||
  private: 'lock',
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,6 +91,7 @@ export default class GroupsStore {
 | 
			
		|||
      subgroupCount: rawGroupItem.subgroup_count,
 | 
			
		||||
      memberCount: rawGroupItem.number_users_with_delimiter,
 | 
			
		||||
      starCount: rawGroupItem.star_count,
 | 
			
		||||
      updatedAt: rawGroupItem.updated_at,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,11 +2,18 @@
 | 
			
		|||
import { mapGetters, mapState, mapActions } from 'vuex';
 | 
			
		||||
import repoCommitSection from './repo_commit_section.vue';
 | 
			
		||||
import icon from '../../vue_shared/components/icon.vue';
 | 
			
		||||
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      width: 290,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  components: {
 | 
			
		||||
    repoCommitSection,
 | 
			
		||||
    icon,
 | 
			
		||||
    panelResizer,
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState([
 | 
			
		||||
| 
						 | 
				
			
			@ -18,10 +25,20 @@ export default {
 | 
			
		|||
    currentIcon() {
 | 
			
		||||
      return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
 | 
			
		||||
    },
 | 
			
		||||
    maxSize() {
 | 
			
		||||
      return window.innerWidth / 2;
 | 
			
		||||
    },
 | 
			
		||||
    panelStyle() {
 | 
			
		||||
      if (!this.rightPanelCollapsed) {
 | 
			
		||||
        return { width: `${this.width}px` };
 | 
			
		||||
      }
 | 
			
		||||
      return {};
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions([
 | 
			
		||||
      'setPanelCollapsedStatus',
 | 
			
		||||
      'setResizingStatus',
 | 
			
		||||
    ]),
 | 
			
		||||
    toggleCollapsed() {
 | 
			
		||||
      this.setPanelCollapsedStatus({
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +46,12 @@ export default {
 | 
			
		|||
        collapsed: !this.rightPanelCollapsed,
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    resizingStarted() {
 | 
			
		||||
      this.setResizingStatus(true);
 | 
			
		||||
    },
 | 
			
		||||
    resizingEnded() {
 | 
			
		||||
      this.setResizingStatus(false);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +62,7 @@ export default {
 | 
			
		|||
    :class="{
 | 
			
		||||
      'is-collapsed': rightPanelCollapsed,
 | 
			
		||||
    }"
 | 
			
		||||
    :style="panelStyle"
 | 
			
		||||
  >
 | 
			
		||||
    <div 
 | 
			
		||||
      class="multi-file-commit-panel-section">
 | 
			
		||||
| 
						 | 
				
			
			@ -71,5 +95,14 @@ export default {
 | 
			
		|||
      <repo-commit-section 
 | 
			
		||||
        class=""/>
 | 
			
		||||
    </div>
 | 
			
		||||
    <panel-resizer
 | 
			
		||||
      :size.sync="width"
 | 
			
		||||
      :enabled="!rightPanelCollapsed"
 | 
			
		||||
      :start-size="290"
 | 
			
		||||
      :min-size="200"
 | 
			
		||||
      :max-size="maxSize"
 | 
			
		||||
      @resize-start="resizingStarted"
 | 
			
		||||
      @resize-end="resizingEnded"
 | 
			
		||||
      side="left"/>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,11 +2,18 @@
 | 
			
		|||
import { mapState, mapActions } from 'vuex';
 | 
			
		||||
import projectTree from './ide_project_tree.vue';
 | 
			
		||||
import icon from '../../vue_shared/components/icon.vue';
 | 
			
		||||
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      width: 290,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  components: {
 | 
			
		||||
    projectTree,
 | 
			
		||||
    icon,
 | 
			
		||||
    panelResizer,
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState([
 | 
			
		||||
| 
						 | 
				
			
			@ -16,10 +23,20 @@ export default {
 | 
			
		|||
    currentIcon() {
 | 
			
		||||
      return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
 | 
			
		||||
    },
 | 
			
		||||
    maxSize() {
 | 
			
		||||
      return window.innerWidth / 2;
 | 
			
		||||
    },
 | 
			
		||||
    panelStyle() {
 | 
			
		||||
      if (!this.leftPanelCollapsed) {
 | 
			
		||||
        return { width: `${this.width}px` };
 | 
			
		||||
      }
 | 
			
		||||
      return {};
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions([
 | 
			
		||||
      'setPanelCollapsedStatus',
 | 
			
		||||
      'setResizingStatus',
 | 
			
		||||
    ]),
 | 
			
		||||
    toggleCollapsed() {
 | 
			
		||||
      this.setPanelCollapsedStatus({
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +44,12 @@ export default {
 | 
			
		|||
        collapsed: !this.leftPanelCollapsed,
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    resizingStarted() {
 | 
			
		||||
      this.setResizingStatus(true);
 | 
			
		||||
    },
 | 
			
		||||
    resizingEnded() {
 | 
			
		||||
      this.setResizingStatus(false);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +60,7 @@ export default {
 | 
			
		|||
      :class="{
 | 
			
		||||
        'is-collapsed': leftPanelCollapsed,
 | 
			
		||||
      }"
 | 
			
		||||
      :style="panelStyle"
 | 
			
		||||
    >
 | 
			
		||||
    <div class="multi-file-commit-panel-inner">
 | 
			
		||||
      <project-tree
 | 
			
		||||
| 
						 | 
				
			
			@ -58,5 +82,14 @@ export default {
 | 
			
		|||
        class="collapse-text"
 | 
			
		||||
      >Collapse sidebar</span>
 | 
			
		||||
    </button>
 | 
			
		||||
    <panel-resizer
 | 
			
		||||
      :size.sync="width"
 | 
			
		||||
      :enabled="!leftPanelCollapsed"
 | 
			
		||||
      :start-size="290"
 | 
			
		||||
      :min-size="200"
 | 
			
		||||
      :max-size="maxSize"
 | 
			
		||||
      @resize-start="resizingStarted"
 | 
			
		||||
      @resize-end="resizingEnded"
 | 
			
		||||
      side="right"/>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,6 +90,11 @@ export default {
 | 
			
		|||
    rightPanelCollapsed() {
 | 
			
		||||
      this.editor.updateDimensions();
 | 
			
		||||
    },
 | 
			
		||||
    panelResizing(isResizing) {
 | 
			
		||||
      if (isResizing === false) {
 | 
			
		||||
        this.editor.updateDimensions();
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters([
 | 
			
		||||
| 
						 | 
				
			
			@ -99,6 +104,7 @@ export default {
 | 
			
		|||
    ...mapState([
 | 
			
		||||
      'leftPanelCollapsed',
 | 
			
		||||
      'rightPanelCollapsed',
 | 
			
		||||
      'panelResizing',
 | 
			
		||||
    ]),
 | 
			
		||||
    shouldHideEditor() {
 | 
			
		||||
      return this.activeFile.binary && !this.activeFile.raw;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
  import timeAgoMixin from '../../vue_shared/mixins/timeago';
 | 
			
		||||
  import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
 | 
			
		||||
  import newDropdown from './new_dropdown/index.vue';
 | 
			
		||||
  import fileIcon from '../../vue_shared/components/file_icon.vue';
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
    mixins: [
 | 
			
		||||
| 
						 | 
				
			
			@ -11,6 +12,7 @@
 | 
			
		|||
    components: {
 | 
			
		||||
      skeletonLoadingContainer,
 | 
			
		||||
      newDropdown,
 | 
			
		||||
      fileIcon,
 | 
			
		||||
    },
 | 
			
		||||
    props: {
 | 
			
		||||
      file: {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,13 +28,6 @@
 | 
			
		|||
      ...mapState([
 | 
			
		||||
        'leftPanelCollapsed',
 | 
			
		||||
      ]),
 | 
			
		||||
      fileIcon() {
 | 
			
		||||
        return {
 | 
			
		||||
          'fa-spinner fa-spin': this.file.loading,
 | 
			
		||||
          [this.file.icon]: !this.file.loading,
 | 
			
		||||
          'fa-folder-open': !this.file.loading && this.file.opened,
 | 
			
		||||
        };
 | 
			
		||||
      },
 | 
			
		||||
      isSubmodule() {
 | 
			
		||||
        return this.file.type === 'submodule';
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			@ -94,16 +89,18 @@
 | 
			
		|||
      class="multi-file-table-name"
 | 
			
		||||
      :colspan="submoduleColSpan"
 | 
			
		||||
    >
 | 
			
		||||
      <i
 | 
			
		||||
        class="fa fa-fw file-icon"
 | 
			
		||||
        :class="fileIcon"
 | 
			
		||||
        :style="levelIndentation"
 | 
			
		||||
        aria-hidden="true"
 | 
			
		||||
      >
 | 
			
		||||
      </i>
 | 
			
		||||
      <a
 | 
			
		||||
        class="repo-file-name"
 | 
			
		||||
      >
 | 
			
		||||
        <file-icon
 | 
			
		||||
          :file-name="file.name"
 | 
			
		||||
          :loading="file.loading"
 | 
			
		||||
          :folder="file.type === 'tree'"
 | 
			
		||||
          :opened="file.opened"
 | 
			
		||||
          :style="levelIndentation"
 | 
			
		||||
          :size="16"
 | 
			
		||||
        >
 | 
			
		||||
        </file-icon>
 | 
			
		||||
        {{ file.name }}
 | 
			
		||||
      </a>
 | 
			
		||||
      <new-dropdown
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { mapActions } from 'vuex';
 | 
			
		||||
import fileIcon from '../../vue_shared/components/file_icon.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +9,9 @@ export default {
 | 
			
		|||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  components: {
 | 
			
		||||
    fileIcon,
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    closeLabel() {
 | 
			
		||||
      if (this.tab.changed || this.tab.tempFile) {
 | 
			
		||||
| 
						 | 
				
			
			@ -63,6 +66,11 @@ export default {
 | 
			
		|||
      :class="{active : tab.active }"
 | 
			
		||||
      :title="tab.url"
 | 
			
		||||
    >
 | 
			
		||||
      <file-icon
 | 
			
		||||
        :file-name="tab.name"
 | 
			
		||||
        :size="16"
 | 
			
		||||
      >
 | 
			
		||||
      </file-icon>
 | 
			
		||||
      {{ tab.name }}
 | 
			
		||||
    </div>
 | 
			
		||||
  </li>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,6 +63,10 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
 | 
			
		|||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const setResizingStatus = ({ commit }, resizing) => {
 | 
			
		||||
  commit(types.SET_RESIZING_STATUS, resizing);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const checkCommitStatus = ({ state }) =>
 | 
			
		||||
  service
 | 
			
		||||
    .getBranchData(state.currentProjectId, state.currentBranchId)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ export const SET_ROOT = 'SET_ROOT';
 | 
			
		|||
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
 | 
			
		||||
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
 | 
			
		||||
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
 | 
			
		||||
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
 | 
			
		||||
 | 
			
		||||
// Project Mutation Types
 | 
			
		||||
export const SET_PROJECT = 'SET_PROJECT';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,6 +49,11 @@ export default {
 | 
			
		|||
      rightPanelCollapsed: collapsed,
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  [types.SET_RESIZING_STATUS](state, resizing) {
 | 
			
		||||
    Object.assign(state, {
 | 
			
		||||
      panelResizing: resizing,
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
 | 
			
		||||
    Object.assign(entry.lastCommit, {
 | 
			
		||||
      id: lastCommit.commit.id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,4 +19,5 @@ export default () => ({
 | 
			
		|||
  projects: {},
 | 
			
		||||
  leftPanelCollapsed: false,
 | 
			
		||||
  rightPanelCollapsed: true,
 | 
			
		||||
  panelResizing: false,
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
/* eslint-disable no-new */
 | 
			
		||||
/* global MilestoneSelect */
 | 
			
		||||
 | 
			
		||||
import MilestoneSelect from './milestone_select';
 | 
			
		||||
import LabelsSelect from './labels_select';
 | 
			
		||||
import IssuableContext from './issuable_context';
 | 
			
		||||
import Sidebar from './right_sidebar';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
/* eslint-disable no-new */
 | 
			
		||||
import LabelsSelect from './labels_select';
 | 
			
		||||
/* global MilestoneSelect */
 | 
			
		||||
import subscriptionSelect from './subscription_select';
 | 
			
		||||
import UsersSelect from './users_select';
 | 
			
		||||
import issueStatusSelect from './issue_status_select';
 | 
			
		||||
import MilestoneSelect from './milestone_select';
 | 
			
		||||
 | 
			
		||||
export default () => {
 | 
			
		||||
  new UsersSelect();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,7 @@
 | 
			
		|||
/* eslint-disable class-methods-use-this, no-new */
 | 
			
		||||
/* global MilestoneSelect */
 | 
			
		||||
 | 
			
		||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
 | 
			
		||||
import './milestone_select';
 | 
			
		||||
import MilestoneSelect from './milestone_select';
 | 
			
		||||
import issueStatusSelect from './issue_status_select';
 | 
			
		||||
import subscriptionSelect from './subscription_select';
 | 
			
		||||
import LabelsSelect from './labels_select';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -172,8 +172,8 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    updateIssuable() {
 | 
			
		||||
      this.service.updateIssuable(this.store.formState)
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
      return this.service.updateIssuable(this.store.formState)
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then(data => this.checkForSpam(data))
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          if (location.pathname !== data.web_url) {
 | 
			
		||||
| 
						 | 
				
			
			@ -182,7 +182,7 @@ export default {
 | 
			
		|||
 | 
			
		||||
          return this.service.getData();
 | 
			
		||||
        })
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          this.store.updateState(data);
 | 
			
		||||
          eventHub.$emit('close.form');
 | 
			
		||||
| 
						 | 
				
			
			@ -207,7 +207,7 @@ export default {
 | 
			
		|||
 | 
			
		||||
    deleteIssuable() {
 | 
			
		||||
      this.service.deleteIssuable()
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          // Stop the poll so we don't get 404's with the issuable not existing
 | 
			
		||||
          this.poll.stop();
 | 
			
		||||
| 
						 | 
				
			
			@ -225,7 +225,7 @@ export default {
 | 
			
		|||
    this.poll = new Poll({
 | 
			
		||||
      resource: this.service,
 | 
			
		||||
      method: 'getData',
 | 
			
		||||
      successCallback: res => res.json().then(data => this.store.updateState(data)),
 | 
			
		||||
      successCallback: res => this.store.updateState(res.data),
 | 
			
		||||
      errorCallback(err) {
 | 
			
		||||
        throw new Error(err);
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,29 +1,20 @@
 | 
			
		|||
import Vue from 'vue';
 | 
			
		||||
import VueResource from 'vue-resource';
 | 
			
		||||
 | 
			
		||||
Vue.use(VueResource);
 | 
			
		||||
import axios from '../../lib/utils/axios_utils';
 | 
			
		||||
 | 
			
		||||
export default class Service {
 | 
			
		||||
  constructor(endpoint) {
 | 
			
		||||
    this.endpoint = endpoint;
 | 
			
		||||
 | 
			
		||||
    this.resource = Vue.resource(`${this.endpoint}.json`, {}, {
 | 
			
		||||
      realtimeChanges: {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        url: `${this.endpoint}/realtime_changes`,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    this.endpoint = `${endpoint}.json`;
 | 
			
		||||
    this.realtimeEndpoint = `${endpoint}/realtime_changes`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getData() {
 | 
			
		||||
    return this.resource.realtimeChanges();
 | 
			
		||||
    return axios.get(this.realtimeEndpoint);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deleteIssuable() {
 | 
			
		||||
    return this.resource.delete();
 | 
			
		||||
    return axios.delete(this.endpoint);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateIssuable(data) {
 | 
			
		||||
    return this.resource.update(data);
 | 
			
		||||
    return axios.put(this.endpoint, data);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import _ from 'underscore';
 | 
			
		||||
import { visitUrl } from './lib/utils/url_utility';
 | 
			
		||||
import bp from './breakpoints';
 | 
			
		||||
import { bytesToKiB } from './lib/utils/number_utils';
 | 
			
		||||
import { numberToHumanSize } from './lib/utils/number_utils';
 | 
			
		||||
import { setCiStatusFavicon } from './lib/utils/common_utils';
 | 
			
		||||
import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -96,14 +96,15 @@ export default class Job {
 | 
			
		|||
 | 
			
		||||
  // eslint-disable-next-line class-methods-use-this
 | 
			
		||||
  canScroll() {
 | 
			
		||||
    return this.$document.height() > this.$window.height();
 | 
			
		||||
    return $(document).height() > $(window).height();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleScroll() {
 | 
			
		||||
    const currentPosition = this.$document.scrollTop();
 | 
			
		||||
    const scrollHeight = this.$document.height();
 | 
			
		||||
    const $document = $(document);
 | 
			
		||||
    const currentPosition = $document.scrollTop();
 | 
			
		||||
    const scrollHeight = $document.height();
 | 
			
		||||
 | 
			
		||||
    const windowHeight = this.$window.height();
 | 
			
		||||
    const windowHeight = $(window).height();
 | 
			
		||||
    if (this.canScroll()) {
 | 
			
		||||
      if (currentPosition > 0 &&
 | 
			
		||||
        (scrollHeight - currentPosition !== windowHeight)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -127,18 +128,22 @@ export default class Job {
 | 
			
		|||
      this.toggleDisableButton(this.$scrollBottomBtn, true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // eslint-disable-next-line class-methods-use-this
 | 
			
		||||
  isScrolledToBottom() {
 | 
			
		||||
    const currentPosition = this.$document.scrollTop();
 | 
			
		||||
    const scrollHeight = this.$document.height();
 | 
			
		||||
    const $document = $(document);
 | 
			
		||||
 | 
			
		||||
    const currentPosition = $document.scrollTop();
 | 
			
		||||
    const scrollHeight = $document.height();
 | 
			
		||||
 | 
			
		||||
    const windowHeight = $(window).height();
 | 
			
		||||
 | 
			
		||||
    const windowHeight = this.$window.height();
 | 
			
		||||
    return scrollHeight - currentPosition === windowHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // eslint-disable-next-line class-methods-use-this
 | 
			
		||||
  scrollDown() {
 | 
			
		||||
    this.$document.scrollTop(this.$document.height());
 | 
			
		||||
    const $document = $(document);
 | 
			
		||||
    $document.scrollTop($document.height());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scrollToBottom() {
 | 
			
		||||
| 
						 | 
				
			
			@ -148,7 +153,7 @@ export default class Job {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  scrollToTop() {
 | 
			
		||||
    this.$document.scrollTop(0);
 | 
			
		||||
    $(document).scrollTop(0);
 | 
			
		||||
    this.hasBeenScrolled = true;
 | 
			
		||||
    this.toggleScroll();
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -193,7 +198,7 @@ export default class Job {
 | 
			
		|||
        // we need to show a message warning the user about that.
 | 
			
		||||
        if (this.logBytes < log.total) {
 | 
			
		||||
          // size is in bytes, we need to calculate KiB
 | 
			
		||||
          const size = bytesToKiB(this.logBytes);
 | 
			
		||||
          const size = numberToHumanSize(this.logBytes);
 | 
			
		||||
          $('.js-truncated-info-size').html(`${size}`);
 | 
			
		||||
          this.$truncatedInfo.removeClass('hidden');
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,8 @@ import axios from 'axios';
 | 
			
		|||
import csrf from './csrf';
 | 
			
		||||
 | 
			
		||||
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
 | 
			
		||||
// Used by Rails to check if it is a valid XHR request
 | 
			
		||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
 | 
			
		||||
 | 
			
		||||
// Maintain a global counter for active requests
 | 
			
		||||
// see: spec/support/wait_for_requests.rb
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -232,7 +232,7 @@ export const nodeMatchesSelector = (node, selector) => {
 | 
			
		|||
export const normalizeHeaders = (headers) => {
 | 
			
		||||
  const upperCaseHeaders = {};
 | 
			
		||||
 | 
			
		||||
  Object.keys(headers).forEach((e) => {
 | 
			
		||||
  Object.keys(headers || {}).forEach((e) => {
 | 
			
		||||
    upperCaseHeaders[e.toUpperCase()] = headers[e];
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@ export function getParameterValues(sParam) {
 | 
			
		|||
// @param {String} url
 | 
			
		||||
export function mergeUrlParams(params, url) {
 | 
			
		||||
  let newUrl = Object.keys(params).reduce((acc, paramName) => {
 | 
			
		||||
    const paramValue = params[paramName];
 | 
			
		||||
    const paramValue = encodeURIComponent(params[paramName]);
 | 
			
		||||
    const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`);
 | 
			
		||||
 | 
			
		||||
    if (paramValue === null) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,48 +1,50 @@
 | 
			
		|||
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
 | 
			
		||||
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
 | 
			
		||||
/* global Issuable */
 | 
			
		||||
/* global ListMilestone */
 | 
			
		||||
import _ from 'underscore';
 | 
			
		||||
import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		||||
 | 
			
		||||
(function() {
 | 
			
		||||
  this.MilestoneSelect = (function() {
 | 
			
		||||
    function MilestoneSelect(currentProject, els, options = {}) {
 | 
			
		||||
      var _this, $els;
 | 
			
		||||
      if (currentProject != null) {
 | 
			
		||||
        _this = this;
 | 
			
		||||
export default class MilestoneSelect {
 | 
			
		||||
  constructor(currentProject, els, options = {}) {
 | 
			
		||||
    if (currentProject !== null) {
 | 
			
		||||
      this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      $els = $(els);
 | 
			
		||||
    this.init(els, options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init(els, options) {
 | 
			
		||||
    let $els = $(els);
 | 
			
		||||
 | 
			
		||||
    if (!els) {
 | 
			
		||||
      $els = $('.js-milestone-select');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      $els.each(function(i, dropdown) {
 | 
			
		||||
        var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, defaultNo, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, selectedMilestoneDefault, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
 | 
			
		||||
        $dropdown = $(dropdown);
 | 
			
		||||
        projectId = $dropdown.data('project-id');
 | 
			
		||||
        milestonesUrl = $dropdown.data('milestones');
 | 
			
		||||
        issueUpdateURL = $dropdown.data('issueUpdate');
 | 
			
		||||
        showNo = $dropdown.data('show-no');
 | 
			
		||||
        showAny = $dropdown.data('show-any');
 | 
			
		||||
        showMenuAbove = $dropdown.data('showMenuAbove');
 | 
			
		||||
        showUpcoming = $dropdown.data('show-upcoming');
 | 
			
		||||
        showStarted = $dropdown.data('show-started');
 | 
			
		||||
        useId = $dropdown.data('use-id');
 | 
			
		||||
        defaultLabel = $dropdown.data('default-label');
 | 
			
		||||
        defaultNo = $dropdown.data('default-no');
 | 
			
		||||
        issuableId = $dropdown.data('issuable-id');
 | 
			
		||||
        abilityName = $dropdown.data('ability-name');
 | 
			
		||||
        $selectbox = $dropdown.closest('.selectbox');
 | 
			
		||||
        $block = $selectbox.closest('.block');
 | 
			
		||||
        $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
 | 
			
		||||
        $value = $block.find('.value');
 | 
			
		||||
        $loading = $block.find('.block-loading').fadeOut();
 | 
			
		||||
    $els.each((i, dropdown) => {
 | 
			
		||||
      let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
 | 
			
		||||
      const $dropdown = $(dropdown);
 | 
			
		||||
      const projectId = $dropdown.data('project-id');
 | 
			
		||||
      const milestonesUrl = $dropdown.data('milestones');
 | 
			
		||||
      const issueUpdateURL = $dropdown.data('issueUpdate');
 | 
			
		||||
      const showNo = $dropdown.data('show-no');
 | 
			
		||||
      const showAny = $dropdown.data('show-any');
 | 
			
		||||
      const showMenuAbove = $dropdown.data('showMenuAbove');
 | 
			
		||||
      const showUpcoming = $dropdown.data('show-upcoming');
 | 
			
		||||
      const showStarted = $dropdown.data('show-started');
 | 
			
		||||
      const useId = $dropdown.data('use-id');
 | 
			
		||||
      const defaultLabel = $dropdown.data('default-label');
 | 
			
		||||
      const defaultNo = $dropdown.data('default-no');
 | 
			
		||||
      const issuableId = $dropdown.data('issuable-id');
 | 
			
		||||
      const abilityName = $dropdown.data('ability-name');
 | 
			
		||||
      const $selectBox = $dropdown.closest('.selectbox');
 | 
			
		||||
      const $block = $selectBox.closest('.block');
 | 
			
		||||
      const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
 | 
			
		||||
      const $value = $block.find('.value');
 | 
			
		||||
      const $loading = $block.find('.block-loading').fadeOut();
 | 
			
		||||
      selectedMilestoneDefault = (showAny ? '' : null);
 | 
			
		||||
      selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
 | 
			
		||||
      selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
 | 
			
		||||
 | 
			
		||||
      if (issueUpdateURL) {
 | 
			
		||||
        milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
 | 
			
		||||
        milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
 | 
			
		||||
| 
						 | 
				
			
			@ -50,11 +52,10 @@ import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		|||
      }
 | 
			
		||||
      return $dropdown.glDropdown({
 | 
			
		||||
        showMenuAbove: showMenuAbove,
 | 
			
		||||
          data: function(term, callback) {
 | 
			
		||||
            return $.ajax({
 | 
			
		||||
        data: (term, callback) => $.ajax({
 | 
			
		||||
          url: milestonesUrl
 | 
			
		||||
            }).done(function(data) {
 | 
			
		||||
              var extraOptions = [];
 | 
			
		||||
        }).done((data) => {
 | 
			
		||||
          const extraOptions = [];
 | 
			
		||||
          if (showAny) {
 | 
			
		||||
            extraOptions.push({
 | 
			
		||||
              id: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -92,23 +93,20 @@ import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		|||
            $dropdown.data('glDropdown').positionMenuAbove();
 | 
			
		||||
          }
 | 
			
		||||
          $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
          renderRow: function(milestone) {
 | 
			
		||||
            return `
 | 
			
		||||
        }),
 | 
			
		||||
        renderRow: milestone => `
 | 
			
		||||
          <li data-milestone-id="${milestone.name}">
 | 
			
		||||
            <a href='#' class='dropdown-menu-milestone-link'>
 | 
			
		||||
              ${_.escape(milestone.title)}
 | 
			
		||||
            </a>
 | 
			
		||||
          </li>
 | 
			
		||||
            `;
 | 
			
		||||
          },
 | 
			
		||||
        `,
 | 
			
		||||
        filterable: true,
 | 
			
		||||
        search: {
 | 
			
		||||
          fields: ['title']
 | 
			
		||||
        },
 | 
			
		||||
        selectable: true,
 | 
			
		||||
          toggleLabel: function(selected, el, e) {
 | 
			
		||||
        toggleLabel: (selected, el, e) => {
 | 
			
		||||
          if (selected && 'id' in selected && $(el).hasClass('is-active')) {
 | 
			
		||||
            return selected.title;
 | 
			
		||||
          } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -117,25 +115,21 @@ import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		|||
        },
 | 
			
		||||
        defaultLabel: defaultLabel,
 | 
			
		||||
        fieldName: $dropdown.data('field-name'),
 | 
			
		||||
          text: function(milestone) {
 | 
			
		||||
            return _.escape(milestone.title);
 | 
			
		||||
          },
 | 
			
		||||
          id: function(milestone) {
 | 
			
		||||
        text: milestone => _.escape(milestone.title),
 | 
			
		||||
        id: (milestone) => {
 | 
			
		||||
          if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
 | 
			
		||||
            return milestone.name;
 | 
			
		||||
          } else {
 | 
			
		||||
            return milestone.id;
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
          isSelected: function(milestone) {
 | 
			
		||||
            return milestone.name === selectedMilestone;
 | 
			
		||||
          },
 | 
			
		||||
          hidden: function() {
 | 
			
		||||
            $selectbox.hide();
 | 
			
		||||
        isSelected: milestone => milestone.name === selectedMilestone,
 | 
			
		||||
        hidden: () => {
 | 
			
		||||
          $selectBox.hide();
 | 
			
		||||
          // display:block overrides the hide-collapse rule
 | 
			
		||||
          return $value.css('display', '');
 | 
			
		||||
        },
 | 
			
		||||
          opened: function(e) {
 | 
			
		||||
        opened: (e) => {
 | 
			
		||||
          const $el = $(e.currentTarget);
 | 
			
		||||
          if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
 | 
			
		||||
            selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
 | 
			
		||||
| 
						 | 
				
			
			@ -144,11 +138,11 @@ import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		|||
          $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
 | 
			
		||||
        },
 | 
			
		||||
        vue: $dropdown.hasClass('js-issue-board-sidebar'),
 | 
			
		||||
          clicked: function(clickEvent) {
 | 
			
		||||
        clicked: (clickEvent) => {
 | 
			
		||||
          const { $el, e } = clickEvent;
 | 
			
		||||
          let selected = clickEvent.selectedObj;
 | 
			
		||||
 | 
			
		||||
            var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
 | 
			
		||||
          let data, boardsStore;
 | 
			
		||||
          if (!selected) return;
 | 
			
		||||
 | 
			
		||||
          if (options.handleClick) {
 | 
			
		||||
| 
						 | 
				
			
			@ -157,10 +151,10 @@ import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		|||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
            page = $('body').attr('data-page');
 | 
			
		||||
            isIssueIndex = page === 'projects:issues:index';
 | 
			
		||||
            isMRIndex = (page === page && page === 'projects:merge_requests:index');
 | 
			
		||||
            isSelecting = (selected.name !== selectedMilestone);
 | 
			
		||||
          const page = $('body').attr('data-page');
 | 
			
		||||
          const isIssueIndex = page === 'projects:issues:index';
 | 
			
		||||
          const isMRIndex = (page === page && page === 'projects:merge_requests:index');
 | 
			
		||||
          const isSelecting = (selected.name !== selectedMilestone);
 | 
			
		||||
          selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
 | 
			
		||||
          if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
| 
						 | 
				
			
			@ -192,7 +186,7 @@ import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		|||
            $loading.removeClass('hidden').fadeIn();
 | 
			
		||||
 | 
			
		||||
            gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
 | 
			
		||||
                .then(function () {
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                $dropdown.trigger('loaded.gl.dropdown');
 | 
			
		||||
                $loading.fadeOut();
 | 
			
		||||
              })
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +194,7 @@ import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		|||
                $loading.fadeOut();
 | 
			
		||||
              });
 | 
			
		||||
          } else {
 | 
			
		||||
              selected = $selectbox.find('input[type="hidden"]').val();
 | 
			
		||||
            selected = $selectBox.find('input[type="hidden"]').val();
 | 
			
		||||
            data = {};
 | 
			
		||||
            data[abilityName] = {};
 | 
			
		||||
            data[abilityName].milestone_id = selected != null ? selected : null;
 | 
			
		||||
| 
						 | 
				
			
			@ -210,13 +204,13 @@ import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		|||
              type: 'PUT',
 | 
			
		||||
              url: issueUpdateURL,
 | 
			
		||||
              data: data
 | 
			
		||||
              }).done(function(data) {
 | 
			
		||||
            }).done((data) => {
 | 
			
		||||
              $dropdown.trigger('loaded.gl.dropdown');
 | 
			
		||||
              $loading.fadeOut();
 | 
			
		||||
                $selectbox.hide();
 | 
			
		||||
              $selectBox.hide();
 | 
			
		||||
              $value.css('display', '');
 | 
			
		||||
              if (data.milestone != null) {
 | 
			
		||||
                  data.milestone.full_path = _this.currentProject.full_path;
 | 
			
		||||
                data.milestone.full_path = this.currentProject.full_path;
 | 
			
		||||
                data.milestone.remaining = timeFor(data.milestone.due_date);
 | 
			
		||||
                data.milestone.name = data.milestone.title;
 | 
			
		||||
                $value.html(milestoneLinkTemplate(data.milestone));
 | 
			
		||||
| 
						 | 
				
			
			@ -231,7 +225,4 @@ import { timeFor } from './lib/utils/datetime_utility';
 | 
			
		|||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    return MilestoneSelect;
 | 
			
		||||
  })();
 | 
			
		||||
}).call(window);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,18 @@
 | 
			
		|||
import { timeFormat as time } from 'd3-time-format';
 | 
			
		||||
import { timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3-time';
 | 
			
		||||
import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from 'd3-time';
 | 
			
		||||
import { bisector } from 'd3-array';
 | 
			
		||||
 | 
			
		||||
const d3 = { time, bisector, timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear };
 | 
			
		||||
const d3 = {
 | 
			
		||||
  time,
 | 
			
		||||
  bisector,
 | 
			
		||||
  timeSecond,
 | 
			
		||||
  timeMinute,
 | 
			
		||||
  timeHour,
 | 
			
		||||
  timeDay,
 | 
			
		||||
  timeWeek,
 | 
			
		||||
  timeMonth,
 | 
			
		||||
  timeYear,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dateFormat = d3.time('%b %-d, %Y');
 | 
			
		||||
export const timeFormat = d3.time('%-I:%M%p');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
 | 
			
		||||
import Cookies from 'js-cookie';
 | 
			
		||||
import Flash from '../flash';
 | 
			
		||||
import { getPagePath } from '../lib/utils/common_utils';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -7,6 +8,8 @@ import { getPagePath } from '../lib/utils/common_utils';
 | 
			
		|||
    constructor({ form } = {}) {
 | 
			
		||||
      this.onSubmitForm = this.onSubmitForm.bind(this);
 | 
			
		||||
      this.form = form || $('.edit-user');
 | 
			
		||||
      this.newRepoActivated = Cookies.get('new_repo');
 | 
			
		||||
      this.setRepoRadio();
 | 
			
		||||
      this.bindEvents();
 | 
			
		||||
      this.initAvatarGlCrop();
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +28,7 @@ import { getPagePath } from '../lib/utils/common_utils';
 | 
			
		|||
 | 
			
		||||
    bindEvents() {
 | 
			
		||||
      $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
 | 
			
		||||
      $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
 | 
			
		||||
      $('#user_notification_email').on('change', this.submitForm);
 | 
			
		||||
      $('#user_notified_of_own_activity').on('change', this.submitForm);
 | 
			
		||||
      $('.update-username').on('ajax:before', this.beforeUpdateUsername);
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +86,23 @@ import { getPagePath } from '../lib/utils/common_utils';
 | 
			
		|||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setNewRepoCookie() {
 | 
			
		||||
      if (this.value === 'off') {
 | 
			
		||||
        Cookies.remove('new_repo');
 | 
			
		||||
      } else {
 | 
			
		||||
        Cookies.set('new_repo', true, { expires_in: 365 });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setRepoRadio() {
 | 
			
		||||
      const multiEditRadios = $('input[name="user[multi_file]"]');
 | 
			
		||||
      if (this.newRepoActivated || this.newRepoActivated === 'true') {
 | 
			
		||||
        multiEditRadios.filter('[value=on]').prop('checked', true);
 | 
			
		||||
      } else {
 | 
			
		||||
        multiEditRadios.filter('[value=off]').prop('checked', true);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $(function() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,10 +34,10 @@ export default {
 | 
			
		|||
 | 
			
		||||
      if (isConfirmed) {
 | 
			
		||||
        MRWidgetService.stopEnvironment(deployment.stop_url)
 | 
			
		||||
          .then(res => res.json())
 | 
			
		||||
          .then((res) => {
 | 
			
		||||
            if (res.redirect_url) {
 | 
			
		||||
              visitUrl(res.redirect_url);
 | 
			
		||||
          .then(res => res.data)
 | 
			
		||||
          .then((data) => {
 | 
			
		||||
            if (data.redirect_url) {
 | 
			
		||||
              visitUrl(data.redirect_url);
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
          .catch(() => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -102,11 +102,11 @@ export default {
 | 
			
		|||
            return res;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return res.json();
 | 
			
		||||
          return res.data;
 | 
			
		||||
        })
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          this.computeGraphData(res.metrics, res.deployment_time);
 | 
			
		||||
          return res;
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          this.computeGraphData(data.metrics, data.deployment_time);
 | 
			
		||||
          return data;
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
          this.loadFailed = true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,9 +16,9 @@ export default {
 | 
			
		|||
      <div class="media-body">
 | 
			
		||||
        <mr-widget-author-and-time
 | 
			
		||||
          actionText="Closed by"
 | 
			
		||||
          :author="mr.closedEvent.author"
 | 
			
		||||
          :dateTitle="mr.closedEvent.updatedAt"
 | 
			
		||||
          :dateReadable="mr.closedEvent.formattedUpdatedAt"
 | 
			
		||||
          :author="mr.metrics.closedBy"
 | 
			
		||||
          :dateTitle="mr.metrics.closedAt"
 | 
			
		||||
          :dateReadable="mr.metrics.readableClosedAt"
 | 
			
		||||
        />
 | 
			
		||||
        <section class="mr-info-list">
 | 
			
		||||
          <p>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,9 +31,9 @@ export default {
 | 
			
		|||
    cancelAutomaticMerge() {
 | 
			
		||||
      this.isCancellingAutoMerge = true;
 | 
			
		||||
      this.service.cancelAutomaticMerge()
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          eventHub.$emit('UpdateWidgetData', res);
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          eventHub.$emit('UpdateWidgetData', data);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
          this.isCancellingAutoMerge = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -49,9 +49,9 @@ export default {
 | 
			
		|||
 | 
			
		||||
      this.isRemovingSourceBranch = true;
 | 
			
		||||
      this.service.mergeResource.save(options)
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          if (res.status === 'merge_when_pipeline_succeeds') {
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          if (data.status === 'merge_when_pipeline_succeeds') {
 | 
			
		||||
            eventHub.$emit('MRWidgetUpdateRequested');
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,9 +47,9 @@ export default {
 | 
			
		|||
    removeSourceBranch() {
 | 
			
		||||
      this.isMakingRequest = true;
 | 
			
		||||
      this.service.removeSourceBranch()
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          if (res.message === 'Branch was removed') {
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          if (data.message === 'Branch was removed') {
 | 
			
		||||
            eventHub.$emit('MRWidgetUpdateRequested', () => {
 | 
			
		||||
              this.isMakingRequest = false;
 | 
			
		||||
            });
 | 
			
		||||
| 
						 | 
				
			
			@ -68,9 +68,9 @@ export default {
 | 
			
		|||
        <div class="space-children">
 | 
			
		||||
          <mr-widget-author-and-time
 | 
			
		||||
            actionText="Merged by"
 | 
			
		||||
            :author="mr.mergedEvent.author"
 | 
			
		||||
            :date-title="mr.mergedEvent.updatedAt"
 | 
			
		||||
            :date-readable="mr.mergedEvent.formattedUpdatedAt" />
 | 
			
		||||
            :author="mr.metrics.mergedBy"
 | 
			
		||||
            :date-title="mr.metrics.mergedAt"
 | 
			
		||||
            :date-readable="mr.metrics.readableMergedAt" />
 | 
			
		||||
          <a
 | 
			
		||||
            v-if="mr.canRevertInCurrentMR"
 | 
			
		||||
            v-tooltip
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -135,16 +135,16 @@ export default {
 | 
			
		|||
 | 
			
		||||
      this.isMakingRequest = true;
 | 
			
		||||
      this.service.merge(options)
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          const hasError = res.status === 'failed' || res.status === 'hook_validation_error';
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
 | 
			
		||||
 | 
			
		||||
          if (res.status === 'merge_when_pipeline_succeeds') {
 | 
			
		||||
          if (data.status === 'merge_when_pipeline_succeeds') {
 | 
			
		||||
            eventHub.$emit('MRWidgetUpdateRequested');
 | 
			
		||||
          } else if (res.status === 'success') {
 | 
			
		||||
          } else if (data.status === 'success') {
 | 
			
		||||
            this.initiateMergePolling();
 | 
			
		||||
          } else if (hasError) {
 | 
			
		||||
            eventHub.$emit('FailedToMerge', res.merge_error);
 | 
			
		||||
            eventHub.$emit('FailedToMerge', data.merge_error);
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -159,9 +159,9 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    handleMergePolling(continuePolling, stopPolling) {
 | 
			
		||||
      this.service.poll()
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          if (res.state === 'merged') {
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          if (data.state === 'merged') {
 | 
			
		||||
            // If state is merged we should update the widget and stop the polling
 | 
			
		||||
            eventHub.$emit('MRWidgetUpdateRequested');
 | 
			
		||||
            eventHub.$emit('FetchActionsContent');
 | 
			
		||||
| 
						 | 
				
			
			@ -174,11 +174,11 @@ export default {
 | 
			
		|||
 | 
			
		||||
            // If user checked remove source branch and we didn't remove the branch yet
 | 
			
		||||
            // we should start another polling for source branch remove process
 | 
			
		||||
            if (this.removeSourceBranch && res.source_branch_exists) {
 | 
			
		||||
            if (this.removeSourceBranch && data.source_branch_exists) {
 | 
			
		||||
              this.initiateRemoveSourceBranchPolling();
 | 
			
		||||
            }
 | 
			
		||||
          } else if (res.merge_error) {
 | 
			
		||||
            eventHub.$emit('FailedToMerge', res.merge_error);
 | 
			
		||||
          } else if (data.merge_error) {
 | 
			
		||||
            eventHub.$emit('FailedToMerge', data.merge_error);
 | 
			
		||||
            stopPolling();
 | 
			
		||||
          } else {
 | 
			
		||||
            // MR is not merged yet, continue polling until the state becomes 'merged'
 | 
			
		||||
| 
						 | 
				
			
			@ -199,11 +199,11 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    handleRemoveBranchPolling(continuePolling, stopPolling) {
 | 
			
		||||
      this.service.poll()
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          // If source branch exists then we should continue polling
 | 
			
		||||
          // because removing a source branch is a background task and takes time
 | 
			
		||||
          if (res.source_branch_exists) {
 | 
			
		||||
          if (data.source_branch_exists) {
 | 
			
		||||
            continuePolling();
 | 
			
		||||
          } else {
 | 
			
		||||
            // Branch is removed. Update widget, stop polling and hide the spinner
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,9 +23,9 @@ export default {
 | 
			
		|||
    removeWIP() {
 | 
			
		||||
      this.isMakingRequest = true;
 | 
			
		||||
      this.service.removeWIP()
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          eventHub.$emit('UpdateWidgetData', res);
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          eventHub.$emit('UpdateWidgetData', data);
 | 
			
		||||
          new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
 | 
			
		||||
          $('.merge-request .detail-page-description .title').text(this.mr.title);
 | 
			
		||||
        })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -84,14 +84,14 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    checkStatus(cb) {
 | 
			
		||||
      return this.service.checkStatus()
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          this.handleNotification(res);
 | 
			
		||||
          this.mr.setData(res);
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          this.handleNotification(data);
 | 
			
		||||
          this.mr.setData(data);
 | 
			
		||||
          this.setFaviconHelper();
 | 
			
		||||
 | 
			
		||||
          if (cb) {
 | 
			
		||||
            cb.call(null, res);
 | 
			
		||||
            cb.call(null, data);
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -124,10 +124,10 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    fetchDeployments() {
 | 
			
		||||
      return this.service.fetchDeployments()
 | 
			
		||||
        .then(res => res.json())
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          if (res.length) {
 | 
			
		||||
            this.mr.deployments = res;
 | 
			
		||||
        .then(res => res.data)
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
          if (data.length) {
 | 
			
		||||
            this.mr.deployments = data;
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -137,9 +137,9 @@ export default {
 | 
			
		|||
    fetchActionsContent() {
 | 
			
		||||
      this.service.fetchMergeActionsContent()
 | 
			
		||||
        .then((res) => {
 | 
			
		||||
          if (res.body) {
 | 
			
		||||
          if (res.data) {
 | 
			
		||||
            const el = document.createElement('div');
 | 
			
		||||
            el.innerHTML = res.body;
 | 
			
		||||
            el.innerHTML = res.data;
 | 
			
		||||
            document.body.appendChild(el);
 | 
			
		||||
            Project.initRefSwitcher();
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,57 +1,47 @@
 | 
			
		|||
import Vue from 'vue';
 | 
			
		||||
import VueResource from 'vue-resource';
 | 
			
		||||
 | 
			
		||||
Vue.use(VueResource);
 | 
			
		||||
import axios from '../../lib/utils/axios_utils';
 | 
			
		||||
 | 
			
		||||
export default class MRWidgetService {
 | 
			
		||||
  constructor(endpoints) {
 | 
			
		||||
    this.mergeResource = Vue.resource(endpoints.mergePath);
 | 
			
		||||
    this.mergeCheckResource = Vue.resource(`${endpoints.statusPath}?serializer=widget`);
 | 
			
		||||
    this.cancelAutoMergeResource = Vue.resource(endpoints.cancelAutoMergePath);
 | 
			
		||||
    this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
 | 
			
		||||
    this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
 | 
			
		||||
    this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
 | 
			
		||||
    this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
 | 
			
		||||
    this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
 | 
			
		||||
    this.endpoints = endpoints;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  merge(data) {
 | 
			
		||||
    return this.mergeResource.save(data);
 | 
			
		||||
    return axios.post(this.endpoints.mergePath, data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  cancelAutomaticMerge() {
 | 
			
		||||
    return this.cancelAutoMergeResource.save();
 | 
			
		||||
    return axios.post(this.endpoints.cancelAutoMergePath);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeWIP() {
 | 
			
		||||
    return this.removeWIPResource.save();
 | 
			
		||||
    return axios.post(this.endpoints.removeWIPPath);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeSourceBranch() {
 | 
			
		||||
    return this.removeSourceBranchResource.delete();
 | 
			
		||||
    return axios.delete(this.endpoints.sourceBranchPath);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fetchDeployments() {
 | 
			
		||||
    return this.deploymentsResource.get();
 | 
			
		||||
    return axios.get(this.endpoints.ciEnvironmentsStatusPath);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  poll() {
 | 
			
		||||
    return this.pollResource.get();
 | 
			
		||||
    return axios.get(`${this.endpoints.statusPath}?serializer=basic`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  checkStatus() {
 | 
			
		||||
    return this.mergeCheckResource.get();
 | 
			
		||||
    return axios.get(`${this.endpoints.statusPath}?serializer=widget`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fetchMergeActionsContent() {
 | 
			
		||||
    return this.mergeActionsContentResource.get();
 | 
			
		||||
    return axios.get(this.endpoints.mergeActionsContentPath);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static stopEnvironment(url) {
 | 
			
		||||
    return Vue.http.post(url);
 | 
			
		||||
    return axios.post(url);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static fetchMetrics(metricsUrl) {
 | 
			
		||||
    return Vue.http.get(`${metricsUrl}.json`);
 | 
			
		||||
    return axios.get(`${metricsUrl}.json`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,9 +39,8 @@ export default class MergeRequestStore {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    this.updatedAt = data.updated_at;
 | 
			
		||||
    this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event);
 | 
			
		||||
    this.closedEvent = MergeRequestStore.getEventObject(data.closed_event);
 | 
			
		||||
    this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
 | 
			
		||||
    this.metrics = MergeRequestStore.buildMetrics(data.metrics);
 | 
			
		||||
    this.setToMWPSBy = MergeRequestStore.formatUserObject(data.merge_user || {});
 | 
			
		||||
    this.mergeUserId = data.merge_user_id;
 | 
			
		||||
    this.currentUserId = gon.current_user_id;
 | 
			
		||||
    this.sourceBranchPath = data.source_branch_path;
 | 
			
		||||
| 
						 | 
				
			
			@ -125,43 +124,42 @@ export default class MergeRequestStore {
 | 
			
		|||
    return this.state === stateKey.nothingToMerge;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getEventObject(event) {
 | 
			
		||||
    return {
 | 
			
		||||
      author: MergeRequestStore.getAuthorObject(event),
 | 
			
		||||
      updatedAt: formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
 | 
			
		||||
      formattedUpdatedAt: MergeRequestStore.getEventDate(event),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getAuthorObject(event) {
 | 
			
		||||
    if (!event) {
 | 
			
		||||
  static buildMetrics(metrics) {
 | 
			
		||||
    if (!metrics) {
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      name: event.author.name || '',
 | 
			
		||||
      username: event.author.username || '',
 | 
			
		||||
      webUrl: event.author.web_url || '',
 | 
			
		||||
      avatarUrl: event.author.avatar_url || '',
 | 
			
		||||
      mergedBy: MergeRequestStore.formatUserObject(metrics.merged_by),
 | 
			
		||||
      closedBy: MergeRequestStore.formatUserObject(metrics.closed_by),
 | 
			
		||||
      mergedAt: formatDate(metrics.merged_at),
 | 
			
		||||
      closedAt: formatDate(metrics.closed_at),
 | 
			
		||||
      readableMergedAt: MergeRequestStore.getReadableDate(metrics.merged_at),
 | 
			
		||||
      readableClosedAt: MergeRequestStore.getReadableDate(metrics.closed_at),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getEventUpdatedAtDate(event) {
 | 
			
		||||
    if (!event) {
 | 
			
		||||
  static formatUserObject(user) {
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      name: user.name || '',
 | 
			
		||||
      username: user.username || '',
 | 
			
		||||
      webUrl: user.web_url || '',
 | 
			
		||||
      avatarUrl: user.avatar_url || '',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getReadableDate(date) {
 | 
			
		||||
    if (!date) {
 | 
			
		||||
      return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return event.updated_at;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getEventDate(event) {
 | 
			
		||||
    const timeagoInstance = new Timeago();
 | 
			
		||||
 | 
			
		||||
    if (!event) {
 | 
			
		||||
      return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event));
 | 
			
		||||
    return timeagoInstance.format(date);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,92 @@
 | 
			
		|||
<script>
 | 
			
		||||
  import getIconForFile from './file_icon/file_icon_map';
 | 
			
		||||
  import loadingIcon from '../../vue_shared/components/loading_icon.vue';
 | 
			
		||||
  import icon from '../../vue_shared/components/icon.vue';
 | 
			
		||||
 | 
			
		||||
  /* This is a re-usable vue component for rendering a svg sprite
 | 
			
		||||
    icon
 | 
			
		||||
 | 
			
		||||
    Sample configuration:
 | 
			
		||||
 | 
			
		||||
    <file-icon
 | 
			
		||||
      name="retry"
 | 
			
		||||
      :size="32"
 | 
			
		||||
      css-classes="top"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
  */
 | 
			
		||||
  export default {
 | 
			
		||||
    props: {
 | 
			
		||||
      fileName: {
 | 
			
		||||
        type: String,
 | 
			
		||||
        required: true,
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      folder: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
        required: false,
 | 
			
		||||
        default: false,
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      opened: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
        required: false,
 | 
			
		||||
        default: false,
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      loading: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
        required: false,
 | 
			
		||||
        default: false,
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      size: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
        required: false,
 | 
			
		||||
        default: 16,
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      cssClasses: {
 | 
			
		||||
        type: String,
 | 
			
		||||
        required: false,
 | 
			
		||||
        default: '',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    components: {
 | 
			
		||||
      loadingIcon,
 | 
			
		||||
      icon,
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
      spriteHref() {
 | 
			
		||||
        const iconName = getIconForFile(this.fileName) || 'file';
 | 
			
		||||
        return `${gon.sprite_file_icons}#${iconName}`;
 | 
			
		||||
      },
 | 
			
		||||
      folderIconName() {
 | 
			
		||||
        // We don't have a open folder icon yet
 | 
			
		||||
        return this.opened ? 'folder' : 'folder';
 | 
			
		||||
      },
 | 
			
		||||
      iconSizeClass() {
 | 
			
		||||
        return this.size ? `s${this.size}` : '';
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <span>
 | 
			
		||||
    <svg
 | 
			
		||||
      :class="[iconSizeClass, cssClasses]"
 | 
			
		||||
      v-if="!loading && !folder">
 | 
			
		||||
      <use 
 | 
			
		||||
        v-bind="{'xlink:href':spriteHref}"/>
 | 
			
		||||
    </svg>
 | 
			
		||||
    <icon
 | 
			
		||||
      v-if="!loading && folder"
 | 
			
		||||
      :name="folderIconName"
 | 
			
		||||
      :size="size"
 | 
			
		||||
    />
 | 
			
		||||
    <loading-icon
 | 
			
		||||
      v-if="loading"
 | 
			
		||||
      :inline="true"
 | 
			
		||||
    />
 | 
			
		||||
  </span>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,589 @@
 | 
			
		|||
const fileExtensionIcons = {
 | 
			
		||||
  html: 'html',
 | 
			
		||||
  htm: 'html',
 | 
			
		||||
  html_vm: 'html',
 | 
			
		||||
  asp: 'html',
 | 
			
		||||
  jade: 'pug',
 | 
			
		||||
  pug: 'pug',
 | 
			
		||||
  md: 'markdown',
 | 
			
		||||
  'md.rendered': 'markdown',
 | 
			
		||||
  markdown: 'markdown',
 | 
			
		||||
  'markdown.rendered': 'markdown',
 | 
			
		||||
  rst: 'markdown',
 | 
			
		||||
  blink: 'blink',
 | 
			
		||||
  css: 'css',
 | 
			
		||||
  scss: 'sass',
 | 
			
		||||
  sass: 'sass',
 | 
			
		||||
  less: 'less',
 | 
			
		||||
  json: 'json',
 | 
			
		||||
  yaml: 'yaml',
 | 
			
		||||
  'YAML-tmLanguage': 'yaml',
 | 
			
		||||
  yml: 'yaml',
 | 
			
		||||
  xml: 'xml',
 | 
			
		||||
  plist: 'xml',
 | 
			
		||||
  xsd: 'xml',
 | 
			
		||||
  dtd: 'xml',
 | 
			
		||||
  xsl: 'xml',
 | 
			
		||||
  xslt: 'xml',
 | 
			
		||||
  resx: 'xml',
 | 
			
		||||
  iml: 'xml',
 | 
			
		||||
  xquery: 'xml',
 | 
			
		||||
  tmLanguage: 'xml',
 | 
			
		||||
  manifest: 'xml',
 | 
			
		||||
  project: 'xml',
 | 
			
		||||
  png: 'image',
 | 
			
		||||
  jpeg: 'image',
 | 
			
		||||
  jpg: 'image',
 | 
			
		||||
  gif: 'image',
 | 
			
		||||
  svg: 'image',
 | 
			
		||||
  ico: 'image',
 | 
			
		||||
  tif: 'image',
 | 
			
		||||
  tiff: 'image',
 | 
			
		||||
  psd: 'image',
 | 
			
		||||
  psb: 'image',
 | 
			
		||||
  ami: 'image',
 | 
			
		||||
  apx: 'image',
 | 
			
		||||
  bmp: 'image',
 | 
			
		||||
  bpg: 'image',
 | 
			
		||||
  brk: 'image',
 | 
			
		||||
  cur: 'image',
 | 
			
		||||
  dds: 'image',
 | 
			
		||||
  dng: 'image',
 | 
			
		||||
  exr: 'image',
 | 
			
		||||
  fpx: 'image',
 | 
			
		||||
  gbr: 'image',
 | 
			
		||||
  img: 'image',
 | 
			
		||||
  jbig2: 'image',
 | 
			
		||||
  jb2: 'image',
 | 
			
		||||
  jng: 'image',
 | 
			
		||||
  jxr: 'image',
 | 
			
		||||
  pbm: 'image',
 | 
			
		||||
  pgf: 'image',
 | 
			
		||||
  pic: 'image',
 | 
			
		||||
  raw: 'image',
 | 
			
		||||
  webp: 'image',
 | 
			
		||||
  js: 'javascript',
 | 
			
		||||
  ejs: 'javascript',
 | 
			
		||||
  esx: 'javascript',
 | 
			
		||||
  jsx: 'react',
 | 
			
		||||
  tsx: 'react',
 | 
			
		||||
  ini: 'settings',
 | 
			
		||||
  dlc: 'settings',
 | 
			
		||||
  dll: 'settings',
 | 
			
		||||
  config: 'settings',
 | 
			
		||||
  conf: 'settings',
 | 
			
		||||
  properties: 'settings',
 | 
			
		||||
  prop: 'settings',
 | 
			
		||||
  settings: 'settings',
 | 
			
		||||
  option: 'settings',
 | 
			
		||||
  props: 'settings',
 | 
			
		||||
  toml: 'settings',
 | 
			
		||||
  prefs: 'settings',
 | 
			
		||||
  'sln.dotsettings': 'settings',
 | 
			
		||||
  'sln.dotsettings.user': 'settings',
 | 
			
		||||
  ts: 'typescript',
 | 
			
		||||
  'd.ts': 'typescript-def',
 | 
			
		||||
  marko: 'markojs',
 | 
			
		||||
  pdf: 'pdf',
 | 
			
		||||
  xlsx: 'table',
 | 
			
		||||
  xls: 'table',
 | 
			
		||||
  csv: 'table',
 | 
			
		||||
  tsv: 'table',
 | 
			
		||||
  vscodeignore: 'vscode',
 | 
			
		||||
  vsixmanifest: 'vscode',
 | 
			
		||||
  vsix: 'vscode',
 | 
			
		||||
  'code-workplace': 'vscode',
 | 
			
		||||
  suo: 'visualstudio',
 | 
			
		||||
  sln: 'visualstudio',
 | 
			
		||||
  csproj: 'visualstudio',
 | 
			
		||||
  vb: 'visualstudio',
 | 
			
		||||
  pdb: 'database',
 | 
			
		||||
  sql: 'database',
 | 
			
		||||
  pks: 'database',
 | 
			
		||||
  pkb: 'database',
 | 
			
		||||
  accdb: 'database',
 | 
			
		||||
  mdb: 'database',
 | 
			
		||||
  sqlite: 'database',
 | 
			
		||||
  cs: 'csharp',
 | 
			
		||||
  zip: 'zip',
 | 
			
		||||
  tar: 'zip',
 | 
			
		||||
  gz: 'zip',
 | 
			
		||||
  xz: 'zip',
 | 
			
		||||
  bzip2: 'zip',
 | 
			
		||||
  gzip: 'zip',
 | 
			
		||||
  '7z': 'zip',
 | 
			
		||||
  rar: 'zip',
 | 
			
		||||
  tgz: 'zip',
 | 
			
		||||
  exe: 'exe',
 | 
			
		||||
  msi: 'exe',
 | 
			
		||||
  java: 'java',
 | 
			
		||||
  jar: 'java',
 | 
			
		||||
  jsp: 'java',
 | 
			
		||||
  c: 'c',
 | 
			
		||||
  m: 'c',
 | 
			
		||||
  h: 'h',
 | 
			
		||||
  cc: 'cpp',
 | 
			
		||||
  cpp: 'cpp',
 | 
			
		||||
  mm: 'cpp',
 | 
			
		||||
  cxx: 'cpp',
 | 
			
		||||
  hpp: 'hpp',
 | 
			
		||||
  go: 'go',
 | 
			
		||||
  py: 'python',
 | 
			
		||||
  url: 'url',
 | 
			
		||||
  sh: 'console',
 | 
			
		||||
  ksh: 'console',
 | 
			
		||||
  csh: 'console',
 | 
			
		||||
  tcsh: 'console',
 | 
			
		||||
  zsh: 'console',
 | 
			
		||||
  bash: 'console',
 | 
			
		||||
  bat: 'console',
 | 
			
		||||
  cmd: 'console',
 | 
			
		||||
  ps1: 'powershell',
 | 
			
		||||
  psm1: 'powershell',
 | 
			
		||||
  psd1: 'powershell',
 | 
			
		||||
  ps1xml: 'powershell',
 | 
			
		||||
  psc1: 'powershell',
 | 
			
		||||
  pssc: 'powershell',
 | 
			
		||||
  gradle: 'gradle',
 | 
			
		||||
  doc: 'word',
 | 
			
		||||
  docx: 'word',
 | 
			
		||||
  rtf: 'word',
 | 
			
		||||
  cer: 'certificate',
 | 
			
		||||
  cert: 'certificate',
 | 
			
		||||
  crt: 'certificate',
 | 
			
		||||
  pub: 'key',
 | 
			
		||||
  key: 'key',
 | 
			
		||||
  pem: 'key',
 | 
			
		||||
  asc: 'key',
 | 
			
		||||
  gpg: 'key',
 | 
			
		||||
  woff: 'font',
 | 
			
		||||
  woff2: 'font',
 | 
			
		||||
  ttf: 'font',
 | 
			
		||||
  eot: 'font',
 | 
			
		||||
  suit: 'font',
 | 
			
		||||
  otf: 'font',
 | 
			
		||||
  bmap: 'font',
 | 
			
		||||
  fnt: 'font',
 | 
			
		||||
  odttf: 'font',
 | 
			
		||||
  ttc: 'font',
 | 
			
		||||
  font: 'font',
 | 
			
		||||
  fonts: 'font',
 | 
			
		||||
  sui: 'font',
 | 
			
		||||
  ntf: 'font',
 | 
			
		||||
  mrf: 'font',
 | 
			
		||||
  lib: 'lib',
 | 
			
		||||
  bib: 'lib',
 | 
			
		||||
  rb: 'ruby',
 | 
			
		||||
  erb: 'ruby',
 | 
			
		||||
  fs: 'fsharp',
 | 
			
		||||
  fsx: 'fsharp',
 | 
			
		||||
  fsi: 'fsharp',
 | 
			
		||||
  fsproj: 'fsharp',
 | 
			
		||||
  swift: 'swift',
 | 
			
		||||
  ino: 'arduino',
 | 
			
		||||
  dockerignore: 'docker',
 | 
			
		||||
  dockerfile: 'docker',
 | 
			
		||||
  tex: 'tex',
 | 
			
		||||
  cls: 'tex',
 | 
			
		||||
  sty: 'tex',
 | 
			
		||||
  pptx: 'powerpoint',
 | 
			
		||||
  ppt: 'powerpoint',
 | 
			
		||||
  pptm: 'powerpoint',
 | 
			
		||||
  potx: 'powerpoint',
 | 
			
		||||
  pot: 'powerpoint',
 | 
			
		||||
  potm: 'powerpoint',
 | 
			
		||||
  ppsx: 'powerpoint',
 | 
			
		||||
  ppsm: 'powerpoint',
 | 
			
		||||
  pps: 'powerpoint',
 | 
			
		||||
  ppam: 'powerpoint',
 | 
			
		||||
  ppa: 'powerpoint',
 | 
			
		||||
  webm: 'movie',
 | 
			
		||||
  mkv: 'movie',
 | 
			
		||||
  flv: 'movie',
 | 
			
		||||
  vob: 'movie',
 | 
			
		||||
  ogv: 'movie',
 | 
			
		||||
  ogg: 'movie',
 | 
			
		||||
  gifv: 'movie',
 | 
			
		||||
  avi: 'movie',
 | 
			
		||||
  mov: 'movie',
 | 
			
		||||
  qt: 'movie',
 | 
			
		||||
  wmv: 'movie',
 | 
			
		||||
  yuv: 'movie',
 | 
			
		||||
  rm: 'movie',
 | 
			
		||||
  rmvb: 'movie',
 | 
			
		||||
  mp4: 'movie',
 | 
			
		||||
  m4v: 'movie',
 | 
			
		||||
  mpg: 'movie',
 | 
			
		||||
  mp2: 'movie',
 | 
			
		||||
  mpeg: 'movie',
 | 
			
		||||
  mpe: 'movie',
 | 
			
		||||
  mpv: 'movie',
 | 
			
		||||
  m2v: 'movie',
 | 
			
		||||
  vdi: 'virtual',
 | 
			
		||||
  vbox: 'virtual',
 | 
			
		||||
  'vbox-prev': 'virtual',
 | 
			
		||||
  ics: 'email',
 | 
			
		||||
  mp3: 'music',
 | 
			
		||||
  flac: 'music',
 | 
			
		||||
  m4a: 'music',
 | 
			
		||||
  wma: 'music',
 | 
			
		||||
  aiff: 'music',
 | 
			
		||||
  coffee: 'coffee',
 | 
			
		||||
  txt: 'document',
 | 
			
		||||
  graphql: 'graphql',
 | 
			
		||||
  rs: 'rust',
 | 
			
		||||
  raml: 'raml',
 | 
			
		||||
  xaml: 'xaml',
 | 
			
		||||
  hs: 'haskell',
 | 
			
		||||
  kt: 'kotlin',
 | 
			
		||||
  kts: 'kotlin',
 | 
			
		||||
  patch: 'git',
 | 
			
		||||
  lua: 'lua',
 | 
			
		||||
  clj: 'clojure',
 | 
			
		||||
  cljs: 'clojure',
 | 
			
		||||
  groovy: 'groovy',
 | 
			
		||||
  r: 'r',
 | 
			
		||||
  rmd: 'r',
 | 
			
		||||
  dart: 'dart',
 | 
			
		||||
  as: 'actionscript',
 | 
			
		||||
  mxml: 'mxml',
 | 
			
		||||
  ahk: 'autohotkey',
 | 
			
		||||
  swf: 'flash',
 | 
			
		||||
  swc: 'swc',
 | 
			
		||||
  cmake: 'cmake',
 | 
			
		||||
  asm: 'assembly',
 | 
			
		||||
  a51: 'assembly',
 | 
			
		||||
  inc: 'assembly',
 | 
			
		||||
  nasm: 'assembly',
 | 
			
		||||
  s: 'assembly',
 | 
			
		||||
  ms: 'assembly',
 | 
			
		||||
  agc: 'assembly',
 | 
			
		||||
  ags: 'assembly',
 | 
			
		||||
  aea: 'assembly',
 | 
			
		||||
  argus: 'assembly',
 | 
			
		||||
  mitigus: 'assembly',
 | 
			
		||||
  binsource: 'assembly',
 | 
			
		||||
  vue: 'vue',
 | 
			
		||||
  ml: 'ocaml',
 | 
			
		||||
  mli: 'ocaml',
 | 
			
		||||
  cmx: 'ocaml',
 | 
			
		||||
  'js.map': 'javascript-map',
 | 
			
		||||
  'css.map': 'css-map',
 | 
			
		||||
  lock: 'lock',
 | 
			
		||||
  hbs: 'handlebars',
 | 
			
		||||
  mustache: 'handlebars',
 | 
			
		||||
  pl: 'perl',
 | 
			
		||||
  pm: 'perl',
 | 
			
		||||
  hx: 'haxe',
 | 
			
		||||
  'spec.ts': 'test-ts',
 | 
			
		||||
  'test.ts': 'test-ts',
 | 
			
		||||
  'ts.snap': 'test-ts',
 | 
			
		||||
  'spec.tsx': 'test-jsx',
 | 
			
		||||
  'test.tsx': 'test-jsx',
 | 
			
		||||
  'tsx.snap': 'test-jsx',
 | 
			
		||||
  'spec.jsx': 'test-jsx',
 | 
			
		||||
  'test.jsx': 'test-jsx',
 | 
			
		||||
  'jsx.snap': 'test-jsx',
 | 
			
		||||
  'spec.js': 'test-js',
 | 
			
		||||
  'test.js': 'test-js',
 | 
			
		||||
  'js.snap': 'test-js',
 | 
			
		||||
  'routing.ts': 'angular-routing',
 | 
			
		||||
  'routing.js': 'angular-routing',
 | 
			
		||||
  'module.ts': 'angular',
 | 
			
		||||
  'module.js': 'angular',
 | 
			
		||||
  'ng-template': 'angular',
 | 
			
		||||
  'component.ts': 'angular-component',
 | 
			
		||||
  'component.js': 'angular-component',
 | 
			
		||||
  'guard.ts': 'angular-guard',
 | 
			
		||||
  'guard.js': 'angular-guard',
 | 
			
		||||
  'service.ts': 'angular-service',
 | 
			
		||||
  'service.js': 'angular-service',
 | 
			
		||||
  'pipe.ts': 'angular-pipe',
 | 
			
		||||
  'pipe.js': 'angular-pipe',
 | 
			
		||||
  'filter.js': 'angular-pipe',
 | 
			
		||||
  'directive.ts': 'angular-directive',
 | 
			
		||||
  'directive.js': 'angular-directive',
 | 
			
		||||
  'resolver.ts': 'angular-resolver',
 | 
			
		||||
  'resolver.js': 'angular-resolver',
 | 
			
		||||
  pp: 'puppet',
 | 
			
		||||
  ex: 'elixir',
 | 
			
		||||
  exs: 'elixir',
 | 
			
		||||
  ls: 'livescript',
 | 
			
		||||
  erl: 'erlang',
 | 
			
		||||
  twig: 'twig',
 | 
			
		||||
  jl: 'julia',
 | 
			
		||||
  elm: 'elm',
 | 
			
		||||
  pure: 'purescript',
 | 
			
		||||
  tpl: 'smarty',
 | 
			
		||||
  styl: 'stylus',
 | 
			
		||||
  re: 'reason',
 | 
			
		||||
  rei: 'reason',
 | 
			
		||||
  cmj: 'bucklescript',
 | 
			
		||||
  merlin: 'merlin',
 | 
			
		||||
  v: 'verilog',
 | 
			
		||||
  vhd: 'verilog',
 | 
			
		||||
  sv: 'verilog',
 | 
			
		||||
  svh: 'verilog',
 | 
			
		||||
  nb: 'mathematica',
 | 
			
		||||
  wl: 'wolframlanguage',
 | 
			
		||||
  wls: 'wolframlanguage',
 | 
			
		||||
  njk: 'nunjucks',
 | 
			
		||||
  nunjucks: 'nunjucks',
 | 
			
		||||
  robot: 'robot',
 | 
			
		||||
  sol: 'solidity',
 | 
			
		||||
  au3: 'autoit',
 | 
			
		||||
  haml: 'haml',
 | 
			
		||||
  yang: 'yang',
 | 
			
		||||
  tf: 'terraform',
 | 
			
		||||
  'tf.json': 'terraform',
 | 
			
		||||
  tfvars: 'terraform',
 | 
			
		||||
  tfstate: 'terraform',
 | 
			
		||||
  'blade.php': 'laravel',
 | 
			
		||||
  'inky.php': 'laravel',
 | 
			
		||||
  applescript: 'applescript',
 | 
			
		||||
  cake: 'cake',
 | 
			
		||||
  feature: 'cucumber',
 | 
			
		||||
  nim: 'nim',
 | 
			
		||||
  nimble: 'nim',
 | 
			
		||||
  apib: 'apiblueprint',
 | 
			
		||||
  apiblueprint: 'apiblueprint',
 | 
			
		||||
  tag: 'riot',
 | 
			
		||||
  vfl: 'vfl',
 | 
			
		||||
  kl: 'kl',
 | 
			
		||||
  pcss: 'postcss',
 | 
			
		||||
  sss: 'postcss',
 | 
			
		||||
  todo: 'todo',
 | 
			
		||||
  cfml: 'coldfusion',
 | 
			
		||||
  cfc: 'coldfusion',
 | 
			
		||||
  lucee: 'coldfusion',
 | 
			
		||||
  cabal: 'cabal',
 | 
			
		||||
  nix: 'nix',
 | 
			
		||||
  slim: 'slim',
 | 
			
		||||
  http: 'http',
 | 
			
		||||
  rest: 'http',
 | 
			
		||||
  rql: 'restql',
 | 
			
		||||
  restql: 'restql',
 | 
			
		||||
  kv: 'kivy',
 | 
			
		||||
  graphcool: 'graphcool',
 | 
			
		||||
  sbt: 'sbt',
 | 
			
		||||
  'reducer.ts': 'ngrx-reducer',
 | 
			
		||||
  'rootReducer.ts': 'ngrx-reducer',
 | 
			
		||||
  'state.ts': 'ngrx-state',
 | 
			
		||||
  'actions.ts': 'ngrx-actions',
 | 
			
		||||
  'effects.ts': 'ngrx-effects',
 | 
			
		||||
  cr: 'crystal',
 | 
			
		||||
  'drone.yml': 'drone',
 | 
			
		||||
  cu: 'cuda',
 | 
			
		||||
  cuh: 'cuda',
 | 
			
		||||
  log: 'log',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fileNameIcons = {
 | 
			
		||||
  '.jscsrc': 'json',
 | 
			
		||||
  '.jshintrc': 'json',
 | 
			
		||||
  'tsconfig.json': 'json',
 | 
			
		||||
  'tslint.json': 'json',
 | 
			
		||||
  'composer.lock': 'json',
 | 
			
		||||
  '.jsbeautifyrc': 'json',
 | 
			
		||||
  '.esformatter': 'json',
 | 
			
		||||
  'cdp.pid': 'json',
 | 
			
		||||
  '.htaccess': 'xml',
 | 
			
		||||
  '.jshintignore': 'settings',
 | 
			
		||||
  '.buildignore': 'settings',
 | 
			
		||||
  makefile: 'settings',
 | 
			
		||||
  '.mrconfig': 'settings',
 | 
			
		||||
  '.yardopts': 'settings',
 | 
			
		||||
  'gradle.properties': 'gradle',
 | 
			
		||||
  gradlew: 'gradle',
 | 
			
		||||
  'gradle-wrapper.properties': 'gradle',
 | 
			
		||||
  license: 'certificate',
 | 
			
		||||
  'license.md': 'certificate',
 | 
			
		||||
  'license.md.rendered': 'certificate',
 | 
			
		||||
  'license.txt': 'certificate',
 | 
			
		||||
  licence: 'certificate',
 | 
			
		||||
  'licence.md': 'certificate',
 | 
			
		||||
  'licence.md.rendered': 'certificate',
 | 
			
		||||
  'licence.txt': 'certificate',
 | 
			
		||||
  dockerfile: 'docker',
 | 
			
		||||
  'docker-compose.yml': 'docker',
 | 
			
		||||
  '.mailmap': 'email',
 | 
			
		||||
  '.gitignore': 'git',
 | 
			
		||||
  '.gitconfig': 'git',
 | 
			
		||||
  '.gitattributes': 'git',
 | 
			
		||||
  '.gitmodules': 'git',
 | 
			
		||||
  '.gitkeep': 'git',
 | 
			
		||||
  'git-history': 'git',
 | 
			
		||||
  '.Rhistory': 'r',
 | 
			
		||||
  'cmakelists.txt': 'cmake',
 | 
			
		||||
  'cmakecache.txt': 'cmake',
 | 
			
		||||
  'angular-cli.json': 'angular',
 | 
			
		||||
  '.angular-cli.json': 'angular',
 | 
			
		||||
  '.vfl': 'vfl',
 | 
			
		||||
  '.kl': 'kl',
 | 
			
		||||
  'postcss.config.js': 'postcss',
 | 
			
		||||
  '.postcssrc.js': 'postcss',
 | 
			
		||||
  'project.graphcool': 'graphcool',
 | 
			
		||||
  'webpack.js': 'webpack',
 | 
			
		||||
  'webpack.ts': 'webpack',
 | 
			
		||||
  'webpack.base.js': 'webpack',
 | 
			
		||||
  'webpack.base.ts': 'webpack',
 | 
			
		||||
  'webpack.config.js': 'webpack',
 | 
			
		||||
  'webpack.config.ts': 'webpack',
 | 
			
		||||
  'webpack.common.js': 'webpack',
 | 
			
		||||
  'webpack.common.ts': 'webpack',
 | 
			
		||||
  'webpack.config.common.js': 'webpack',
 | 
			
		||||
  'webpack.config.common.ts': 'webpack',
 | 
			
		||||
  'webpack.config.common.babel.js': 'webpack',
 | 
			
		||||
  'webpack.config.common.babel.ts': 'webpack',
 | 
			
		||||
  'webpack.dev.js': 'webpack',
 | 
			
		||||
  'webpack.dev.ts': 'webpack',
 | 
			
		||||
  'webpack.config.dev.js': 'webpack',
 | 
			
		||||
  'webpack.config.dev.ts': 'webpack',
 | 
			
		||||
  'webpack.config.dev.babel.js': 'webpack',
 | 
			
		||||
  'webpack.config.dev.babel.ts': 'webpack',
 | 
			
		||||
  'webpack.prod.js': 'webpack',
 | 
			
		||||
  'webpack.prod.ts': 'webpack',
 | 
			
		||||
  'webpack.server.js': 'webpack',
 | 
			
		||||
  'webpack.server.ts': 'webpack',
 | 
			
		||||
  'webpack.client.js': 'webpack',
 | 
			
		||||
  'webpack.client.ts': 'webpack',
 | 
			
		||||
  'webpack.config.server.js': 'webpack',
 | 
			
		||||
  'webpack.config.server.ts': 'webpack',
 | 
			
		||||
  'webpack.config.client.js': 'webpack',
 | 
			
		||||
  'webpack.config.client.ts': 'webpack',
 | 
			
		||||
  'webpack.config.production.babel.js': 'webpack',
 | 
			
		||||
  'webpack.config.production.babel.ts': 'webpack',
 | 
			
		||||
  'webpack.config.prod.babel.js': 'webpack',
 | 
			
		||||
  'webpack.config.prod.babel.ts': 'webpack',
 | 
			
		||||
  'webpack.config.prod.js': 'webpack',
 | 
			
		||||
  'webpack.config.prod.ts': 'webpack',
 | 
			
		||||
  'webpack.config.production.js': 'webpack',
 | 
			
		||||
  'webpack.config.production.ts': 'webpack',
 | 
			
		||||
  'webpack.config.staging.js': 'webpack',
 | 
			
		||||
  'webpack.config.staging.ts': 'webpack',
 | 
			
		||||
  'webpack.config.babel.js': 'webpack',
 | 
			
		||||
  'webpack.config.babel.ts': 'webpack',
 | 
			
		||||
  'webpack.config.base.babel.js': 'webpack',
 | 
			
		||||
  'webpack.config.base.babel.ts': 'webpack',
 | 
			
		||||
  'webpack.config.base.js': 'webpack',
 | 
			
		||||
  'webpack.config.base.ts': 'webpack',
 | 
			
		||||
  'webpack.config.staging.babel.js': 'webpack',
 | 
			
		||||
  'webpack.config.staging.babel.ts': 'webpack',
 | 
			
		||||
  'webpack.config.coffee': 'webpack',
 | 
			
		||||
  'webpack.config.test.js': 'webpack',
 | 
			
		||||
  'webpack.config.test.ts': 'webpack',
 | 
			
		||||
  'webpack.config.vendor.js': 'webpack',
 | 
			
		||||
  'webpack.config.vendor.ts': 'webpack',
 | 
			
		||||
  'webpack.config.vendor.production.js': 'webpack',
 | 
			
		||||
  'webpack.config.vendor.production.ts': 'webpack',
 | 
			
		||||
  'webpack.test.js': 'webpack',
 | 
			
		||||
  'webpack.test.ts': 'webpack',
 | 
			
		||||
  'webpack.dist.js': 'webpack',
 | 
			
		||||
  'webpack.dist.ts': 'webpack',
 | 
			
		||||
  'webpackfile.js': 'webpack',
 | 
			
		||||
  'webpackfile.ts': 'webpack',
 | 
			
		||||
  'ionic.config.json': 'ionic',
 | 
			
		||||
  '.io-config.json': 'ionic',
 | 
			
		||||
  'gulpfile.js': 'gulp',
 | 
			
		||||
  'gulpfile.ts': 'gulp',
 | 
			
		||||
  'gulpfile.babel.js': 'gulp',
 | 
			
		||||
  'package.json': 'nodejs',
 | 
			
		||||
  'package-lock.json': 'nodejs',
 | 
			
		||||
  '.nvmrc': 'nodejs',
 | 
			
		||||
  '.npmignore': 'npm',
 | 
			
		||||
  '.npmrc': 'npm',
 | 
			
		||||
  '.yarnrc': 'yarn',
 | 
			
		||||
  'yarn.lock': 'yarn',
 | 
			
		||||
  '.yarnclean': 'yarn',
 | 
			
		||||
  '.yarn-integrity': 'yarn',
 | 
			
		||||
  'yarn-error.log': 'yarn',
 | 
			
		||||
  'androidmanifest.xml': 'android',
 | 
			
		||||
  '.env': 'tune',
 | 
			
		||||
  '.env.example': 'tune',
 | 
			
		||||
  '.babelrc': 'babel',
 | 
			
		||||
  'contributing.md': 'contributing',
 | 
			
		||||
  'contributing.md.rendered': 'contributing',
 | 
			
		||||
  'readme.md': 'readme',
 | 
			
		||||
  'readme.md.rendered': 'readme',
 | 
			
		||||
  changelog: 'changelog',
 | 
			
		||||
  'changelog.md': 'changelog',
 | 
			
		||||
  'changelog.md.rendered': 'changelog',
 | 
			
		||||
  CREDITS: 'credits',
 | 
			
		||||
  'credits.txt': 'credits',
 | 
			
		||||
  'credits.md': 'credits',
 | 
			
		||||
  'credits.md.rendered': 'credits',
 | 
			
		||||
  '.flowconfig': 'flow',
 | 
			
		||||
  'favicon.ico': 'favicon',
 | 
			
		||||
  'karma.conf.js': 'karma',
 | 
			
		||||
  'karma.conf.ts': 'karma',
 | 
			
		||||
  'karma.conf.coffee': 'karma',
 | 
			
		||||
  'karma.config.js': 'karma',
 | 
			
		||||
  'karma.config.ts': 'karma',
 | 
			
		||||
  'karma-main.js': 'karma',
 | 
			
		||||
  'karma-main.ts': 'karma',
 | 
			
		||||
  '.bithoundrc': 'bithound',
 | 
			
		||||
  'appveyor.yml': 'appveyor',
 | 
			
		||||
  '.travis.yml': 'travis',
 | 
			
		||||
  'protractor.conf.js': 'protractor',
 | 
			
		||||
  'protractor.conf.ts': 'protractor',
 | 
			
		||||
  'protractor.conf.coffee': 'protractor',
 | 
			
		||||
  'protractor.config.js': 'protractor',
 | 
			
		||||
  'protractor.config.ts': 'protractor',
 | 
			
		||||
  'fuse.js': 'fusebox',
 | 
			
		||||
  procfile: 'heroku',
 | 
			
		||||
  '.editorconfig': 'editorconfig',
 | 
			
		||||
  '.gitlab-ci.yml': 'gitlab',
 | 
			
		||||
  '.bowerrc': 'bower',
 | 
			
		||||
  'bower.json': 'bower',
 | 
			
		||||
  '.eslintrc.js': 'eslint',
 | 
			
		||||
  '.eslintrc.yaml': 'eslint',
 | 
			
		||||
  '.eslintrc.yml': 'eslint',
 | 
			
		||||
  '.eslintrc.json': 'eslint',
 | 
			
		||||
  '.eslintrc': 'eslint',
 | 
			
		||||
  '.eslintignore': 'eslint',
 | 
			
		||||
  'code_of_conduct.md': 'conduct',
 | 
			
		||||
  'code_of_conduct.md.rendered': 'conduct',
 | 
			
		||||
  '.watchmanconfig': 'watchman',
 | 
			
		||||
  'aurelia.json': 'aurelia',
 | 
			
		||||
  'mocha.opts': 'mocha',
 | 
			
		||||
  jenkinsfile: 'jenkins',
 | 
			
		||||
  'firebase.json': 'firebase',
 | 
			
		||||
  '.firebaserc': 'firebase',
 | 
			
		||||
  'rollup.config.js': 'rollup',
 | 
			
		||||
  'rollup.config.ts': 'rollup',
 | 
			
		||||
  'rollup-config.js': 'rollup',
 | 
			
		||||
  'rollup-config.ts': 'rollup',
 | 
			
		||||
  'rollup.config.prod.js': 'rollup',
 | 
			
		||||
  'rollup.config.prod.ts': 'rollup',
 | 
			
		||||
  'rollup.config.dev.js': 'rollup',
 | 
			
		||||
  'rollup.config.dev.ts': 'rollup',
 | 
			
		||||
  'rollup.config.prod.vendor.js': 'rollup',
 | 
			
		||||
  'rollup.config.prod.vendor.ts': 'rollup',
 | 
			
		||||
  '.hhconfig': 'hack',
 | 
			
		||||
  '.stylelintrc': 'stylelint',
 | 
			
		||||
  'stylelint.config.js': 'stylelint',
 | 
			
		||||
  '.stylelintrc.json': 'stylelint',
 | 
			
		||||
  '.stylelintrc.yaml': 'stylelint',
 | 
			
		||||
  '.stylelintrc.yml': 'stylelint',
 | 
			
		||||
  '.stylelintrc.js': 'stylelint',
 | 
			
		||||
  '.stylelintignore': 'stylelint',
 | 
			
		||||
  '.codeclimate.yml': 'code-climate',
 | 
			
		||||
  '.prettierrc': 'prettier',
 | 
			
		||||
  'prettier.config.js': 'prettier',
 | 
			
		||||
  '.prettierrc.js': 'prettier',
 | 
			
		||||
  '.prettierrc.json': 'prettier',
 | 
			
		||||
  '.prettierrc.yaml': 'prettier',
 | 
			
		||||
  '.prettierrc.yml': 'prettier',
 | 
			
		||||
  'nodemon.json': 'nodemon',
 | 
			
		||||
  '.sonarrc': 'sonar',
 | 
			
		||||
  browserslist: 'browserlist',
 | 
			
		||||
  '.browserslistrc': 'browserlist',
 | 
			
		||||
  '.snyk': 'snyk',
 | 
			
		||||
  '.drone.yml': 'drone',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function getIconForFile(name) {
 | 
			
		||||
  return fileNameIcons[name] ||
 | 
			
		||||
         fileExtensionIcons[name ? name.split('.').pop() : ''] ||
 | 
			
		||||
         '';
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,91 @@
 | 
			
		|||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    startSize: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    side: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    minSize: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: 0,
 | 
			
		||||
    },
 | 
			
		||||
    maxSize: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: Number.MAX_VALUE,
 | 
			
		||||
    },
 | 
			
		||||
    enabled: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      size: this.startSize,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    className() {
 | 
			
		||||
      return `drag${this.side}`;
 | 
			
		||||
    },
 | 
			
		||||
    cursorStyle() {
 | 
			
		||||
      if (this.enabled) {
 | 
			
		||||
        return { cursor: 'ew-resize' };
 | 
			
		||||
      }
 | 
			
		||||
      return {};
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    resetSize(e) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      this.size = this.startSize;
 | 
			
		||||
      this.$emit('update:size', this.size);
 | 
			
		||||
    },
 | 
			
		||||
    startDrag(e) {
 | 
			
		||||
      if (this.enabled) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        this.startPos = e.clientX;
 | 
			
		||||
        this.currentStartSize = this.size;
 | 
			
		||||
        document.addEventListener('mousemove', this.drag);
 | 
			
		||||
        document.addEventListener('mouseup', this.endDrag, { once: true });
 | 
			
		||||
        this.$emit('resize-start', this.size);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    drag(e) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      let moved = e.clientX - this.startPos;
 | 
			
		||||
      if (this.side === 'left') moved = -moved;
 | 
			
		||||
      let newSize = this.currentStartSize + moved;
 | 
			
		||||
      if (newSize < this.minSize) {
 | 
			
		||||
        newSize = this.minSize;
 | 
			
		||||
      } else if (newSize > this.maxSize) {
 | 
			
		||||
        newSize = this.maxSize;
 | 
			
		||||
      }
 | 
			
		||||
      this.size = newSize;
 | 
			
		||||
 | 
			
		||||
      this.$emit('update:size', newSize);
 | 
			
		||||
    },
 | 
			
		||||
    endDrag(e) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      document.removeEventListener('mousemove', this.drag);
 | 
			
		||||
      this.$emit('resize-end', this.size);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div 
 | 
			
		||||
    class="dragHandle"
 | 
			
		||||
    :class="className"
 | 
			
		||||
    :style="cursorStyle"
 | 
			
		||||
    @mousedown="startDrag"
 | 
			
		||||
    @dblclick="resetSize"
 | 
			
		||||
  ></div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +71,7 @@
 | 
			
		|||
  vertical-align: top;
 | 
			
		||||
 | 
			
		||||
  &.s16 { font-size: 12px; line-height: 1.33; }
 | 
			
		||||
  &.s24 { font-size: 14px; line-height: 1.8; }
 | 
			
		||||
  &.s24 { font-size: 13px; line-height: 1.8; }
 | 
			
		||||
  &.s26 { font-size: 20px; line-height: 1.33; }
 | 
			
		||||
  &.s32 { font-size: 20px; line-height: 30px; }
 | 
			
		||||
  &.s40 { font-size: 16px; line-height: 38px; }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,6 @@
 | 
			
		|||
  color: $gl-text-color;
 | 
			
		||||
  font-weight: $gl-font-weight-normal;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  line-height: 36px;
 | 
			
		||||
 | 
			
		||||
  &.diff-collapsed {
 | 
			
		||||
    padding: 5px;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@
 | 
			
		|||
.context-header {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin-right: 2px;
 | 
			
		||||
  width: $contextual-sidebar-width;
 | 
			
		||||
 | 
			
		||||
  a {
 | 
			
		||||
    transition: padding $sidebar-transition-duration;
 | 
			
		||||
| 
						 | 
				
			
			@ -358,10 +359,6 @@
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  .sidebar-top-level-items > li {
 | 
			
		||||
    &.active a {
 | 
			
		||||
      padding-left: 12px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sidebar-sub-level-items {
 | 
			
		||||
      &:not(.flyout-list) {
 | 
			
		||||
        display: none;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -516,7 +516,7 @@
 | 
			
		|||
.header-user {
 | 
			
		||||
  .dropdown-menu-nav {
 | 
			
		||||
    width: auto;
 | 
			
		||||
    min-width: 140px;
 | 
			
		||||
    min-width: 160px;
 | 
			
		||||
    margin-top: 4px;
 | 
			
		||||
    color: $gl-text-color;
 | 
			
		||||
    left: auto;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@
 | 
			
		|||
    padding: 10px 15px;
 | 
			
		||||
    min-height: 20px;
 | 
			
		||||
    border-bottom: 1px solid $list-border;
 | 
			
		||||
    word-wrap: break-word;
 | 
			
		||||
 | 
			
		||||
    &::after {
 | 
			
		||||
      content: " ";
 | 
			
		||||
| 
						 | 
				
			
			@ -125,10 +126,8 @@ ul.content-list {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    .description {
 | 
			
		||||
      p {
 | 
			
		||||
      @include str-truncated;
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
      }
 | 
			
		||||
      color: $gl-text-color-secondary;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .controls {
 | 
			
		||||
| 
						 | 
				
			
			@ -314,7 +313,7 @@ ul.indent-list {
 | 
			
		|||
        border: 2px solid $white-normal;
 | 
			
		||||
 | 
			
		||||
        &.identicon {
 | 
			
		||||
          line-height: 30px;
 | 
			
		||||
          line-height: 15px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -348,14 +347,19 @@ ul.indent-list {
 | 
			
		|||
 | 
			
		||||
  .folder-caret {
 | 
			
		||||
    width: 15px;
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      margin-bottom: 2px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .item-type-icon {
 | 
			
		||||
    margin-top: 2px;
 | 
			
		||||
    width: 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  > .group-row:not(.has-children) {
 | 
			
		||||
    .folder-caret .fa {
 | 
			
		||||
    .folder-caret {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -438,12 +442,61 @@ ul.indent-list {
 | 
			
		|||
 | 
			
		||||
    .avatar-container > a {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      text-decoration: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.has-more-items {
 | 
			
		||||
      display: block;
 | 
			
		||||
      padding: 20px 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .stats {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      line-height: 46px;
 | 
			
		||||
 | 
			
		||||
      > span {
 | 
			
		||||
        display: inline-flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        height: 16px;
 | 
			
		||||
        min-width: 30px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      > span:last-child {
 | 
			
		||||
        margin-right: 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .stat-value {
 | 
			
		||||
        margin: 2px 0 0 5px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .controls {
 | 
			
		||||
      margin-left: 5px;
 | 
			
		||||
 | 
			
		||||
      > .btn {
 | 
			
		||||
        margin-right: $btn-xs-side-margin;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .project-row-contents .stats {
 | 
			
		||||
    line-height: inherit;
 | 
			
		||||
 | 
			
		||||
    > span:first-child {
 | 
			
		||||
      margin-left: 25px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .item-visibility {
 | 
			
		||||
      margin-right: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .last-updated {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: 12px;
 | 
			
		||||
      min-width: 250px;
 | 
			
		||||
      text-align: right;
 | 
			
		||||
      color: $gl-text-color-secondary;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -455,12 +508,12 @@ ul.indent-list {
 | 
			
		|||
 | 
			
		||||
ul.group-list-tree {
 | 
			
		||||
  li.group-row {
 | 
			
		||||
    &.has-description .title {
 | 
			
		||||
      line-height: inherit;
 | 
			
		||||
    > .group-row-contents .title {
 | 
			
		||||
      line-height: $list-text-height;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:not(.has-description) .title {
 | 
			
		||||
      line-height: $list-text-height;
 | 
			
		||||
    &.has-description > .group-row-contents .title {
 | 
			
		||||
      line-height: inherit;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -178,6 +178,10 @@
 | 
			
		|||
    font-weight: inherit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  dd {
 | 
			
		||||
    margin-left: $gl-padding;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ul,
 | 
			
		||||
  ol {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -727,3 +727,8 @@ Popup
 | 
			
		|||
$popup-triangle-size: 15px;
 | 
			
		||||
$popup-triangle-border-size: 1px;
 | 
			
		||||
$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Multi file editor
 | 
			
		||||
*/
 | 
			
		||||
$border-color-settings: #e1e1e1;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -159,7 +159,6 @@
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Last push widget
 | 
			
		||||
 */
 | 
			
		||||
| 
						 | 
				
			
			@ -182,6 +181,12 @@
 | 
			
		|||
  .event-item {
 | 
			
		||||
    padding-left: 0;
 | 
			
		||||
 | 
			
		||||
    &.event-inline {
 | 
			
		||||
      .event-title {
 | 
			
		||||
        line-height: 20px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .event-title {
 | 
			
		||||
      white-space: normal;
 | 
			
		||||
      overflow: visible;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,22 @@
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.multi-file-editor-options {
 | 
			
		||||
  label {
 | 
			
		||||
    margin-right: 20px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .preview {
 | 
			
		||||
    font-size: 0;
 | 
			
		||||
 | 
			
		||||
    img {
 | 
			
		||||
      border: 1px solid $border-color-settings;
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.application-theme {
 | 
			
		||||
  label {
 | 
			
		||||
    margin-right: 20px;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,10 +36,6 @@
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.with-performance-bar .ide-view {
 | 
			
		||||
  height: calc(100vh - #{$header-height});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ide-file-list {
 | 
			
		||||
  flex: 1;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -96,8 +92,14 @@
 | 
			
		|||
  padding: 6px 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.multi-file-table-name {
 | 
			
		||||
table.table tr td.multi-file-table-name {
 | 
			
		||||
  width: 350px;
 | 
			
		||||
  padding: 6px 12px;
 | 
			
		||||
 | 
			
		||||
  svg {
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
    margin-right: 2px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.multi-file-table-col-commit-message {
 | 
			
		||||
| 
						 | 
				
			
			@ -132,6 +134,10 @@
 | 
			
		|||
  border-bottom: 1px solid $white-dark;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  svg {
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.active {
 | 
			
		||||
    background-color: $white-light;
 | 
			
		||||
    border-bottom-color: $white-light;
 | 
			
		||||
| 
						 | 
				
			
			@ -232,12 +238,13 @@
 | 
			
		|||
 | 
			
		||||
.multi-file-commit-panel {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  width: 290px;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  background-color: $gray-light;
 | 
			
		||||
  border-left: 1px solid $white-dark;
 | 
			
		||||
  padding-right: 3px;
 | 
			
		||||
 | 
			
		||||
  .projects-sidebar {
 | 
			
		||||
    display: flex;
 | 
			
		||||
| 
						 | 
				
			
			@ -486,3 +493,30 @@
 | 
			
		|||
  margin-top: $header-height;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.with-performance-bar {
 | 
			
		||||
  .ide-flash-container.flash-container {
 | 
			
		||||
    margin-top: $header-height + $performance-bar-height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ide-view {
 | 
			
		||||
    height: calc(100vh - #{$header-height + $performance-bar-height});
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.dragHandle {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  width: 3px;
 | 
			
		||||
  background-color: $white-dark;
 | 
			
		||||
 | 
			
		||||
  &.dragright {
 | 
			
		||||
    right: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.dragleft {
 | 
			
		||||
    left: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -268,3 +268,7 @@
 | 
			
		|||
    margin: 0 0 5px 17px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deprecated-service {
 | 
			
		||||
  cursor: default;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
class PasswordsController < Devise::PasswordsController
 | 
			
		||||
  include Gitlab::CurrentSettings
 | 
			
		||||
 | 
			
		||||
  skip_before_action :require_no_authentication, only: [:edit, :update]
 | 
			
		||||
 | 
			
		||||
  before_action :resource_from_email, only: [:create]
 | 
			
		||||
  before_action :check_password_authentication_available, only: [:create]
 | 
			
		||||
  before_action :throttle_reset,      only: [:create]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,14 +46,16 @@ class Projects::BranchesController < Projects::ApplicationController
 | 
			
		|||
    result = CreateBranchService.new(project, current_user)
 | 
			
		||||
        .execute(branch_name, ref)
 | 
			
		||||
 | 
			
		||||
    if params[:issue_iid]
 | 
			
		||||
    success = (result[:status] == :success)
 | 
			
		||||
 | 
			
		||||
    if params[:issue_iid] && success
 | 
			
		||||
      issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid])
 | 
			
		||||
      SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    respond_to do |format|
 | 
			
		||||
      format.html do
 | 
			
		||||
        if result[:status] == :success
 | 
			
		||||
        if success
 | 
			
		||||
          if redirect_to_autodeploy
 | 
			
		||||
            redirect_to url_to_autodeploy_setup(project, branch_name),
 | 
			
		||||
              notice: view_context.autodeploy_flash_notice(branch_name)
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +69,7 @@ class Projects::BranchesController < Projects::ApplicationController
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      format.json do
 | 
			
		||||
        if result[:status] == :success
 | 
			
		||||
        if success
 | 
			
		||||
          render json: { name: branch_name, url: project_tree_url(@project, branch_name) }
 | 
			
		||||
        else
 | 
			
		||||
          render json: result[:messsage], status: :unprocessable_entity
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,17 +29,17 @@ class Projects::RunnersController < Projects::ApplicationController
 | 
			
		|||
 | 
			
		||||
  def resume
 | 
			
		||||
    if Ci::UpdateRunnerService.new(@runner).update(active: true)
 | 
			
		||||
      redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
 | 
			
		||||
      redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
 | 
			
		||||
    else
 | 
			
		||||
      redirect_to runner_path(@runner), alert: 'Runner was not updated.'
 | 
			
		||||
      redirect_to runners_path(@project), alert: 'Runner was not updated.'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pause
 | 
			
		||||
    if Ci::UpdateRunnerService.new(@runner).update(active: false)
 | 
			
		||||
      redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
 | 
			
		||||
      redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
 | 
			
		||||
    else
 | 
			
		||||
      redirect_to runner_path(@runner), alert: 'Runner was not updated.'
 | 
			
		||||
      redirect_to runners_path(@project), alert: 'Runner was not updated.'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,6 +30,13 @@ module IconsHelper
 | 
			
		|||
    ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def sprite_file_icons_path
 | 
			
		||||
    # SVG Sprites currently don't work across domains, so in the case of a CDN
 | 
			
		||||
    # we have to set the current path deliberately to prevent addition of asset_host
 | 
			
		||||
    sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host
 | 
			
		||||
    ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def sprite_icon(icon_name, size: nil, css_class: nil)
 | 
			
		||||
    css_classes = size ? "s#{size}" : ""
 | 
			
		||||
    css_classes << " #{css_class}" unless css_class.blank?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -389,7 +389,7 @@ module ProjectsHelper
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil)
 | 
			
		||||
    commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name.downcase }
 | 
			
		||||
    commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
 | 
			
		||||
    project_new_blob_path(
 | 
			
		||||
      project,
 | 
			
		||||
      project.default_branch || 'master',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,5 +27,16 @@ module ServicesHelper
 | 
			
		|||
    "#{event}_events"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def service_save_button(service)
 | 
			
		||||
    button_tag(class: 'btn btn-save', type: 'submit', disabled: service.deprecated?) do
 | 
			
		||||
      icon('spinner spin', class: 'hidden js-btn-spinner') +
 | 
			
		||||
        content_tag(:span, 'Save changes', class: 'js-btn-label')
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def disable_fields_service?(service)
 | 
			
		||||
    !current_controller?("admin/services") && service.deprecated?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  extend self
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -96,7 +96,7 @@ module Issuable
 | 
			
		|||
 | 
			
		||||
    strip_attributes :title
 | 
			
		||||
 | 
			
		||||
    after_save :record_metrics, unless: :imported?
 | 
			
		||||
    after_save :ensure_metrics, unless: :imported?
 | 
			
		||||
 | 
			
		||||
    # We want to use optimistic lock for cases when only title or description are involved
 | 
			
		||||
    # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
 | 
			
		||||
| 
						 | 
				
			
			@ -335,11 +335,6 @@ module Issuable
 | 
			
		|||
    false
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def record_metrics
 | 
			
		||||
    metrics = self.metrics || create_metrics
 | 
			
		||||
    metrics.record!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  ##
 | 
			
		||||
  # Override in issuable specialization
 | 
			
		||||
  #
 | 
			
		||||
| 
						 | 
				
			
			@ -347,6 +342,10 @@ module Issuable
 | 
			
		|||
    false
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ensure_metrics
 | 
			
		||||
    self.metrics || create_metrics
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  ##
 | 
			
		||||
  # Overriden in MergeRequest
 | 
			
		||||
  #
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,6 +34,8 @@ module Storage
 | 
			
		|||
      # So we basically we mute exceptions in next actions
 | 
			
		||||
      begin
 | 
			
		||||
        send_update_instructions
 | 
			
		||||
        write_projects_repository_config
 | 
			
		||||
 | 
			
		||||
        true
 | 
			
		||||
      rescue
 | 
			
		||||
        # Returning false does not rollback after_* transaction but gives
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,8 +23,13 @@ class DiffDiscussion < Discussion
 | 
			
		|||
  def merge_request_version_params
 | 
			
		||||
    return unless for_merge_request?
 | 
			
		||||
 | 
			
		||||
    version_params = get_params
 | 
			
		||||
 | 
			
		||||
    return version_params unless on_merge_request_commit? && commit_id
 | 
			
		||||
 | 
			
		||||
    version_params ||= {}
 | 
			
		||||
    version_params.tap do |params|
 | 
			
		||||
      params[:commit_id] = commit_id if on_merge_request_commit?
 | 
			
		||||
      params[:commit_id] = commit_id
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +42,7 @@ class DiffDiscussion < Discussion
 | 
			
		|||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def version_params
 | 
			
		||||
  def get_params
 | 
			
		||||
    return {} if active?
 | 
			
		||||
 | 
			
		||||
    noteable.version_params_for(position.diff_refs)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,7 +48,18 @@ class Event < ActiveRecord::Base
 | 
			
		|||
 | 
			
		||||
  belongs_to :author, class_name: "User"
 | 
			
		||||
  belongs_to :project
 | 
			
		||||
  belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
 | 
			
		||||
 | 
			
		||||
  belongs_to :target, -> {
 | 
			
		||||
    # If the association for "target" defines an "author" association we want to
 | 
			
		||||
    # eager-load this so Banzai & friends don't end up performing N+1 queries to
 | 
			
		||||
    # get the authors of notes, issues, etc.
 | 
			
		||||
    if reflections['events'].active_record.reflect_on_association(:author)
 | 
			
		||||
      includes(:author)
 | 
			
		||||
    else
 | 
			
		||||
      self
 | 
			
		||||
    end
 | 
			
		||||
  }, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
 | 
			
		||||
 | 
			
		||||
  has_one :push_event_payload
 | 
			
		||||
 | 
			
		||||
  # Callbacks
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -276,6 +276,11 @@ class Issue < ActiveRecord::Base
 | 
			
		|||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def ensure_metrics
 | 
			
		||||
    super
 | 
			
		||||
    metrics.record!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Returns `true` if the given User can read the current Issue.
 | 
			
		||||
  #
 | 
			
		||||
  # This method duplicates the same check of issue_policy.rb
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,6 @@
 | 
			
		|||
class MergeRequest::Metrics < ActiveRecord::Base
 | 
			
		||||
  belongs_to :merge_request
 | 
			
		||||
  belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
 | 
			
		||||
 | 
			
		||||
  def record!
 | 
			
		||||
    if merge_request.merged? && self.merged_at.blank?
 | 
			
		||||
      self.merged_at = Time.now
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    self.save
 | 
			
		||||
  end
 | 
			
		||||
  belongs_to :latest_closed_by, class_name: 'User'
 | 
			
		||||
  belongs_to :merged_by, class_name: 'User'
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -268,4 +268,11 @@ class Namespace < ActiveRecord::Base
 | 
			
		|||
  def namespace_previously_created_with_same_path?
 | 
			
		||||
    RedirectRoute.permanent.exists?(path: path)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def write_projects_repository_config
 | 
			
		||||
    all_projects.find_each do |project|
 | 
			
		||||
      project.expires_full_path_cache # we need to clear cache to validate renames correctly
 | 
			
		||||
      project.write_repository_config
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -226,7 +226,7 @@ class Project < ActiveRecord::Base
 | 
			
		|||
  delegate :name, to: :owner, allow_nil: true, prefix: true
 | 
			
		||||
  delegate :members, to: :team, prefix: true
 | 
			
		||||
  delegate :add_user, :add_users, to: :team
 | 
			
		||||
  delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
 | 
			
		||||
  delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
 | 
			
		||||
 | 
			
		||||
  # Validations
 | 
			
		||||
  validates :creator, presence: true, on: :create
 | 
			
		||||
| 
						 | 
				
			
			@ -639,7 +639,7 @@ class Project < ActiveRecord::Base
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def import?
 | 
			
		||||
    external_import? || forked? || gitlab_project_import?
 | 
			
		||||
    external_import? || forked? || gitlab_project_import? || bare_repository_import?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def no_import?
 | 
			
		||||
| 
						 | 
				
			
			@ -679,6 +679,10 @@ class Project < ActiveRecord::Base
 | 
			
		|||
    Gitlab::UrlSanitizer.new(import_url).masked_url
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def bare_repository_import?
 | 
			
		||||
    import_type == 'bare_repository'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def gitlab_project_import?
 | 
			
		||||
    import_type == 'gitlab_project'
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -1416,6 +1420,8 @@ class Project < ActiveRecord::Base
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def after_rename_repo
 | 
			
		||||
    write_repository_config
 | 
			
		||||
 | 
			
		||||
    path_before_change = previous_changes['path'].first
 | 
			
		||||
 | 
			
		||||
    # We need to check if project had been rolled out to move resource to hashed storage or not and decide
 | 
			
		||||
| 
						 | 
				
			
			@ -1428,6 +1434,16 @@ class Project < ActiveRecord::Base
 | 
			
		|||
    Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def write_repository_config(gl_full_path: full_path)
 | 
			
		||||
    # We'd need to keep track of project full path otherwise directory tree
 | 
			
		||||
    # created with hashed storage enabled cannot be usefully imported using
 | 
			
		||||
    # the import rake task.
 | 
			
		||||
    repo.config['gitlab.fullpath'] = gl_full_path
 | 
			
		||||
  rescue Gitlab::Git::Repository::NoRepository => e
 | 
			
		||||
    Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
 | 
			
		||||
    nil
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def rename_repo_notify!
 | 
			
		||||
    send_move_instructions(full_path_was)
 | 
			
		||||
    expires_full_path_cache
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,7 @@ class KubernetesService < DeploymentService
 | 
			
		|||
 | 
			
		||||
  before_validation :enforce_namespace_to_lower_case
 | 
			
		||||
 | 
			
		||||
  validate :deprecation_validation, unless: :template?
 | 
			
		||||
  validates :namespace,
 | 
			
		||||
    allow_blank: true,
 | 
			
		||||
    length: 1..63,
 | 
			
		||||
| 
						 | 
				
			
			@ -145,6 +146,17 @@ class KubernetesService < DeploymentService
 | 
			
		|||
    @kubeclient ||= build_kubeclient!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def deprecated?
 | 
			
		||||
    !active
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def deprecation_message
 | 
			
		||||
    content = <<-MESSAGE.strip_heredoc
 | 
			
		||||
    Kubernetes service integration has been deprecated. #{deprecated_message_content} your clusters using the new <a href=\'#{Gitlab::Routing.url_helpers.project_clusters_path(project)}'/>Clusters</a> page
 | 
			
		||||
      MESSAGE
 | 
			
		||||
    content.html_safe
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
| 
						 | 
				
			
			@ -226,4 +238,20 @@ class KubernetesService < DeploymentService
 | 
			
		|||
  def enforce_namespace_to_lower_case
 | 
			
		||||
    self.namespace = self.namespace&.downcase
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def deprecation_validation
 | 
			
		||||
    return if active_changed?(from: true, to: false)
 | 
			
		||||
 | 
			
		||||
    if deprecated?
 | 
			
		||||
      errors[:base] << deprecation_message
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def deprecated_message_content
 | 
			
		||||
    if active?
 | 
			
		||||
      "Your cluster information on this page is still editable, but you are advised to disable and reconfigure"
 | 
			
		||||
    else
 | 
			
		||||
      "Fields on this page are now uneditable, you can configure"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,36 +7,24 @@ class ProjectTeam
 | 
			
		|||
    @project = project
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Shortcut to add users
 | 
			
		||||
  #
 | 
			
		||||
  # Use:
 | 
			
		||||
  #   @team << [@user, :master]
 | 
			
		||||
  #   @team << [@users, :master]
 | 
			
		||||
  #
 | 
			
		||||
  def <<(args)
 | 
			
		||||
    users, access, current_user = *args
 | 
			
		||||
 | 
			
		||||
    if users.respond_to?(:each)
 | 
			
		||||
      add_users(users, access, current_user: current_user)
 | 
			
		||||
    else
 | 
			
		||||
      add_user(users, access, current_user: current_user)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def add_guest(user, current_user: nil)
 | 
			
		||||
    self << [user, :guest, current_user]
 | 
			
		||||
    add_user(user, :guest, current_user: current_user)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def add_reporter(user, current_user: nil)
 | 
			
		||||
    self << [user, :reporter, current_user]
 | 
			
		||||
    add_user(user, :reporter, current_user: current_user)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def add_developer(user, current_user: nil)
 | 
			
		||||
    self << [user, :developer, current_user]
 | 
			
		||||
    add_user(user, :developer, current_user: current_user)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def add_master(user, current_user: nil)
 | 
			
		||||
    self << [user, :master, current_user]
 | 
			
		||||
    add_user(user, :master, current_user: current_user)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def add_role(user, role, current_user: nil)
 | 
			
		||||
    send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def find_member(user_id)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1010,10 +1010,6 @@ class Repository
 | 
			
		|||
    raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def remote_exists?(name)
 | 
			
		||||
    raw_repository.remote_exists?(name)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
 | 
			
		||||
    raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight)
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -263,6 +263,14 @@ class Service < ActiveRecord::Base
 | 
			
		|||
    service
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def deprecated?
 | 
			
		||||
    false
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def deprecation_message
 | 
			
		||||
    nil
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def cache_project_has_external_issue_tracker
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -94,8 +94,8 @@ class User < ActiveRecord::Base
 | 
			
		|||
  has_one :user_synced_attributes_metadata, autosave: true
 | 
			
		||||
 | 
			
		||||
  # Groups
 | 
			
		||||
  has_many :members, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
 | 
			
		||||
  has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, source: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
 | 
			
		||||
  has_many :members
 | 
			
		||||
  has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
 | 
			
		||||
  has_many :groups, through: :group_members
 | 
			
		||||
  has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
 | 
			
		||||
  has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
 | 
			
		||||
| 
						 | 
				
			
			@ -103,7 +103,7 @@ class User < ActiveRecord::Base
 | 
			
		|||
  # Projects
 | 
			
		||||
  has_many :groups_projects,          through: :groups, source: :projects
 | 
			
		||||
  has_many :personal_projects,        through: :namespace, source: :projects
 | 
			
		||||
  has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
 | 
			
		||||
  has_many :project_members, -> { where(requested_at: nil) }
 | 
			
		||||
  has_many :projects,                 through: :project_members
 | 
			
		||||
  has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
 | 
			
		||||
  has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
 | 
			
		||||
| 
						 | 
				
			
			@ -794,10 +794,7 @@ class User < ActiveRecord::Base
 | 
			
		|||
    # `User.select(:id)` raises
 | 
			
		||||
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
 | 
			
		||||
    # without this safeguard!
 | 
			
		||||
    return unless has_attribute?(:projects_limit)
 | 
			
		||||
 | 
			
		||||
    connection_default_value_defined = new_record? && !projects_limit_changed?
 | 
			
		||||
    return unless projects_limit.nil? || connection_default_value_defined
 | 
			
		||||
    return unless has_attribute?(:projects_limit) && projects_limit.nil?
 | 
			
		||||
 | 
			
		||||
    self.projects_limit = current_application_settings.default_projects_limit
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
class EventEntity < Grape::Entity
 | 
			
		||||
  expose :author, using: UserEntity
 | 
			
		||||
  expose :updated_at
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
class MergeRequestMetricsEntity < Grape::Entity
 | 
			
		||||
  expose :latest_closed_at, as: :closed_at
 | 
			
		||||
  expose :merged_at
 | 
			
		||||
  expose :latest_closed_by, as: :closed_by, using: UserEntity
 | 
			
		||||
  expose :merged_by, using: UserEntity
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -17,9 +17,11 @@ class MergeRequestWidgetEntity < IssuableEntity
 | 
			
		|||
    merge_request.project.merge_requests_ff_only_enabled
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Events
 | 
			
		||||
  expose :merge_event, using: EventEntity
 | 
			
		||||
  expose :closed_event, using: EventEntity
 | 
			
		||||
  expose :metrics do |merge_request|
 | 
			
		||||
    metrics = build_metrics(merge_request)
 | 
			
		||||
 | 
			
		||||
    MergeRequestMetricsEntity.new(metrics).as_json
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # User entities
 | 
			
		||||
  expose :merge_user, using: UserEntity
 | 
			
		||||
| 
						 | 
				
			
			@ -178,4 +180,27 @@ class MergeRequestWidgetEntity < IssuableEntity
 | 
			
		|||
    @presenters ||= {}
 | 
			
		||||
    @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Once SchedulePopulateMergeRequestMetricsWithEventsData fully runs,
 | 
			
		||||
  # we can remove this method and just serialize MergeRequest#metrics
 | 
			
		||||
  # instead. See https://gitlab.com/gitlab-org/gitlab-ce/issues/41587
 | 
			
		||||
  def build_metrics(merge_request)
 | 
			
		||||
    # There's no need to query and serialize metrics data for merge requests that are not
 | 
			
		||||
    # merged or closed.
 | 
			
		||||
    return unless merge_request.merged? || merge_request.closed?
 | 
			
		||||
    return merge_request.metrics if merge_request.merged? && merge_request.metrics&.merged_by_id
 | 
			
		||||
    return merge_request.metrics if merge_request.closed? && merge_request.metrics&.latest_closed_by_id
 | 
			
		||||
 | 
			
		||||
    build_metrics_from_events(merge_request)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def build_metrics_from_events(merge_request)
 | 
			
		||||
    closed_event = merge_request.closed_event
 | 
			
		||||
    merge_event = merge_request.merge_event
 | 
			
		||||
 | 
			
		||||
    MergeRequest::Metrics.new(latest_closed_at: closed_event&.updated_at,
 | 
			
		||||
                              latest_closed_by: closed_event&.author,
 | 
			
		||||
                              merged_at: merge_event&.updated_at,
 | 
			
		||||
                              merged_by: merge_event&.author)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -103,6 +103,6 @@ class EventCreateService
 | 
			
		|||
      author_id: current_user.id
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    Event.create(attributes)
 | 
			
		||||
    Event.create!(attributes)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
class MergeRequestMetricsService
 | 
			
		||||
  delegate :update!, to: :@merge_request_metrics
 | 
			
		||||
 | 
			
		||||
  def initialize(merge_request_metrics)
 | 
			
		||||
    @merge_request_metrics = merge_request_metrics
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def merge(event)
 | 
			
		||||
    update!(merged_by_id: event.author_id, merged_at: event.created_at)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def close(event)
 | 
			
		||||
    update!(latest_closed_by_id: event.author_id, latest_closed_at: event.created_at)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def reopen
 | 
			
		||||
    update!(latest_closed_by_id: nil, latest_closed_at: nil)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +24,10 @@ module MergeRequests
 | 
			
		|||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def merge_request_metrics_service(merge_request)
 | 
			
		||||
      MergeRequestMetricsService.new(merge_request.metrics)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def create_assignee_note(merge_request)
 | 
			
		||||
      SystemNoteService.change_assignee(
 | 
			
		||||
        merge_request, merge_request.project, current_user, merge_request.assignee)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ module MergeRequests
 | 
			
		|||
      merge_request.allow_broken = true
 | 
			
		||||
 | 
			
		||||
      if merge_request.close
 | 
			
		||||
        event_service.close_mr(merge_request, current_user)
 | 
			
		||||
        create_event(merge_request)
 | 
			
		||||
        create_note(merge_request)
 | 
			
		||||
        notification_service.close_mr(merge_request, current_user)
 | 
			
		||||
        todo_service.close_merge_request(merge_request, current_user)
 | 
			
		||||
| 
						 | 
				
			
			@ -19,5 +19,16 @@ module MergeRequests
 | 
			
		|||
 | 
			
		||||
      merge_request
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def create_event(merge_request)
 | 
			
		||||
      # Making sure MergeRequest::Metrics updates are in sync with
 | 
			
		||||
      # Event creation.
 | 
			
		||||
      Event.transaction do
 | 
			
		||||
        close_event = event_service.close_mr(merge_request, current_user)
 | 
			
		||||
        merge_request_metrics_service(merge_request).close(close_event)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ module MergeRequests
 | 
			
		|||
          # when there are no conflict files.
 | 
			
		||||
          conflicts.files.each(&:lines)
 | 
			
		||||
          @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
 | 
			
		||||
        rescue Rugged::OdbError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
 | 
			
		||||
        rescue Gitlab::Git::CommandError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
 | 
			
		||||
          @conflicts_can_be_resolved_in_ui = false
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue