Commit 5f271a9f authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into fix/gb/pipeline-retry-builds-started

* master: (313 commits)
  Allow slashes in slash command arguments
  Add API endpoint to get all milestone merge requests
  remove trailing comma
  Restore pagination to admin abuse reports
  replace deprecated NoErrorsPlugin with NoEmitOnErrorsPlugin
  only compress assets in production
  Reduce number of pipelines created to test pagination
  add CHANGELOG.md entry for !8761
  prevent diff unfolding link from appearing for deleted files
  fix build failures
  only show diff unfolding link if there are more lines to show
  fix typo in node section
  Only yield valid references in ReferenceFilter.references_in
  Cache js selectors; fix css
  move "Install node modules" step before "Migrate DB" within update process
  Renders pagination again for pipelines table
  update migration docs for 8.17 to include minimum node version
  Add CHANGELOG file
  Fix positioning of top scroll button
  Remove comments in migration
  ...
parents 953e590b 827a56a4
...@@ -12,12 +12,18 @@ ...@@ -12,12 +12,18 @@
"localStorage": false "localStorage": false
}, },
"plugins": [ "plugins": [
"filenames" "filenames",
"import"
], ],
"settings": {
"import/resolver": {
"webpack": {
"config": "./config/webpack.config.js"
}
}
},
"rules": { "rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"], "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
"no-multiple-empty-lines": ["error", { "max": 1 }], "no-multiple-empty-lines": ["error", { "max": 1 }]
"import/no-extraneous-dependencies": "off",
"import/no-unresolved": "off"
} }
} }
...@@ -107,7 +107,10 @@ setup-test-env: ...@@ -107,7 +107,10 @@ setup-test-env:
<<: *dedicated-runner <<: *dedicated-runner
stage: prepare stage: prepare
script: script:
- npm install - node --version
- yarn --version
- yarn install --pure-lockfile
- yarn check # ensure that yarn.lock matches package.json
- bundle exec rake gitlab:assets:compile - bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts: artifacts:
...@@ -246,13 +249,12 @@ karma: ...@@ -246,13 +249,12 @@ karma:
<<: *use-db <<: *use-db
<<: *dedicated-runner <<: *dedicated-runner
script: script:
- npm link istanbul
- bundle exec rake karma - bundle exec rake karma
artifacts: artifacts:
name: coverage-javascript name: coverage-javascript
expire_in: 31d expire_in: 31d
paths: paths:
- coverage-javascript/default/ - coverage-javascript/
lint-doc: lint-doc:
stage: test stage: test
...@@ -325,11 +327,9 @@ lint:javascript: ...@@ -325,11 +327,9 @@ lint:javascript:
paths: paths:
- node_modules/ - node_modules/
stage: test stage: test
image: "node:7.1" before_script: []
before_script:
- npm install
script: script:
- npm --silent run eslint - yarn run eslint
lint:javascript:report: lint:javascript:report:
<<: *dedicated-runner <<: *dedicated-runner
...@@ -337,12 +337,10 @@ lint:javascript:report: ...@@ -337,12 +337,10 @@ lint:javascript:report:
paths: paths:
- node_modules/ - node_modules/
stage: post-test stage: post-test
image: "node:7.1" before_script: []
before_script:
- npm install
script: script:
- find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files - find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
- npm --silent run eslint-report || true # ignore exit code - yarn run eslint-report || true # ignore exit code
artifacts: artifacts:
name: eslint-report name: eslint-report
expire_in: 31d expire_in: 31d
...@@ -395,7 +393,7 @@ pages: ...@@ -395,7 +393,7 @@ pages:
- mv public/ .public/ - mv public/ .public/
- mkdir public/ - mkdir public/
- mv coverage/ public/coverage-ruby/ || true - mv coverage/ public/coverage-ruby/ || true
- mv coverage-javascript/default/ public/coverage-javascript/ || true - mv coverage-javascript/ public/coverage-javascript/ || true
- mv eslint-report.html public/ || true - mv eslint-report.html public/ || true
artifacts: artifacts:
paths: paths:
......
...@@ -5,7 +5,7 @@ require: ...@@ -5,7 +5,7 @@ require:
inherit_from: .rubocop_todo.yml inherit_from: .rubocop_todo.yml
AllCops: AllCops:
TargetRubyVersion: 2.1 TargetRubyVersion: 2.3
# Cop names are not d§splayed in offense messages by default. Change behavior # Cop names are not d§splayed in offense messages by default. Change behavior
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names # by overriding DisplayCopNames, or by giving the -D/--display-cop-names
# option. # option.
...@@ -339,6 +339,10 @@ Style/OpMethod: ...@@ -339,6 +339,10 @@ Style/OpMethod:
Style/ParenthesesAroundCondition: Style/ParenthesesAroundCondition:
Enabled: true Enabled: true
# Checks for an obsolete RuntimeException argument in raise/fail.
Style/RedundantException:
Enabled: true
# Checks for parentheses that seem not to serve any purpose. # Checks for parentheses that seem not to serve any purpose.
Style/RedundantParentheses: Style/RedundantParentheses:
Enabled: true Enabled: true
...@@ -568,6 +572,10 @@ Lint/ElseLayout: ...@@ -568,6 +572,10 @@ Lint/ElseLayout:
Lint/EmptyEnsure: Lint/EmptyEnsure:
Enabled: true Enabled: true
# Checks for the presence of `when` branches without a body.
Lint/EmptyWhen:
Enabled: true
# Align ends correctly. # Align ends correctly.
Lint/EndAlignment: Lint/EndAlignment:
Enabled: true Enabled: true
...@@ -769,6 +777,10 @@ Rails/ScopeArgs: ...@@ -769,6 +777,10 @@ Rails/ScopeArgs:
RSpec/AnyInstance: RSpec/AnyInstance:
Enabled: false Enabled: false
# Check for expectations where `be(...)` can replace `eql(...)`.
RSpec/BeEql:
Enabled: false
# Check that the first argument to the top level describe is the tested class or # Check that the first argument to the top level describe is the tested class or
# module. # module.
RSpec/DescribeClass: RSpec/DescribeClass:
...@@ -797,6 +809,10 @@ RSpec/ExampleWording: ...@@ -797,6 +809,10 @@ RSpec/ExampleWording:
not: does not not: does not
IgnoredWords: [] IgnoredWords: []
# Checks for `expect(...)` calls containing literal values.
RSpec/ExpectActual:
Enabled: true
# Checks the file and folder naming of the spec file. # Checks the file and folder naming of the spec file.
RSpec/FilePath: RSpec/FilePath:
Enabled: false Enabled: false
......
...@@ -21,10 +21,6 @@ Lint/AmbiguousRegexpLiteral: ...@@ -21,10 +21,6 @@ Lint/AmbiguousRegexpLiteral:
Lint/AssignmentInCondition: Lint/AssignmentInCondition:
Enabled: false Enabled: false
# Offense count: 1
Lint/EmptyWhen:
Enabled: false
# Offense count: 20 # Offense count: 20
Lint/HandleExceptions: Lint/HandleExceptions:
Enabled: false Enabled: false
...@@ -80,19 +76,11 @@ Performance/RedundantMatch: ...@@ -80,19 +76,11 @@ Performance/RedundantMatch:
Performance/RedundantMerge: Performance/RedundantMerge:
Enabled: false Enabled: false
# Offense count: 7
RSpec/BeEql:
Enabled: false
# Offense count: 15 # Offense count: 15
# Configuration parameters: CustomIncludeMethods. # Configuration parameters: CustomIncludeMethods.
RSpec/EmptyExampleGroup: RSpec/EmptyExampleGroup:
Enabled: false Enabled: false
# Offense count: 24
RSpec/ExpectActual:
Enabled: false
# Offense count: 58 # Offense count: 58
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example # SupportedStyles: implicit, each, example
...@@ -424,11 +412,6 @@ Style/RaiseArgs: ...@@ -424,11 +412,6 @@ Style/RaiseArgs:
Style/RedundantBegin: Style/RedundantBegin:
Enabled: false Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantException:
Enabled: false
# Offense count: 29 # Offense count: 29
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/RedundantFreeze: Style/RedundantFreeze:
......
...@@ -2,6 +2,13 @@ ...@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 8.16.5 (2017-02-14)
- Patch Asciidocs rendering to block XSS.
- Fix XSS vulnerability in SVG attachments.
- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
- Patch XSS vulnerability in RDOC support.
## 8.16.4 (2017-02-02) ## 8.16.4 (2017-02-02)
- Support non-ASCII characters in GFM autocomplete. !8729 - Support non-ASCII characters in GFM autocomplete. !8729
...@@ -174,6 +181,13 @@ entry. ...@@ -174,6 +181,13 @@ entry.
- Add margin to markdown math blocks. - Add margin to markdown math blocks.
- Add hover state to MR comment reply button. - Add hover state to MR comment reply button.
## 8.15.6 (2017-02-14)
- Patch Asciidocs rendering to block XSS.
- Fix XSS vulnerability in SVG attachments.
- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
- Patch XSS vulnerability in RDOC support.
## 8.15.4 (2017-01-09) ## 8.15.4 (2017-01-09)
- Make successful pipeline emails off for watchers. !8176 - Make successful pipeline emails off for watchers. !8176
...@@ -437,6 +451,13 @@ entry. ...@@ -437,6 +451,13 @@ entry.
- Whitelist next project names: help, ci, admin, search. !8227 - Whitelist next project names: help, ci, admin, search. !8227
- Adds back CSS for progress-bars. !8237 - Adds back CSS for progress-bars. !8237
## 8.14.9 (2017-02-14)
- Patch Asciidocs rendering to block XSS.
- Fix XSS vulnerability in SVG attachments.
- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
- Patch XSS vulnerability in RDOC support.
## 8.14.8 (2017-01-25) ## 8.14.8 (2017-01-25)
- Accept environment variables from the `pre-receive` script. !7967 - Accept environment variables from the `pre-receive` script. !7967
......
...@@ -29,6 +29,7 @@ gem 'omniauth-github', '~> 1.1.1' ...@@ -29,6 +29,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2' gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.4.1' gem 'omniauth-google-oauth2', '~> 0.4.1'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.7.0' gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
......
...@@ -483,6 +483,8 @@ GEM ...@@ -483,6 +483,8 @@ GEM
omniauth-oauth2 (1.3.1) omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0) oauth2 (~> 1.0)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-oauth2-generic (0.2.2)
omniauth-oauth2 (~> 1.0)
omniauth-saml (1.7.0) omniauth-saml (1.7.0)
omniauth (~> 1.3) omniauth (~> 1.3)
ruby-saml (~> 1.4) ruby-saml (~> 1.4)
...@@ -931,6 +933,7 @@ DEPENDENCIES ...@@ -931,6 +933,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.2) omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.4.1) omniauth-google-oauth2 (~> 0.4.1)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.7.0) omniauth-saml (~> 1.7.0)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0) omniauth-twitter (~> 1.2.0)
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
licensePath: "/api/:version/templates/licenses/:key", licensePath: "/api/:version/templates/licenses/:key",
gitignorePath: "/api/:version/templates/gitignores/:key", gitignorePath: "/api/:version/templates/gitignores/:key",
gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key", gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
dockerfilePath: "/api/:version/dockerfiles/:key", dockerfilePath: "/api/:version/templates/dockerfiles/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
group: function(group_id, callback) { group: function(group_id, callback) {
var url = Api.buildUrl(Api.groupPath) var url = Api.buildUrl(Api.groupPath)
......
...@@ -56,8 +56,7 @@ requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/)); ...@@ -56,8 +56,7 @@ requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/)); requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/));
require('vendor/fuzzaldrin-plus'); require('vendor/fuzzaldrin-plus');
window.ES6Promise = require('vendor/es6-promise.auto'); require('es6-promise').polyfill();
window.ES6Promise.polyfill();
(function () { (function () {
document.addEventListener('beforeunload', function () { document.addEventListener('beforeunload', function () {
...@@ -102,11 +101,6 @@ window.ES6Promise.polyfill(); ...@@ -102,11 +101,6 @@ window.ES6Promise.polyfill();
} }
}); });
$('.nav-sidebar').niceScroll({
cursoropacitymax: '0.4',
cursorcolor: '#FFF',
cursorborder: '1px solid #FFF'
});
$('.js-select-on-focus').on('focusin', function () { $('.js-select-on-focus').on('focusin', function () {
return $(this).select().one('mouseup', function (e) { return $(this).select().one('mouseup', function (e) {
return e.preventDefault(); return e.preventDefault();
...@@ -246,8 +240,6 @@ window.ES6Promise.polyfill(); ...@@ -246,8 +240,6 @@ window.ES6Promise.polyfill();
}); });
gl.awardsHandler = new AwardsHandler(); gl.awardsHandler = new AwardsHandler();
new Aside(); new Aside();
// bind sidebar events
new gl.Sidebar();
gl.utils.initTimeagoTimeout(); gl.utils.initTimeagoTimeout();
}); });
......
...@@ -95,7 +95,7 @@ $(() => { ...@@ -95,7 +95,7 @@ $(() => {
}, },
computed: { computed: {
disabled() { disabled() {
return Store.shouldAddBlankState(); return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
}, },
}, },
template: ` template: `
......
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
Vue.filter('due-date', (value) => { Vue.filter('due-date', (value) => {
const date = new Date(value); const date = new Date(value);
return dateFormat(date, 'mmm d, yyyy'); return dateFormat(date, 'mmm d, yyyy', true);
}); });
...@@ -20,7 +20,10 @@ $(() => { ...@@ -20,7 +20,10 @@ $(() => {
gl.commits.PipelinesTableBundle.$destroy(true); gl.commits.PipelinesTableBundle.$destroy(true);
} }
gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({ const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
el: document.querySelector('#commit-pipeline-table-view'), gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
});
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
}
}); });
...@@ -8,7 +8,22 @@ ...@@ -8,7 +8,22 @@
* Uses Vue.Resource * Uses Vue.Resource
*/ */
class PipelinesService { class PipelinesService {
constructor(endpoint) {
/**
* FIXME: The url provided to request the pipelines in the new merge request
* page already has `.json`.
* This should be fixed when the endpoint is improved.
*
* @param {String} root
*/
constructor(root) {
let endpoint;
if (root.indexOf('.json') === -1) {
endpoint = `${root}.json`;
} else {
endpoint = root;
}
this.pipelines = Vue.resource(endpoint); this.pipelines = Vue.resource(endpoint);
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
* *
* Used to store the Pipelines rendered in the commit view in the pipelines table. * Used to store the Pipelines rendered in the commit view in the pipelines table.
*/ */
require('../../vue_realtime_listener');
class PipelinesStore { class PipelinesStore {
constructor() { constructor() {
...@@ -24,7 +25,7 @@ class PipelinesStore { ...@@ -24,7 +25,7 @@ class PipelinesStore {
* update the time to show how long as passed. * update the time to show how long as passed.
* *
*/ */
startTimeAgoLoops() { static startTimeAgoLoops() {
const startTimeLoops = () => { const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => { this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => { this.$children[0].$children.reduce((acc, component) => {
...@@ -44,7 +45,4 @@ class PipelinesStore { ...@@ -44,7 +45,4 @@ class PipelinesStore {
} }
} }
window.gl = window.gl || {}; module.exports = PipelinesStore;
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesStore = PipelinesStore;
...@@ -6,9 +6,8 @@ window.Vue.use(require('vue-resource')); ...@@ -6,9 +6,8 @@ window.Vue.use(require('vue-resource'));
require('../../lib/utils/common_utils'); require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor'); require('../../vue_shared/vue_resource_interceptor');
require('../../vue_shared/components/pipelines_table'); require('../../vue_shared/components/pipelines_table');
require('../../vue_realtime_listener/index');
require('./pipelines_service'); require('./pipelines_service');
require('./pipelines_store'); const PipelineStore = require('./pipelines_store');
/** /**
* *
...@@ -41,7 +40,7 @@ require('./pipelines_store'); ...@@ -41,7 +40,7 @@ require('./pipelines_store');
data() { data() {
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
const svgsData = document.querySelector('.pipeline-svgs').dataset; const svgsData = document.querySelector('.pipeline-svgs').dataset;
const store = new gl.commits.pipelines.PipelinesStore(); const store = new PipelineStore();
// Transform svgs DOMStringMap to a plain Object. // Transform svgs DOMStringMap to a plain Object.
const svgsObject = gl.utils.DOMStringMapToObject(svgsData); const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
...@@ -56,15 +55,14 @@ require('./pipelines_store'); ...@@ -56,15 +55,14 @@ require('./pipelines_store');
}, },
/** /**
* When the component is created the service to fetch the data will be * When the component is about to be mounted, tell the service to fetch the data
* initialized with the correct endpoint.
* *
* A request to fetch the pipelines will be made. * A request to fetch the pipelines will be made.
* In case of a successfull response we will store the data in the provided * In case of a successfull response we will store the data in the provided
* store, in case of a failed response we need to warn the user. * store, in case of a failed response we need to warn the user.
* *
*/ */
created() { beforeMount() {
const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint); const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
this.isLoading = true; this.isLoading = true;
...@@ -72,7 +70,6 @@ require('./pipelines_store'); ...@@ -72,7 +70,6 @@ require('./pipelines_store');
.then(response => response.json()) .then(response => response.json())
.then((json) => { .then((json) => {
this.store.storePipelines(json); this.store.storePipelines(json);
this.store.startTimeAgoLoops.call(this, Vue);
this.isLoading = false; this.isLoading = false;
}) })
.catch(() => { .catch(() => {
...@@ -81,9 +78,15 @@ require('./pipelines_store'); ...@@ -81,9 +78,15 @@ require('./pipelines_store');
}); });
}, },
beforeUpdate() {
if (this.state.pipelines.length && this.$children) {
PipelineStore.startTimeAgoLoops.call(this, Vue);
}
},
template: ` template: `
<div> <div class="pipelines">
<div class="pipelines realtime-loading" v-if="isLoading"> <div class="realtime-loading" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i> <i class="fa fa-spinner fa-spin"></i>
</div> </div>
......
...@@ -13,6 +13,12 @@ ...@@ -13,6 +13,12 @@
<div> <div>
<div class="events-description"> <div class="events-description">
{{ stage.description }} {{ stage.description }}
<span v-if="items.length === 50" class="events-info pull-right">
<i class="fa fa-warning has-tooltip"
title="Limited to showing 50 events at most"
data-placement="top"></i>
Showing 50 events
</span>
</div> </div>
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item"> <li v-for="commit in items" class="stage-event-item">
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $dueDateInput.get(0), field: $dueDateInput.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme',
format: 'YYYY-MM-DD', format: 'yyyy-mm-dd',
onSelect: (dateText) => { onSelect: (dateText) => {
const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
} }
}); });
calendar.setDate(new Date($dueDateInput.val()));
this.$datePicker.append(calendar.el); this.$datePicker.append(calendar.el);
this.$datePicker.data('pikaday', calendar); this.$datePicker.data('pikaday', calendar);
} }
...@@ -169,11 +170,12 @@ ...@@ -169,11 +170,12 @@
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $datePicker.get(0), field: $datePicker.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme',
format: 'YYYY-MM-DD', format: 'yyyy-mm-dd',
onSelect(dateText) { onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
}); });
calendar.setDate(new Date($datePicker.val()));
$datePicker.data('pikaday', calendar); $datePicker.data('pikaday', calendar);
}); });
......
/* eslint-disable no-param-reassign, no-new */ /* eslint-disable no-param-reassign, no-new */
/* global Vue */
/* global EnvironmentsService */
/* global Flash */ /* global Flash */
window.Vue = require('vue'); const Vue = require('vue');
window.Vue.use(require('vue-resource')); Vue.use(require('vue-resource'));
require('../services/environments_service'); const EnvironmentsService = require('../services/environments_service');
require('./environment_item'); const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
(() => { module.exports = Vue.component('environment-component', {
window.gl = window.gl || {};
gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
props: {
store: {
type: Object,
required: true,
default: () => ({}),
},
},
components: { components: {
'environment-item': gl.environmentsList.EnvironmentItem, 'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination,
}, },
data() { data() {
const environmentsData = document.querySelector('#environments-list-view').dataset; const environmentsData = document.querySelector('#environments-list-view').dataset;
const store = new EnvironmentsStore();
return { return {
state: this.store.state, store,
state: store.state,
visibility: 'available', visibility: 'available',
isLoading: false, isLoading: false,
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
...@@ -43,25 +37,30 @@ require('./environment_item'); ...@@ -43,25 +37,30 @@ require('./environment_item');
commitIconSvg: environmentsData.commitIconSvg, commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg, playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg, terminalIconSvg: environmentsData.terminalIconSvg,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
}; };
}, },
computed: { computed: {
scope() { scope() {
return this.$options.getQueryParameter('scope'); return gl.utils.getParameterByName('scope');
}, },
canReadEnvironmentParsed() { canReadEnvironmentParsed() {
return this.$options.convertPermissionToBoolean(this.canReadEnvironment); return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
}, },
canCreateDeploymentParsed() { canCreateDeploymentParsed() {
return this.$options.convertPermissionToBoolean(this.canCreateDeployment); return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
}, },
canCreateEnvironmentParsed() { canCreateEnvironmentParsed() {
return this.$options.convertPermissionToBoolean(this.canCreateEnvironment); return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
}, },
}, },
/** /**
...@@ -69,19 +68,27 @@ require('./environment_item'); ...@@ -69,19 +68,27 @@ require('./environment_item');
* Toggles loading property. * Toggles loading property.
*/ */
created() { created() {
gl.environmentsService = new EnvironmentsService(this.endpoint); const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const scope = this.$options.getQueryParameter('scope'); const service = new EnvironmentsService(endpoint);
if (scope) {
this.store.storeVisibility(scope);
}
this.isLoading = true; this.isLoading = true;
return gl.environmentsService.all() return service.all()
.then(resp => resp.json()) .then(resp => ({
.then((json) => { headers: resp.headers,
this.store.storeEnvironments(json); body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false; this.isLoading = false;
}) })
.catch(() => { .catch(() => {
...@@ -90,33 +97,22 @@ require('./environment_item'); ...@@ -90,33 +97,22 @@ require('./environment_item');
}); });
}, },
/** methods: {
* Transforms the url parameter into an object and toggleRow(model) {
* returns the one requested. return this.store.toggleFolder(model.name);
*
* @param {String} param
* @returns {String} The value of the requested parameter.
*/
getQueryParameter(parameter) {
return window.location.search.substring(1).split('&').reduce((acc, param) => {
const paramSplited = param.split('=');
acc[paramSplited[0]] = paramSplited[1];
return acc;
}, {})[parameter];
}, },
/** /**
* Converts permission provided as strings to booleans. * Will change the page number and update the URL.
* @param {String} string *
* @returns {Boolean} * @param {Number} pageNumber desired page to go to.
* @return {String}
*/ */
convertPermissionToBoolean(string) { changePage(pageNumber) {
return string === 'true'; const param = gl.utils.setParamInURL('page', pageNumber);
},
methods: { gl.utils.visitUrl(param);
toggleRow(model) { return param;
return this.store.toggleFolder(model.name);
}, },
}, },
...@@ -124,14 +120,15 @@ require('./environment_item'); ...@@ -124,14 +120,15 @@ require('./environment_item');
<div :class="cssContainerClass"> <div :class="cssContainerClass">
<div class="top-area"> <div class="top-area">
<ul v-if="!isLoading" class="nav-links"> <ul v-if="!isLoading" class="nav-links">
<li v-bind:class="{ 'active': scope === undefined }"> <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="projectEnvironmentsPath"> <a :href="projectEnvironmentsPath">
Available Available
<span class="badge js-available-environments-count"> <span class="badge js-available-environments-count">
{{state.availableCounter}} {{state.availableCounter}}
</span> </span>
</a> </a>
</li><li v-bind:class="{ 'active' : scope === 'stopped' }"> </li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath"> <a :href="projectStoppedEnvironmentsPath">
Stopped Stopped
<span class="badge js-stopped-environments-count"> <span class="badge js-stopped-environments-count">
...@@ -165,8 +162,7 @@ require('./environment_item'); ...@@ -165,8 +162,7 @@ require('./environment_item');
</a> </a>
</p> </p>
<a <a v-if="canCreateEnvironmentParsed"
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath" :href="newEnvironmentPath"
class="btn btn-create js-new-environment-button"> class="btn btn-create js-new-environment-button">
New Environment New Environment
...@@ -174,50 +170,23 @@ require('./environment_item'); ...@@ -174,50 +170,23 @@ require('./environment_item');
</div> </div>
<div class="table-holder" <div class="table-holder"
v-if="!isLoading && state.filteredEnvironments.length > 0"> v-if="!isLoading && state.environments.length > 0">
<table class="table ci-table environments">
<thead>
<tr>
<th class="environments-name">Environment</th>
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Job</th>
<th class="environments-commit">Commit</th>
<th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
<tbody>
<template v-for="model in state.filteredEnvironments"
v-bind:model="model">
<tr
is="environment-item"
:model="model"
:toggleRow="toggleRow.bind(model)"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
<tr v-if="model.isOpen && model.children && model.children.length > 0" <environment-table
is="environment-item" :environments="state.environments"
v-for="children in model.children"
:model="children"
:toggleRow="toggleRow.bind(children)"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg" :play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg" :terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"> :commit-icon-svg="commitIconSvg">
</tr> </environment-table>
</template> <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
</tbody> :change="changePage"
</table> :pageInfo="state.paginationInformation">
</table-pagination>
</div> </div>
</div> </div>
</div> </div>
`, `,
}); });
})();
/* global Vue */ const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('actions-component', {
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
props: { props: {
actions: { actions: {
type: Array, type: Array,
...@@ -46,5 +40,4 @@ window.Vue = require('vue'); ...@@ -46,5 +40,4 @@ window.Vue = require('vue');
</div> </div>
</div> </div>
`, `,
}); });
})();
/* global Vue */ /**
* Renders the external url link in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('external-url-component', {
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
props: { props: {
externalUrl: { externalUrl: {
type: String, type: String,
...@@ -19,5 +16,4 @@ window.Vue = require('vue'); ...@@ -19,5 +16,4 @@ window.Vue = require('vue');
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
</a> </a>
`, `,
}); });
})();
/* global Vue */ /**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('rollback-component', {
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
props: { props: {
retryUrl: { retryUrl: {
type: String, type: String,
...@@ -29,5 +27,4 @@ window.Vue = require('vue'); ...@@ -29,5 +27,4 @@ window.Vue = require('vue');
</span> </span>
</a> </a>
`, `,
}); });
})();
/* global Vue */ /**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('stop-component', {
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.StopComponent = Vue.component('stop-component', {
props: { props: {
stopUrl: { stopUrl: {
type: String, type: String,
...@@ -20,8 +18,7 @@ window.Vue = require('vue'); ...@@ -20,8 +18,7 @@ window.Vue = require('vue');
data-confirm="Are you sure you want to stop this environment?" data-confirm="Are you sure you want to stop this environment?"
data-method="post" data-method="post"
rel="nofollow"> rel="nofollow">
<i class="fa fa-stop stop-env-icon"></i> <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
</a> </a>
`, `,
}); });
})();
/* global Vue */ /**
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('terminal-button-component', {
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
props: { props: {
terminalPath: { terminalPath: {
type: String, type: String,
...@@ -24,5 +22,4 @@ window.Vue = require('vue'); ...@@ -24,5 +22,4 @@ window.Vue = require('vue');
<span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
</a> </a>
`, `,
}); });
})();
/**
* Render environments table.
*/
const Vue = require('vue');
const EnvironmentItem = require('./environment_item');
module.exports = Vue.component('environment-table-component', {
components: {
'environment-item': EnvironmentItem,
},
props: {
environments: {
type: Array,
required: true,
default: () => ([]),
},
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
canCreateDeployment: {
type: Boolean,
required: false,
default: false,
},
commitIconSvg: {
type: String,
required: false,
},
playIconSvg: {
type: String,
required: false,
},
terminalIconSvg: {
type: String,
required: false,
},
},
template: `
<table class="table ci-table environments">
<thead>
<tr>
<th class="environments-name">Environment</th>
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Job</th>
<th class="environments-commit">Commit</th>
<th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
<tbody>
<template v-for="model in environments"
v-bind:model="model">
<tr is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
</template>
</tbody>
</table>
`,
});
window.Vue = require('vue'); const EnvironmentsComponent = require('./components/environment');
require('./stores/environments_store');
require('./components/environment');
require('../vue_shared/vue_resource_interceptor'); require('../vue_shared/vue_resource_interceptor');
$(() => { $(() => {
...@@ -9,14 +7,8 @@ $(() => { ...@@ -9,14 +7,8 @@ $(() => {
if (gl.EnvironmentsListApp) { if (gl.EnvironmentsListApp) {
gl.EnvironmentsListApp.$destroy(true); gl.EnvironmentsListApp.$destroy(true);
} }
const Store = gl.environmentsList.EnvironmentsStore;
gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({ gl.EnvironmentsListApp = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'), el: document.querySelector('#environments-list-view'),
propsData: {
store: Store.create(),
},
}); });
}); });
const EnvironmentsFolderComponent = require('./environments_folder_view');
require('../../vue_shared/vue_resource_interceptor');
$(() => {
window.gl = window.gl || {};
if (gl.EnvironmentsListFolderApp) {
gl.EnvironmentsListFolderApp.$destroy(true);
}
gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
el: document.querySelector('#environments-folder-list-view'),
});
});
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
const Vue = require('vue');
Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('../components/environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
module.exports = Vue.component('environment-folder-view', {
components: {
'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination,
},
data() {
const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
const store = new EnvironmentsStore();
const pathname = window.location.pathname;
const endpoint = `${pathname}.json`;
const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
return {
store,
folderName,
endpoint,
state: store.state,
visibility: 'available',
isLoading: false,
cssContainerClass: environmentsData.cssClass,
canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment,
// svgs
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
};
},
computed: {
scope() {
return gl.utils.getParameterByName('scope');
},
canReadEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
},
/**
* URL to link in the stopped tab.
*
* @return {String}
*/
stoppedPath() {
return `${window.location.pathname}?scope=stopped`;
},
/**
* URL to link in the available tab.
*
* @return {String}
*/
availablePath() {
return window.location.pathname;
},
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.all()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert');
});
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
changePage(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
},
template: `
<div :class="cssContainerClass">
<div class="top-area" v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b>
</h4>
<ul class="nav-links">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="availablePath" class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="stoppedPath" class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
</div>
<div class="environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</environment-table>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
</div>
</div>
</div>
`,
});
/* globals Vue */ const Vue = require('vue');
/* eslint-disable no-unused-vars, no-param-reassign */
class EnvironmentsService { class EnvironmentsService {
constructor(endpoint) {
constructor(root) { this.environments = Vue.resource(endpoint);
Vue.http.options.root = root;
this.environments = Vue.resource(root);
Vue.http.interceptors.push((request, next) => {
// needed in order to not break the tests.
if ($.rails) {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
}
next();
});
} }
all() { all() {
...@@ -22,4 +10,4 @@ class EnvironmentsService { ...@@ -22,4 +10,4 @@ class EnvironmentsService {
} }
} }
window.EnvironmentsService = EnvironmentsService; module.exports = EnvironmentsService;
/* eslint-disable no-param-reassign */ require('~/lib/utils/common_utils');
(() => { /**
window.gl = window.gl || {}; * Environments Store.
window.gl.environmentsList = window.gl.environmentsList || {}; *
* Stores received environments, count of stopped environments and count of
gl.environmentsList.EnvironmentsStore = { * available environments.
state: {}, */
class EnvironmentsStore {
create() { constructor() {
this.state = {};
this.state.environments = []; this.state.environments = [];
this.state.stoppedCounter = 0; this.state.stoppedCounter = 0;
this.state.availableCounter = 0; this.state.availableCounter = 0;
this.state.visibility = 'available'; this.state.paginationInformation = {};
this.state.filteredEnvironments = [];
return this; return this;
}, }
/** /**
* In order to display a tree view we need to modify the received
* data in to a tree structure based on `environment_type`
* sorted alphabetically.
* In each children a `vue-` property will be added. This property will be
* used to know if an item is a children mostly for css purposes. This is
* needed because the children row is a fragment instance and therfore does
* not accept non-prop attributes.
* *
* Stores the received environments.
* *
* @example * In the main environments endpoint, each environment has the following schema
* it will transform this: * { name: String, size: Number, latest: Object }
* [ * In the endpoint to retrieve environments from each folder, the environment does
* { name: "environment", environment_type: "review" }, * not have the `latest` key and the data is all in the root level.
* { name: "environment_1", environment_type: null } * To avoid doing this check in the view, we store both cases the same by extracting
* { name: "environment_2, environment_type: "review" } * what is inside the `latest` key.
* ]
* into this:
* [
* { name: "review", children:
* [
* { name: "environment", environment_type: "review", vue-isChildren: true},
* { name: "environment_2", environment_type: "review", vue-isChildren: true}
* ]
* },
* {name: "environment_1", environment_type: null}
* ]
* *
* If the `size` is bigger than 1, it means it should be rendered as a folder.
* In those cases we add `isFolder` key in order to render it properly.
* *
* @param {Array} environments List of environments. * @param {Array} environments
* @returns {Array} Tree structured array with the received environments. * @returns {Array}
*/ */
storeEnvironments(environments = []) { storeEnvironments(environments = []) {
this.state.stoppedCounter = this.countByState(environments, 'stopped'); const filteredEnvironments = environments.map((env) => {
this.state.availableCounter = this.countByState(environments, 'available'); let filtered = {};
const environmentsTree = environments.reduce((acc, environment) => {
if (environment.environment_type !== null) {
const occurs = acc.filter(element => element.children &&
element.name === environment.environment_type);
environment['vue-isChildren'] = true;
if (occurs.length) { if (env.size > 1) {
acc[acc.indexOf(occurs[0])].children.push(environment); filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
} else {
acc.push({
name: environment.environment_type,
children: [environment],
isOpen: false,
'vue-isChildren': environment['vue-isChildren'],
});
} }
if (env.latest) {
filtered = Object.assign(filtered, env, env.latest);
delete filtered.latest;
} else { } else {
acc.push(environment); filtered = Object.assign(filtered, env);
} }
return acc; return filtered;
}, []).slice().sort(this.sortByName); });
this.state.environments = environmentsTree;
this.filterEnvironmentsByVisibility(this.state.environments);
return environmentsTree;
},
storeVisibility(visibility) {
this.state.visibility = visibility;
},
/**
* Given the visibility prop provided by the url query parameter and which
* changes according to the active tab we need to filter which environments
* should be visible.
*
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
* The second guarantees both root level and children elements are filtered as well.
*
* Given array of environments will return only
* the environments that match the state stored.
*
* @param {Array} array
* @return {Array}
*/
filterEnvironmentsByVisibility(arr) {
const filteredEnvironments = arr.map((item) => {
if (item.children) {
const filteredChildren = this.filterEnvironmentsByVisibility(
item.children,
).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
}
}
return this.filterState(this.state.visibility, item); this.state.environments = filteredEnvironments;
}).filter(Boolean);
this.state.filteredEnvironments = filteredEnvironments;
return filteredEnvironments; return filteredEnvironments;
},
/**
* Given the state and the environment,
* returns only if the environment state matches the one provided.
*
* @param {String} state
* @param {Object} environment
* @return {Object}
*/
filterState(state, environment) {
return environment.state === state && environment;
},
/**
* Toggles folder open property given the environment type.
*
* @param {String} envType
* @return {Array}
*/
toggleFolder(envType) {
const environments = this.state.environments;
const environmentsCopy = environments.map((env) => {
if (env['vue-isChildren'] && env.name === envType) {
env.isOpen = !env.isOpen;
} }
return env; setPagination(pagination = {}) {
}); const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
this.state.environments = environmentsCopy;
return environmentsCopy; this.state.paginationInformation = paginationInformation;
}, return paginationInformation;
}
/** /**
* Given an array of environments, returns the number of environments * Stores the number of available environments.
* that have the given state.
* *
* @param {Array} environments * @param {Number} count = 0
* @param {String} state * @return {Number}
* @returns {Number}
*/ */
countByState(environments, state) { storeAvailableCount(count = 0) {
return environments.filter(env => env.state === state).length; this.state.availableCounter = count;
}, return count;
}
/** /**
* Sorts the two objects provided by their name. * Stores the number of closed environments.
* *
* @param {Object} a * @param {Number} count = 0
* @param {Object} b * @return {Number}
* @returns {Number}
*/ */
sortByName(a, b) { storeStoppedCount(count = 0) {
const nameA = a.name.toUpperCase(); this.state.stoppedCounter = count;
const nameB = b.name.toUpperCase(); return count;
}
}
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line module.exports = EnvironmentsStore;
},
};
})();
...@@ -103,6 +103,9 @@ ...@@ -103,6 +103,9 @@
this.input.each((i, input) => { this.input.each((i, input) => {
const $input = $(input); const $input = $(input);
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
// This triggers at.js again
// Needed for slash commands with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
}); });
}, },
setupAtWho: function($input) { setupAtWho: function($input) {
......
...@@ -47,9 +47,11 @@ ...@@ -47,9 +47,11 @@
} }
// Only filter asynchronously only if option remote is set // Only filter asynchronously only if option remote is set
if (this.options.remote) { if (this.options.remote) {
$inputContainer.parent().addClass('is-loading');
clearTimeout(timeout); clearTimeout(timeout);
return timeout = setTimeout(function() { return timeout = setTimeout(function() {
return this.options.query(this.input.val(), function(data) { return this.options.query(this.input.val(), function(data) {
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data); return this.options.callback(data);
}.bind(this)); }.bind(this));
}.bind(this), 250); }.bind(this), 250);
......
...@@ -40,11 +40,12 @@ ...@@ -40,11 +40,12 @@
calendar = new Pikaday({ calendar = new Pikaday({
field: $issuableDueDate.get(0), field: $issuableDueDate.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme',
format: 'YYYY-MM-DD', format: 'yyyy-mm-dd',
onSelect: function(dateText) { onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
}); });
calendar.setDate(new Date($issuableDueDate.val()));
} }
} }
......
...@@ -54,16 +54,19 @@ require('vendor/task_list'); ...@@ -54,16 +54,19 @@ require('vendor/task_list');
success: function(data, textStatus, jqXHR) { success: function(data, textStatus, jqXHR) {
if ('id' in data) { if ('id' in data) {
$(document).trigger('issuable:change'); $(document).trigger('issuable:change');
const currentTotal = Number($('.issue_counter').text());
if (isClose) { if (isClose) {
$('a.btn-close').addClass('hidden'); $('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden'); $('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden'); $('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden'); $('div.status-box-open').addClass('hidden');
$('.issue_counter').text(currentTotal - 1);
} else { } else {
$('a.btn-reopen').addClass('hidden'); $('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden'); $('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden'); $('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden'); $('div.status-box-open').removeClass('hidden');
$('.issue_counter').text(currentTotal + 1);
} }
} else { } else {
new Flash(issueFailMessage, 'alert'); new Flash(issueFailMessage, 'alert');
......
...@@ -231,6 +231,21 @@ ...@@ -231,6 +231,21 @@
return upperCaseHeaders; return upperCaseHeaders;
}; };
/**
* Parses pagination object string values into numbers.
*
* @param {Object} paginationInformation
* @returns {Object}
*/
w.gl.utils.parseIntPagination = paginationInformation => ({
perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
page: parseInt(paginationInformation['X-PAGE'], 10),
total: parseInt(paginationInformation['X-TOTAL'], 10),
totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
});
/** /**
* Transforms a DOMStringMap into a plain object. * Transforms a DOMStringMap into a plain object.
* *
...@@ -241,5 +256,45 @@ ...@@ -241,5 +256,45 @@
acc[element] = DOMStringMapObject[element]; acc[element] = DOMStringMapObject[element];
return acc; return acc;
}, {}); }, {});
/**
* Updates the search parameter of a URL given the parameter and values provided.
*
* If no search params are present we'll add it.
* If param for page is already present, we'll update it
* If there are params but not for the given one, we'll add it at the end.
* Returns the new search parameters.
*
* @param {String} param
* @param {Number|String|Undefined|Null} value
* @return {String}
*/
w.gl.utils.setParamInURL = (param, value) => {
let search;
const locationSearch = window.location.search;
if (locationSearch.length === 0) {
search = `?${param}=${value}`;
}
if (locationSearch.indexOf(param) !== -1) {
const regex = new RegExp(param + '=\\d');
search = locationSearch.replace(regex, `${param}=${value}`);
}
if (locationSearch.length && locationSearch.indexOf(param) === -1) {
search = `${locationSearch}&${param}=${value}`;
}
return search;
};
/**
* Converts permission provided as strings to booleans.
*
* @param {String} string
* @returns {Boolean}
*/
w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
})(window); })(window);
}).call(this); }).call(this);
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $input.get(0), field: $input.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme',
format: 'YYYY-MM-DD', format: 'yyyy-mm-dd',
minDate: new Date(), minDate: new Date(),
onSelect(dateText) { onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
}, },
}); });
calendar.setDate(new Date($input.val()));
$input.data('pikaday', calendar); $input.data('pikaday', calendar);
}); });
......
...@@ -61,6 +61,7 @@ require('./flash'); ...@@ -61,6 +61,7 @@ require('./flash');
constructor({ action, setUrl, stubLocation } = {}) { constructor({ action, setUrl, stubLocation } = {}) {
this.diffsLoaded = false; this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false; this.commitsLoaded = false;
this.fixedLayoutPref = null; this.fixedLayoutPref = null;
...@@ -102,9 +103,10 @@ require('./flash'); ...@@ -102,9 +103,10 @@ require('./flash');
} }
clickTab(e) { clickTab(e) {
if (e.target && gl.utils.isMetaClick(e)) { if (e.currentTarget && gl.utils.isMetaClick(e)) {
const targetLink = e.target.getAttribute('href'); const targetLink = e.currentTarget.getAttribute('href');
e.stopImmediatePropagation(); e.stopImmediatePropagation();
e.preventDefault();
window.open(targetLink, '_blank'); window.open(targetLink, '_blank');
} }
} }
...@@ -128,6 +130,13 @@ require('./flash'); ...@@ -128,6 +130,13 @@ require('./flash');
$.scrollTo('.merge-request-details .merge-request-tabs', { $.scrollTo('.merge-request-details .merge-request-tabs', {
offset: 0, offset: 0,
}); });
} else if (action === 'pipelines') {
if (this.pipelinesLoaded) {
return;
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
this.pipelinesLoaded = true;
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
......
...@@ -110,7 +110,7 @@ require('./smart_interval'); ...@@ -110,7 +110,7 @@ require('./smart_interval');
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix; return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) { } else if (data.merge_error) {
return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
} else { } else {
callback = function() { callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch); return merge_request_widget.mergeInProgress(deleteSourceBranch);
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
if (selected.id == null) { if (selected.id == null) {
return selected.text; return selected.text;
} else { } else {
return selected.kind + ": " + selected.path; return selected.kind + ": " + selected.full_path;
} }
}, },
data: function(term, dataCallback) { data: function(term, dataCallback) {
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
if (namespace.id == null) { if (namespace.id == null) {
return namespace.text; return namespace.text;
} else { } else {
return namespace.kind + ": " + namespace.path; return namespace.kind + ": " + namespace.full_path;
} }
}, },
renderRow: this.renderRow, renderRow: this.renderRow,
......
...@@ -923,9 +923,10 @@ require('vendor/task_list'); ...@@ -923,9 +923,10 @@ require('vendor/task_list');
}; };
Notes.prototype.toggleCommitList = function(e) { Notes.prototype.toggleCommitList = function(e) {
const $element = $(e.target); const $element = $(e.currentTarget);
const $closestSystemCommitList = $element.siblings('.system-note-commit-list'); const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
$element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
$closestSystemCommitList.toggleClass('hide-shade'); $closestSystemCommitList.toggleClass('hide-shade');
}; };
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
bindEvents() { bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm); $('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername); $('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername); $('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs); $('.update-notifications').on('ajax:success', this.onUpdateNotifs);
......
...@@ -38,13 +38,15 @@ ...@@ -38,13 +38,15 @@
this.$buttons.attr('data-status', newStatus); this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction); this.$buttons.find('> span').text(newAction);
for (const button of this.$buttons) { this.$buttons.map((button) => {
const $button = $(button); const $button = $(button);
if ($button.attr('data-original-title')) { if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
} }
}
return button;
});
}); });
} }
} }
......
...@@ -21,11 +21,16 @@ ...@@ -21,11 +21,16 @@
}; };
Sidebar.prototype.addEventListeners = function() { Sidebar.prototype.addEventListeners = function() {
const $document = $(document);
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(document).on('click', '.js-sidebar-toggle', function(e, triggered) { $(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => throttledSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon; var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault(); e.preventDefault();
$this = $(this); $this = $(this);
...@@ -191,6 +196,17 @@ ...@@ -191,6 +196,17 @@
} }
}; };
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $('body').scrollTop();
if (diff > 0) {
$rightSidebar.outerHeight($(window).height() - diff);
} else {
$rightSidebar.outerHeight('100%');
}
};
Sidebar.prototype.isOpen = function() { Sidebar.prototype.isOpen = function() {
return this.sidebar.is('.right-sidebar-expanded'); return this.sidebar.is('.right-sidebar-expanded');
}; };
......
/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
/* global Cookies */
(() => {
const pinnedStateCookie = 'pin_nav';
const sidebarBreakpoint = 1024;
const pageSelector = '.page-with-sidebar';
const navbarSelector = '.navbar-gitlab';
const sidebarWrapperSelector = '.sidebar-wrapper';
const sidebarContentSelector = '.nav-sidebar';
const pinnedToggleSelector = '.js-nav-pin';
const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle';
const pinnedPageClass = 'page-sidebar-pinned';
const expandedPageClass = 'page-sidebar-expanded';
const pinnedNavbarClass = 'header-sidebar-pinned';
const expandedNavbarClass = 'header-sidebar-expanded';
class Sidebar {
constructor() {
if (!Sidebar.singleton) {
Sidebar.singleton = this;
Sidebar.singleton.init();
}
return Sidebar.singleton;
}
init() {
this.isPinned = Cookies.get(pinnedStateCookie) === 'true';
this.isExpanded = (
window.innerWidth >= sidebarBreakpoint &&
$(pageSelector).hasClass(expandedPageClass)
);
$(window).on('resize', () => this.setSidebarHeight());
$(document)
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
.on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
.on('DOMContentLoaded', () => this.renderState())
.on('scroll', () => this.setSidebarHeight())
.on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState();
this.setSidebarHeight();
}
handleClickEvent(e) {
if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) {
const $target = $(e.target);
const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0;
const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0;
if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) {
this.toggleSidebar();
}
}
}
updateTodoCount(count) {
$('.js-todos-count').text(gl.text.addDelimiter(count));
}
toggleSidebar() {
this.isExpanded = !this.isExpanded;
this.renderState();
}
setSidebarHeight() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const diff = $navHeight - $('body').scrollTop();
if (diff > 0) {
$('.js-right-sidebar').outerHeight($(window).height() - diff);
} else {
$('.js-right-sidebar').outerHeight('100%');
}
}
togglePinnedState() {
this.isPinned = !this.isPinned;
if (!this.isPinned) {
this.isExpanded = false;
}
Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 });
this.renderState();
}
renderState() {
$(pageSelector)
.toggleClass(pinnedPageClass, this.isPinned && this.isExpanded)
.toggleClass(expandedPageClass, this.isExpanded);
$(navbarSelector)
.toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded)
.toggleClass(expandedNavbarClass, this.isExpanded);
const $pinnedToggle = $(pinnedToggleSelector);
const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation';
const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide';
$pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
if (this.isExpanded) {
const sidebarContent = $(sidebarContentSelector);
setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200);
}
}
}
window.gl = window.gl || {};
gl.Sidebar = Sidebar;
})();
...@@ -147,24 +147,21 @@ ...@@ -147,24 +147,21 @@
goToTodoUrl(e) { goToTodoUrl(e) {
const todoLink = this.dataset.url; const todoLink = this.dataset.url;
let targetLink = e.target.getAttribute('href');
if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
}
if (!todoLink) { if (!todoLink) {
return; return;
} }
if (gl.utils.isMetaClick(e)) { if (gl.utils.isMetaClick(e)) {
const windowTarget = '_blank';
const selected = e.target;
e.preventDefault(); e.preventDefault();
// Meta-Click on username leads to different URL than todoLink.
// Turbolinks can resolve that URL, but window.open requires URL manually. if (selected.tagName === 'IMG') {
if (targetLink !== todoLink) { const avatarUrl = selected.parentElement.getAttribute('href');
return window.open(targetLink, '_blank'); return window.open(avatarUrl, windowTarget);
} else { } else {
return window.open(todoLink, '_blank'); return window.open(todoLink, windowTarget);
} }
} else { } else {
return gl.utils.visitUrl(todoLink); return gl.utils.visitUrl(todoLink);
......
...@@ -62,6 +62,7 @@ ...@@ -62,6 +62,7 @@
<li v-for='artifact in pipeline.details.artifacts'> <li v-for='artifact in pipeline.details.artifacts'>
<a <a
rel="nofollow" rel="nofollow"
download
:href='artifact.path' :href='artifact.path'
> >
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
......
...@@ -5,6 +5,7 @@ window.Vue = require('vue'); ...@@ -5,6 +5,7 @@ window.Vue = require('vue');
require('../vue_shared/components/table_pagination'); require('../vue_shared/components/table_pagination');
require('./store'); require('./store');
require('../vue_shared/components/pipelines_table'); require('../vue_shared/components/pipelines_table');
const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
((gl) => { ((gl) => {
gl.VuePipelines = Vue.extend({ gl.VuePipelines = Vue.extend({
...@@ -28,15 +29,34 @@ require('../vue_shared/components/pipelines_table'); ...@@ -28,15 +29,34 @@ require('../vue_shared/components/pipelines_table');
}, },
props: ['scope', 'store', 'svgs'], props: ['scope', 'store', 'svgs'],
created() { created() {
const pagenum = gl.utils.getParameterByName('p'); const pagenum = gl.utils.getParameterByName('page');
const scope = gl.utils.getParameterByName('scope'); const scope = gl.utils.getParameterByName('scope');
if (pagenum) this.pagenum = pagenum; if (pagenum) this.pagenum = pagenum;
if (scope) this.apiScope = scope; if (scope) this.apiScope = scope;
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope); this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
}, },
beforeUpdate() {
if (this.pipelines.length && this.$children) {
CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
}
},
methods: { methods: {
/**
* Changes the URL according to the pagination component.
*
* If no scope is provided, 'all' is assumed.
*
* Pagination component sends "null" when no scope is provided.
*
* @param {Number} pagenum
* @param {String} apiScope = 'all'
*/
change(pagenum, apiScope) { change(pagenum, apiScope) {
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); if (!apiScope) apiScope = 'all';
gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`);
}, },
}, },
template: ` template: `
......
/* global gl, Flash */ /* global gl, Flash */
/* eslint-disable no-param-reassign, no-underscore-dangle */ /* eslint-disable no-param-reassign */
require('../vue_realtime_listener');
((gl) => { ((gl) => {
const pageValues = (headers) => { const pageValues = (headers) => {
const normalized = gl.utils.normalizeHeaders(headers); const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = gl.utils.parseIntPagination(normalized);
const paginationInfo = {
perPage: +normalized['X-PER-PAGE'],
page: +normalized['X-PAGE'],
total: +normalized['X-TOTAL'],
totalPages: +normalized['X-TOTAL-PAGES'],
nextPage: +normalized['X-NEXT-PAGE'],
previousPage: +normalized['X-PREV-PAGE'],
};
return paginationInfo; return paginationInfo;
}; };
gl.PipelineStore = class { gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) { fetchDataLoop(Vue, pageNum, url, apiScope) {
this.pageRequest = true; this.pageRequest = true;
const updatePipelineNums = (count) => {
const { all } = count;
const running = count.running_or_pending;
document.querySelector('.js-totalbuilds-count').innerHTML = all;
document.querySelector('.js-running-count').innerHTML = running;
};
const goFetch = () => return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => { .then((response) => {
const pageInfo = pageValues(response.headers); const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
...@@ -38,31 +21,11 @@ require('../vue_realtime_listener'); ...@@ -38,31 +21,11 @@ require('../vue_realtime_listener');
this.count = Object.assign({}, this.count, res.count); this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines); this.pipelines = Object.assign([], this.pipelines, res.pipelines);
updatePipelineNums(this.count);
this.pageRequest = false; this.pageRequest = false;
}, () => { }, () => {
this.pageRequest = false; this.pageRequest = false;
return new Flash('An error occurred while fetching the pipelines, please reload the page again.'); return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
}); });
goFetch();
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
acc.push(timeAgoComponent);
return acc;
}, []).forEach(e => e.changeTime());
}, 10000);
};
startTimeLoops();
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
gl.VueRealtimeListener(removeIntervals, startIntervals);
} }
}; };
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
/* global Vue */ /* global Vue */
window.Vue = require('vue');
(() => { (() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -57,9 +57,7 @@ window.Vue = require('vue'); ...@@ -57,9 +57,7 @@ window.Vue = require('vue');
}, },
methods: { methods: {
changePage(e) { changePage(e) {
let apiScope = gl.utils.getParameterByName('scope'); const apiScope = gl.utils.getParameterByName('scope');
if (!apiScope) apiScope = 'all';
const text = e.target.innerText; const text = e.target.innerText;
const { totalPages, nextPage, previousPage } = this.pageInfo; const { totalPages, nextPage, previousPage } = this.pageInfo;
......
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
@import "framework/flash.scss"; @import "framework/flash.scss";
@import "framework/forms.scss"; @import "framework/forms.scss";
@import "framework/gfm.scss"; @import "framework/gfm.scss";
@import "framework/gitlab-theme.scss";
@import "framework/header.scss"; @import "framework/header.scss";
@import "framework/highlight.scss"; @import "framework/highlight.scss";
@import "framework/issue_box.scss"; @import "framework/issue_box.scss";
......
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
} }
.btn, .btn,
.side-nav-toggle { .global-dropdown-toggle {
@include transition(background-color, border-color, color, box-shadow); @include transition(background-color, border-color, color, box-shadow);
} }
...@@ -128,8 +128,7 @@ ...@@ -128,8 +128,7 @@
.note-action-button .link-highlight, .note-action-button .link-highlight,
.toolbar-btn, .toolbar-btn,
.dropdown-toggle-caret, .dropdown-toggle-caret {
.fa:not(.fa-bell) {
@include transition(color); @include transition(color);
} }
...@@ -141,7 +140,6 @@ a { ...@@ -141,7 +140,6 @@ a {
@include transition(background-color, box-shadow); @include transition(background-color, box-shadow);
} }
.nav-sidebar a,
.dropdown-menu a, .dropdown-menu a,
.dropdown-menu button, .dropdown-menu button,
.dropdown-menu-nav a { .dropdown-menu-nav a {
......
...@@ -28,6 +28,8 @@ ...@@ -28,6 +28,8 @@
.avatar { .avatar {
@extend .avatar-circle; @extend .avatar-circle;
@include transition-property(none);
width: 40px; width: 40px;
height: 40px; height: 40px;
padding: 0; padding: 0;
......
/**
* Styles the GitLab application with a specific color theme
*
* $color-light -
* $color -
* $color-darker -
* $color-dark -
*/
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar {
.toggle-nav-collapse,
.pin-nav-btn {
color: $color-light;
&:hover {
color: $white-light;
}
}
.sidebar-wrapper {
background: $color-darker;
}
.sidebar-action-buttons {
color: $color-light;
background-color: lighten($color-darker, 5%);
}
.nav-sidebar {
li {
a {
color: $color-light;
&:hover,
&:focus,
&:active {
background: $color-dark;
}
i {
color: $color-light;
}
path,
polygon {
fill: $color-light;
}
.count {
color: $color-light;
background: $color-dark;
}
svg {
position: relative;
top: 3px;
}
}
&.separate-item {
border-top: 1px solid $color;
}
&.active a {
color: $white-light;
background: $color-dark;
&.no-highlight {
border: none;
}
i {
color: $white-light;
}
path,
polygon {
fill: $white-light;
}
}
}
.about-gitlab {
color: $color-light;
}
}
}
}
$theme-charcoal-light: #b9bbbe;
$theme-charcoal: #485157;
$theme-charcoal-dark: #3d454d;
$theme-charcoal-darker: #383f45;
$theme-blue-light: #becde9;
$theme-blue: #2980b9;
$theme-blue-dark: #1970a9;
$theme-blue-darker: #096099;
$theme-graphite-light: #ccc;
$theme-graphite: #777;
$theme-graphite-dark: #666;
$theme-graphite-darker: #555;
$theme-black-light: #979797;
$theme-black: #373737;
$theme-black-dark: #272727;
$theme-black-darker: #222;
$theme-green-light: #adc;
$theme-green: #019875;
$theme-green-dark: #018865;
$theme-green-darker: #017855;
$theme-violet-light: #98c;
$theme-violet: #548;
$theme-violet-dark: #436;
$theme-violet-darker: #325;
body {
&.ui_blue {
@include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker);
}
&.ui_charcoal {
@include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker);
}
&.ui_graphite {
@include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker);
}
&.ui_black {
@include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
}
&.ui_green {
@include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker);
}
&.ui_violet {
@include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker);
}
}
...@@ -100,10 +100,30 @@ header { ...@@ -100,10 +100,30 @@ header {
} }
} }
} }
}
.side-nav-toggle { .global-dropdown {
position: absolute; position: absolute;
left: -10px; left: -10px;
.badge {
font-size: 11px;
}
li {
.active a {
font-weight: bold;
}
&:hover {
.badge {
background-color: $white-light;
}
}
}
}
.global-dropdown-toggle {
margin: 7px 0; margin: 7px 0;
font-size: 18px; font-size: 18px;
padding: 6px 10px; padding: 6px 10px;
...@@ -115,7 +135,6 @@ header { ...@@ -115,7 +135,6 @@ header {
color: $gl-header-nav-hover-color; color: $gl-header-nav-hover-color;
} }
} }
}
.header-content { .header-content {
position: relative; position: relative;
......
.page-with-sidebar {
padding-bottom: 25px;
transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned {
.sidebar-wrapper {
box-shadow: none;
}
}
.sidebar-wrapper {
position: fixed;
top: 0;
bottom: 0;
left: 0;
height: 100%;
width: 0;
overflow: hidden;
transition: width $sidebar-transition-duration;
box-shadow: 2px 0 16px 0 $black-transparent;
}
}
.sidebar-wrapper {
z-index: 1000;
background: $gray-light;
.nicescroll-rails-hr {
// TODO: Figure out why nicescroll doesn't hide horizontal bar
display: none!important;
}
}
.content-wrapper { .content-wrapper {
width: 100%; width: 100%;
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
...@@ -47,105 +14,6 @@ ...@@ -47,105 +14,6 @@
} }
} }
.nav-sidebar {
position: absolute;
top: 50px;
bottom: 0;
width: $sidebar_width;
overflow-y: auto;
overflow-x: hidden;
&.navbar-collapse {
padding: 0 !important;
}
li {
&.separate-item {
padding-top: 10px;
margin-top: 10px;
}
.icon-container {
width: 34px;
display: inline-block;
text-align: center;
}
a {
padding: 7px $gl-sidebar-padding;
font-size: $gl-font-size;
line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
&:hover,
&:active,
&:focus {
text-decoration: none;
}
i {
font-size: 16px;
}
i,
svg {
margin-right: 13px;
}
}
}
.count {
float: right;
padding: 0 8px;
border-radius: 6px;
}
.about-gitlab {
padding: 7px $gl-sidebar-padding;
font-size: $gl-font-size;
line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
position: absolute;
bottom: 10px;
}
}
.sidebar-action-buttons {
width: $sidebar_width;
position: absolute;
top: 0;
left: 0;
min-height: 50px;
padding: 5px 0;
font-size: 18px;
line-height: 30px;
.toggle-nav-collapse {
left: 0;
}
.pin-nav-btn {
right: 0;
display: none;
@media (min-width: $sidebar-breakpoint) {
display: block;
}
.fa {
transition: transform .15s;
.page-sidebar-pinned & {
transform: rotate(90deg);
}
}
}
}
.nav-header-btn { .nav-header-btn {
padding: 10px $gl-sidebar-padding; padding: 10px $gl-sidebar-padding;
color: inherit; color: inherit;
...@@ -161,59 +29,16 @@ ...@@ -161,59 +29,16 @@
} }
} }
.page-sidebar-expanded {
.sidebar-wrapper {
width: $sidebar_width;
}
}
.page-sidebar-pinned {
.content-wrapper,
.layout-nav {
@media (min-width: $sidebar-breakpoint) {
padding-left: $sidebar_width;
}
}
.merge-request-tabs-holder.affix {
@media (min-width: $sidebar-breakpoint) {
left: $sidebar_width;
}
}
&.right-sidebar-expanded {
.line-resolve-all-container {
@media (min-width: $sidebar-breakpoint) {
display: none;
}
}
}
}
header.header-sidebar-pinned {
@media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
.side-nav-toggle {
display: none;
}
.header-content {
padding-left: 0;
}
}
}
.right-sidebar-collapsed { .right-sidebar-collapsed {
padding-right: 0; padding-right: 0;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.content-wrapper { .content-wrapper {
padding-right: $sidebar_collapsed_width; padding-right: $gutter_collapsed_width;
} }
.merge-request-tabs-holder.affix { .merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width; right: $gutter_collapsed_width;
} }
} }
...@@ -231,7 +56,7 @@ header.header-sidebar-pinned { ...@@ -231,7 +56,7 @@ header.header-sidebar-pinned {
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.build-sidebar):not(.wiki-sidebar) { &:not(.build-sidebar):not(.wiki-sidebar) {
padding-right: $sidebar_collapsed_width; padding-right: $gutter_collapsed_width;
} }
} }
...@@ -245,12 +70,12 @@ header.header-sidebar-pinned { ...@@ -245,12 +70,12 @@ header.header-sidebar-pinned {
} }
&.with-overlay .merge-request-tabs-holder.affix { &.with-overlay .merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width; right: $gutter_collapsed_width;
} }
} }
&.with-overlay { &.with-overlay {
padding-right: $sidebar_collapsed_width; padding-right: $gutter_collapsed_width;
} }
} }
......
/* /*
* Layout * Layout
*/ */
$sidebar_collapsed_width: 62px;
$sidebar_width: 220px;
$gutter_collapsed_width: 62px; $gutter_collapsed_width: 62px;
$gutter_width: 290px; $gutter_width: 290px;
$gutter_inner_width: 250px; $gutter_inner_width: 250px;
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
} }
&.scroll-top { &.scroll-top {
top: 110px; top: 10px;
} }
&.scroll-bottom { &.scroll-bottom {
......
...@@ -284,7 +284,11 @@ ...@@ -284,7 +284,11 @@
.events-description { .events-description {
line-height: 65px; line-height: 65px;
padding-left: $gl-padding; padding: 0 $gl-padding;
}
.events-info {
color: $gl-text-color-secondary;
} }
} }
......
...@@ -10,6 +10,11 @@ ...@@ -10,6 +10,11 @@
font-size: 34px; font-size: 34px;
} }
.environments-folder-name {
font-weight: normal;
padding-top: 20px;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.environments-container { .environments-container {
width: 100%; width: 100%;
...@@ -110,17 +115,20 @@ ...@@ -110,17 +115,20 @@
} }
} }
.children-row .environment-name {
margin-left: 17px;
margin-right: -17px;
}
.folder-icon { .folder-icon {
padding: 0 5px 0 0; margin-right: 3px;
color: $gl-text-color-secondary;
display: inline-block;
.fa:nth-child(1) {
margin-right: 3px;
}
} }
.folder-name { .folder-name {
cursor: pointer; cursor: pointer;
color: $gl-text-color-secondary;
display: inline-block;
} }
} }
......
...@@ -41,7 +41,6 @@ ...@@ -41,7 +41,6 @@
word-wrap: break-word; word-wrap: break-word;
.md { .md {
color: $gl-grayish-blue;
font-size: $gl-font-size; font-size: $gl-font-size;
.label { .label {
......
...@@ -193,7 +193,6 @@ ...@@ -193,7 +193,6 @@
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
z-index: 8;
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 10px 20px; padding: 10px 20px;
...@@ -254,11 +253,11 @@ ...@@ -254,11 +253,11 @@
display: block; display: block;
} }
width: $sidebar_collapsed_width; width: $gutter_collapsed_width;
padding-top: 0; padding-top: 0;
.block { .block {
width: $sidebar_collapsed_width - 2px; width: $gutter_collapsed_width - 2px;
margin-left: -19px; margin-left: -19px;
padding: 15px 0 0; padding: 15px 0 0;
border-bottom: none; border-bottom: none;
......
...@@ -85,14 +85,18 @@ ...@@ -85,14 +85,18 @@
-webkit-align-items: center; -webkit-align-items: center;
align-items: center; align-items: center;
i,
svg {
margin-right: 8px;
}
svg { svg {
margin-right: 4px;
position: relative; position: relative;
top: 1px; top: 1px;
overflow: visible; overflow: visible;
} }
&> span { & > span {
padding-right: 4px; padding-right: 4px;
} }
......
...@@ -72,6 +72,7 @@ ul.notes { ...@@ -72,6 +72,7 @@ ul.notes {
overflow: hidden; overflow: hidden;
.system-note-commit-list-toggler { .system-note-commit-list-toggler {
color: $gl-link-color;
display: none; display: none;
padding: 10px 0 0; padding: 10px 0 0;
cursor: pointer; cursor: pointer;
...@@ -107,16 +108,6 @@ ul.notes { ...@@ -107,16 +108,6 @@ ul.notes {
display: none; display: none;
} }
p:last-child {
a {
color: $gl-text-color;
&:hover {
color: $gl-link-color;
}
}
}
&::after { &::after {
content: ''; content: '';
width: 100%; width: 100%;
......
...@@ -864,7 +864,7 @@ ...@@ -864,7 +864,7 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 90px; max-width: 70%;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
margin-left: 2px; margin-left: 2px;
display: inline-block; display: inline-block;
......
.application-theme {
label {
margin-right: 20px;
text-align: center;
.preview {
border-radius: 4px;
height: 80px;
margin-bottom: 10px;
width: 160px;
&.ui_blue {
background: $theme-blue;
}
&.ui_charcoal {
background: $theme-charcoal;
}
&.ui_graphite {
background: $theme-graphite;
}
&.ui_black {
background: $theme-black;
}
&.ui_green {
background: $theme-green;
}
&.ui_violet {
background: $theme-violet;
}
}
}
}
.syntax-theme { .syntax-theme {
label { label {
margin-right: 20px; margin-right: 20px;
......
...@@ -35,13 +35,9 @@ ...@@ -35,13 +35,9 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.project-path { .project-path .form-control {
padding-right: 0;
.form-control {
border-radius: $border-radius-base; border-radius: $border-radius-base;
} }
}
.input-group > div { .input-group > div {
...@@ -106,6 +102,7 @@ ...@@ -106,6 +102,7 @@
font-size: 24px; font-size: 24px;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
word-wrap: break-word;
.fa { .fa {
margin-left: 2px; margin-left: 2px;
......
...@@ -171,6 +171,8 @@ ...@@ -171,6 +171,8 @@
.tree-controls { .tree-controls {
float: right; float: right;
margin-top: 11px; margin-top: 11px;
position: relative;
z-index: 2;
.project-action-button { .project-action-button {
margin-left: $btn-side-margin; margin-left: $btn-side-margin;
......
.new-wiki-page {
.new-wiki-page-slug-tip {
display: inline-block;
max-width: 100%;
margin-top: 5px;
}
}
.title .edit-wiki-header { .title .edit-wiki-header {
width: 780px; width: 780px;
margin-left: auto; margin-left: auto;
...@@ -9,12 +17,18 @@ ...@@ -9,12 +17,18 @@
@extend .top-area; @extend .top-area;
position: relative; position: relative;
.wiki-breadcrumb {
border-bottom: 1px solid $white-normal;
padding: 11px 0;
}
.wiki-page-title { .wiki-page-title {
margin: 0; margin: 0;
font-size: 22px; font-size: 22px;
} }
.wiki-last-edit-by { .wiki-last-edit-by {
display: block;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
strong { strong {
...@@ -121,6 +135,10 @@ ...@@ -121,6 +135,10 @@
margin: 5px 0 10px; margin: 5px 0 10px;
} }
ul.wiki-pages ul {
padding-left: 15px;
}
.wiki-sidebar-header { .wiki-sidebar-header {
padding: 0 $gl-padding $gl-padding; padding: 0 $gl-padding $gl-padding;
...@@ -129,3 +147,15 @@ ...@@ -129,3 +147,15 @@
} }
} }
} }
ul.wiki-pages-list.content-list {
& ul {
list-style: none;
margin-left: 0;
padding-left: 15px;
}
& ul li {
padding: 5px 0;
}
}
...@@ -31,7 +31,6 @@ nav.navbar-collapse.collapse, ...@@ -31,7 +31,6 @@ nav.navbar-collapse.collapse,
.blob-commit-info, .blob-commit-info,
.file-title, .file-title,
.file-holder, .file-holder,
.sidebar-wrapper,
.nav, .nav,
.btn, .btn,
ul.notes-form, ul.notes-form,
......
...@@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def update def update
if @runner.update_attributes(runner_params) if Ci::UpdateRunnerService.new(@runner).update(runner_params)
respond_to do |format| respond_to do |format|
format.js format.js
format.html { redirect_to admin_runner_path(@runner) } format.html { redirect_to admin_runner_path(@runner) }
...@@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def resume def resume
if @runner.update_attributes(active: true) if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to admin_runners_path, notice: 'Runner was successfully updated.' redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
else else
redirect_to admin_runners_path, alert: 'Runner was not updated.' redirect_to admin_runners_path, alert: 'Runner was not updated.'
...@@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def pause def pause
if @runner.update_attributes(active: false) if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to admin_runners_path, notice: 'Runner was successfully updated.' redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
else else
redirect_to admin_runners_path, alert: 'Runner was not updated.' redirect_to admin_runners_path, alert: 'Runner was not updated.'
......
...@@ -194,7 +194,6 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -194,7 +194,6 @@ class Admin::UsersController < Admin::ApplicationController
:provider, :provider,
:remember_me, :remember_me,
:skype, :skype,
:theme_id,
:twitter, :twitter,
:username, :username,
:website_url :website_url
......
...@@ -9,6 +9,28 @@ module IssuableCollections ...@@ -9,6 +9,28 @@ module IssuableCollections
private private
def issuable_meta_data(issuable_collection)
# map has to be used here since using pluck or select will
# throw an error when ordering issuables by priority which inserts
# a new order into the collection.
# We cannot use reorder to not mess up the paginated collection.
issuable_ids = issuable_collection.map(&:id)
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
issuable_ids.each_with_object({}) do |id, issuable_meta|
downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
notes = issuable_note_count.find { |notes| notes.noteable_id == id }
issuable_meta[id] = Issuable::IssuableMeta.new(
upvotes.try(:count).to_i,
downvotes.try(:count).to_i,
notes.try(:count).to_i
)
end
end
def issues_collection def issues_collection
issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace) issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
end end
......
...@@ -9,6 +9,9 @@ module IssuesAction ...@@ -9,6 +9,9 @@ module IssuesAction
.non_archived .non_archived
.page(params[:page]) .page(params[:page])
@collection_type = "Issue"
@issuable_meta_data = issuable_meta_data(@issues)
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: false }
......
...@@ -7,6 +7,9 @@ module MergeRequestsAction ...@@ -7,6 +7,9 @@ module MergeRequestsAction
@merge_requests = merge_requests_collection @merge_requests = merge_requests_collection
.page(params[:page]) .page(params[:page])
@collection_type = "MergeRequest"
@issuable_meta_data = issuable_meta_data(@merge_requests)
end end
private private
......
class Dashboard::ProjectsController < Dashboard::ApplicationController class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects include FilterProjects
before_action :event_filter
def index def index
@projects = current_user.authorized_projects.sorted_by_activity @projects = load_projects(current_user.authorized_projects)
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) @projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
format.html { @last_push = current_user.recent_push } format.html { @last_push = current_user.recent_push }
format.atom do format.atom do
event_filter
load_events load_events
render layout: false render layout: false
end end
...@@ -26,9 +21,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -26,9 +21,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end end
def starred def starred
@projects = current_user.viewable_starred_projects.sorted_by_activity @projects = load_projects(current_user.viewable_starred_projects)
@projects = filter_projects(@projects) @projects = @projects.includes(:forked_from_project, :tags)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) @projects = @projects.page(params[:page])
...@@ -37,7 +31,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -37,7 +31,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: { render json: {
html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
...@@ -48,9 +41,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -48,9 +41,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private private
def load_projects(base_scope)
projects = base_scope.sorted_by_activity.includes(:namespace)
filter_projects(projects)
end
def load_events def load_events
@events = Event.in_projects(@projects) @events = Event.in_projects(load_projects(current_user.authorized_projects))
@events = @event_filter.apply_filter(@events).with_associations @events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0) @events = @events.limit(20).offset(params[:offset] || 0)
end end
end end
...@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController ...@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:notification_email) params.require(:user).permit(:notification_email, :notified_of_own_activity)
end end
end end
...@@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController ...@@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:layout, :layout,
:dashboard, :dashboard,
:project_view, :project_view,
:theme_id
) )
end end
end end
...@@ -9,15 +9,40 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -9,15 +9,40 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :verify_api_request!, only: :terminal_websocket_authorize
def index def index
@scope = params[:scope] @environments = project.environments
@environments = project.environments.includes(:last_deployment) .with_state(params[:scope] || :available)
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: EnvironmentSerializer render json: {
.new(project: @project, user: current_user) environments: EnvironmentSerializer
.represent(@environments) .new(project: @project, user: @current_user)
.with_pagination(request, response)
.within_folders
.represent(@environments),
available_count: project.environments.available.count,
stopped_count: project.environments.stopped.count
}
end
end
end
def folder
folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available)
respond_to do |format|
format.html
format.json do
render json: {
environments: EnvironmentSerializer
.new(project: @project, user: @current_user)
.with_pagination(request, response)
.represent(@environments),
available_count: folder_environments.available.count,
stopped_count: folder_environments.stopped.count
}
end end
end end
end end
......
...@@ -23,8 +23,11 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -23,8 +23,11 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html respond_to :html
def index def index
@collection_type = "Issue"
@issues = issues_collection @issues = issues_collection
@issues = @issues.page(params[:page]) @issues = @issues.page(params[:page])
@issuable_meta_data = issuable_meta_data(@issues)
if @issues.out_of_range? && @issues.total_pages != 0 if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages)) return redirect_to url_for(params.merge(page: @issues.total_pages))
end end
......
...@@ -36,8 +36,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -36,8 +36,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts] before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
def index def index
@collection_type = "MergeRequest"
@merge_requests = merge_requests_collection @merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
@issuable_meta_data = issuable_meta_data(@merge_requests)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
end end
...@@ -366,10 +369,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -366,10 +369,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def merge_widget_refresh def merge_widget_refresh
if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged' if merge_request.merge_when_build_succeeds
@status = :success
elsif merge_request.merge_when_build_succeeds
@status = :merge_when_build_succeeds @status = :merge_when_build_succeeds
else
# Only MRs that can be merged end in this action
# MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
# in last case it does not have any special status. Possible error is handled inside widget js function
@status = :success
end end
render 'merge' render 'merge'
......
...@@ -12,7 +12,7 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -12,7 +12,7 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def update def update
if @runner.update_attributes(runner_params) if Ci::UpdateRunnerService.new(@runner).update(runner_params)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else else
render 'edit' render 'edit'
...@@ -28,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -28,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def resume def resume
if @runner.update_attributes(active: true) if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else else
redirect_to runner_path(@runner), alert: 'Runner was not updated.' redirect_to runner_path(@runner), alert: 'Runner was not updated.'
...@@ -36,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -36,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def pause def pause
if @runner.update_attributes(active: false) if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else else
redirect_to runner_path(@runner), alert: 'Runner was not updated.' redirect_to runner_path(@runner), alert: 'Runner was not updated.'
......
...@@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController
end end
def create def create
result = CreateTagService.new(@project, current_user). result = Tags::CreateService.new(@project, current_user).
execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
if result[:status] == :success if result[:status] == :success
...@@ -41,7 +41,7 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -41,7 +41,7 @@ class Projects::TagsController < Projects::ApplicationController
end end
def destroy def destroy
DeleteTagService.new(project, current_user).execute(params[:id]) Tags::DestroyService.new(project, current_user).execute(params[:id])
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -8,6 +8,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -8,6 +8,7 @@ class Projects::WikisController < Projects::ApplicationController
def pages def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]) @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
end end
def show def show
...@@ -83,7 +84,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -83,7 +84,7 @@ class Projects::WikisController < Projects::ApplicationController
def destroy def destroy
@page = @project_wiki.find_page(params[:id]) @page = @project_wiki.find_page(params[:id])
@page.delete if @page WikiPages::DestroyService.new(@project, current_user).execute(@page)
redirect_to( redirect_to(
namespace_project_wiki_path(@project.namespace, @project, :home), namespace_project_wiki_path(@project.namespace, @project, :home),
...@@ -116,7 +117,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -116,7 +117,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized # Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki @project_wiki.wiki
@sidebar_wiki_pages = @project_wiki.pages.first(15) @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
rescue ProjectWiki::CouldNotCreateWikiError rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project) redirect_to project_path(@project)
......
...@@ -296,4 +296,13 @@ module ApplicationHelper ...@@ -296,4 +296,13 @@ module ApplicationHelper
def page_class def page_class
"issue-boards-page" if current_controller?(:boards) "issue-boards-page" if current_controller?(:boards)
end end
# Returns active css class when condition returns true
# otherwise returns nil.
#
# Example:
# %li{ class: active_when(params[:filter] == '1') }
def active_when(condition)
'active' if condition
end
end end
module BuildsHelper module BuildsHelper
def sidebar_build_class(build, current_build) def sidebar_build_class(build, current_build)
build_class = '' build_class = ''
build_class += ' active' if build == current_build build_class += ' active' if build.id === current_build.id
build_class += ' retried' if build.retried? build_class += ' retried' if build.retried?
build_class build_class
end end
......
...@@ -75,10 +75,10 @@ module MergeRequestsHelper ...@@ -75,10 +75,10 @@ module MergeRequestsHelper
new_namespace_project_merge_request_path( new_namespace_project_merge_request_path(
@project.namespace, @project, @project.namespace, @project,
merge_request: { merge_request: {
source_project_id: @merge_request.source_project_id, source_project_id: merge_request.source_project_id,
target_project_id: @merge_request.target_project_id, target_project_id: merge_request.target_project_id,
source_branch: @merge_request.source_branch, source_branch: merge_request.source_branch,
target_branch: @merge_request.target_branch, target_branch: merge_request.target_branch,
}, },
change_branches: true change_branches: true
) )
......
...@@ -10,7 +10,7 @@ module NamespacesHelper ...@@ -10,7 +10,7 @@ module NamespacesHelper
data_attr_users = { 'data-options-parent' => 'users' } data_attr_users = { 'data-options-parent' => 'users' }
group_opts = [ group_opts = [
"Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.path : g.human_name, g.id, data_attr_group] } "Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.full_path : g.human_name, g.id, data_attr_group] }
] ]
users_opts = [ users_opts = [
......
module NavHelper module NavHelper
def page_sidebar_class
if pinned_nav?
"page-sidebar-expanded page-sidebar-pinned"
end
end
def page_gutter_class def page_gutter_class
if current_path?('merge_requests#show') || if current_path?('merge_requests#show') ||
current_path?('merge_requests#diffs') || current_path?('merge_requests#diffs') ||
...@@ -32,10 +26,6 @@ module NavHelper ...@@ -32,10 +26,6 @@ module NavHelper
class_name = '' class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav?
class_name << " header-sidebar-expanded header-sidebar-pinned"
end
class_name class_name
end end
...@@ -46,8 +36,4 @@ module NavHelper ...@@ -46,8 +36,4 @@ module NavHelper
def nav_control_class def nav_control_class
"nav-control" if current_user "nav-control" if current_user
end end
def pinned_nav?
cookies[:pin_nav] == 'true'
end
end end
...@@ -34,6 +34,10 @@ module PageLayoutHelper ...@@ -34,6 +34,10 @@ module PageLayoutHelper
end end
end end
def favicon
Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
end
def page_image def page_image
default = image_url('gitlab_logo.png') default = image_url('gitlab_logo.png')
......
...@@ -23,7 +23,7 @@ module PreferencesHelper ...@@ -23,7 +23,7 @@ module PreferencesHelper
if defined.size != DASHBOARD_CHOICES.size if defined.size != DASHBOARD_CHOICES.size
# Ensure that anyone adding new options updates this method too # Ensure that anyone adding new options updates this method too
raise RuntimeError, "`User` defines #{defined.size} dashboard choices," + raise "`User` defines #{defined.size} dashboard choices," \
" but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}." " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}."
else else
defined.map do |key, _| defined.map do |key, _|
...@@ -41,10 +41,6 @@ module PreferencesHelper ...@@ -41,10 +41,6 @@ module PreferencesHelper
] ]
end end
def user_application_theme
Gitlab::Themes.for_user(current_user).css_class
end
def user_color_scheme def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class Gitlab::ColorSchemes.for_user(current_user).css_class
end end
......
...@@ -63,7 +63,7 @@ module SubmoduleHelper ...@@ -63,7 +63,7 @@ module SubmoduleHelper
namespace = components.pop.gsub(/^\.\.$/, '') namespace = components.pop.gsub(/^\.\.$/, '')
if namespace.empty? if namespace.empty?
namespace = @project.namespace.path namespace = @project.namespace.full_path
end end
[ [
......
...@@ -15,6 +15,7 @@ module TodosHelper ...@@ -15,6 +15,7 @@ module TodosHelper
when Todo::MARKED then 'added a todo for' when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for' when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
when Todo::UNMERGEABLE then 'Could not merge' when Todo::UNMERGEABLE then 'Could not merge'
when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on'
end end
end end
...@@ -88,7 +89,8 @@ module TodosHelper ...@@ -88,7 +89,8 @@ module TodosHelper
{ id: Todo::ASSIGNED, text: 'Assigned' }, { id: Todo::ASSIGNED, text: 'Assigned' },
{ id: Todo::MENTIONED, text: 'Mentioned' }, { id: Todo::MENTIONED, text: 'Mentioned' },
{ id: Todo::MARKED, text: 'Added' }, { id: Todo::MARKED, text: 'Added' },
{ id: Todo::BUILD_FAILED, text: 'Pipelines' } { id: Todo::BUILD_FAILED, text: 'Pipelines' },
{ id: Todo::DIRECTLY_ADDRESSED, text: 'Directly addressed' }
] ]
end end
......
module WikiHelper
# Produces a pure text breadcrumb for a given page.
#
# page_slug - The slug of a WikiPage object.
#
# Returns a String composed of the capitalized name of each directory and the
# capitalized name of the page itself.
def breadcrumb(page_slug)
page_slug.split('/').
map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }.
join(' / ')
end
end
...@@ -151,7 +151,7 @@ class Notify < BaseMailer ...@@ -151,7 +151,7 @@ class Notify < BaseMailer
headers['In-Reply-To'] = message_id(model) headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model) headers['References'] = message_id(model)
headers[:subject].prepend('Re: ') if headers[:subject] headers[:subject]&.prepend('Re: ')
mail_thread(model, headers) mail_thread(model, headers)
end end
......
...@@ -116,34 +116,28 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -116,34 +116,28 @@ class ApplicationSetting < ActiveRecord::Base
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates_each :restricted_visibility_levels do |record, attr, value| validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil? value&.each do |level|
value.each do |level|
unless Gitlab::VisibilityLevel.options.has_value?(level) unless Gitlab::VisibilityLevel.options.has_value?(level)
record.errors.add(attr, "'#{level}' is not a valid visibility level") record.errors.add(attr, "'#{level}' is not a valid visibility level")
end end
end end
end end
end
validates_each :import_sources do |record, attr, value| validates_each :import_sources do |record, attr, value|
unless value.nil? value&.each do |source|
value.each do |source|
unless Gitlab::ImportSources.options.has_value?(source) unless Gitlab::ImportSources.options.has_value?(source)
record.errors.add(attr, "'#{source}' is not a import source") record.errors.add(attr, "'#{source}' is not a import source")
end end
end end
end end
end
validates_each :disabled_oauth_sign_in_sources do |record, attr, value| validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
unless value.nil? value&.each do |source|
value.each do |source|
unless Devise.omniauth_providers.include?(source.to_sym) unless Devise.omniauth_providers.include?(source.to_sym)
record.errors.add(attr, "'#{source}' is not an OAuth sign-in source") record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
end end
end end
end end
end
before_save :ensure_runners_registration_token before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token before_save :ensure_health_check_access_token
...@@ -230,11 +224,11 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -230,11 +224,11 @@ class ApplicationSetting < ActiveRecord::Base
end end
def domain_whitelist_raw def domain_whitelist_raw
self.domain_whitelist.join("\n") unless self.domain_whitelist.nil? self.domain_whitelist&.join("\n")
end end
def domain_blacklist_raw def domain_blacklist_raw
self.domain_blacklist.join("\n") unless self.domain_blacklist.nil? self.domain_blacklist&.join("\n")
end end
def domain_whitelist_raw=(values) def domain_whitelist_raw=(values)
......
...@@ -16,6 +16,14 @@ class AwardEmoji < ActiveRecord::Base ...@@ -16,6 +16,14 @@ class AwardEmoji < ActiveRecord::Base
scope :downvotes, -> { where(name: DOWNVOTE_NAME) } scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) } scope :upvotes, -> { where(name: UPVOTE_NAME) }
class << self
def votes_for_collection(ids, type)
select('name', 'awardable_id', 'COUNT(*) as count').
where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids).
group('name', 'awardable_id')
end
end
def downvote? def downvote?
self.name == DOWNVOTE_NAME self.name == DOWNVOTE_NAME
end end
......
...@@ -22,8 +22,6 @@ module Ci ...@@ -22,8 +22,6 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) } scope :ordered, ->() { order(id: :desc) }
after_save :tick_runner_queue, if: :form_editable_changed?
scope :owned_or_shared, ->(project_id) do scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
...@@ -40,6 +38,8 @@ module Ci ...@@ -40,6 +38,8 @@ module Ci
acts_as_taggable acts_as_taggable
after_destroy :cleanup_runner_queue
# Searches for runners matching the given query. # Searches for runners matching the given query.
# #
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
...@@ -147,14 +147,14 @@ module Ci ...@@ -147,14 +147,14 @@ module Ci
private private
def runner_queue_key def cleanup_runner_queue
"runner:build_queue:#{self.token}" Gitlab::Redis.with do |redis|
redis.del(runner_queue_key)
end end
def form_editable_changed?
FORM_EDITABLE.any? do |editable|
public_send("#{editable}_changed?")
end end
def runner_queue_key
"runner:build_queue:#{self.token}"
end end
def tag_constraints def tag_constraints
......
...@@ -15,6 +15,11 @@ module Issuable ...@@ -15,6 +15,11 @@ module Issuable
include Taskable include Taskable
include TimeTrackable include TimeTrackable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes and notes count for issues and merge requests
# lists avoiding n+1 queries and improving performance.
IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count)
included do included do
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description cache_markdown_field :description
...@@ -95,8 +100,8 @@ module Issuable ...@@ -95,8 +100,8 @@ module Issuable
def update_assignee_cache_counts def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist) # make sure we flush the cache for both the old *and* new assignees(if they exist)
previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
previous_assignee.update_cache_counts if previous_assignee previous_assignee&.update_cache_counts
assignee.update_cache_counts if assignee assignee&.update_cache_counts
end end
# We want to use optimistic lock for cases when only title or description are involved # We want to use optimistic lock for cases when only title or description are involved
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment