Commit 93f3e906 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ce-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 9e2c4d03 cd3e4101
*.js.es6 gitlab-language=javascript
...@@ -63,7 +63,7 @@ stages: ...@@ -63,7 +63,7 @@ stages:
<<: *dedicated-runner <<: *dedicated-runner
<<: *use-db <<: *use-db
script: script:
- JOB_NAME=( $CI_BUILD_NAME ) - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]} - export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
...@@ -83,7 +83,7 @@ stages: ...@@ -83,7 +83,7 @@ stages:
<<: *dedicated-runner <<: *dedicated-runner
<<: *use-db <<: *use-db
script: script:
- JOB_NAME=( $CI_BUILD_NAME ) - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]} - export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
...@@ -185,7 +185,7 @@ spinach 9 10: *spinach-knapsack ...@@ -185,7 +185,7 @@ spinach 9 10: *spinach-knapsack
<<: *dedicated-runner <<: *dedicated-runner
stage: test stage: test
script: script:
- bundle exec $CI_BUILD_NAME - bundle exec $CI_JOB_NAME
rubocop: rubocop:
<<: *ruby-static-analysis <<: *ruby-static-analysis
...@@ -315,7 +315,7 @@ migration paths: ...@@ -315,7 +315,7 @@ migration paths:
- sed -i 's/localhost/redis/g' config/resque.yml - sed -i 's/localhost/redis/g' config/resque.yml
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3 - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- bundle exec rake db:drop db:create db:schema:load db:seed_fu - bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_BUILD_REF - git checkout $CI_COMMIT_SHA
- source scripts/prepare_build.sh - source scripts/prepare_build.sh
- bundle exec rake db:migrate - bundle exec rake db:migrate
...@@ -353,7 +353,7 @@ lint:javascript:report: ...@@ -353,7 +353,7 @@ lint:javascript:report:
stage: post-test stage: post-test
before_script: [] before_script: []
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' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
- yarn run eslint-report || true # ignore exit code - yarn run eslint-report || true # ignore exit code
artifacts: artifacts:
name: eslint-report name: eslint-report
...@@ -379,7 +379,6 @@ trigger_docs: ...@@ -379,7 +379,6 @@ trigger_docs:
- master@gitlab-org/gitlab-ee - master@gitlab-org/gitlab-ee
# Notify slack in the end # Notify slack in the end
notify:slack: notify:slack:
stage: post-test stage: post-test
<<: *dedicated-runner <<: *dedicated-runner
...@@ -387,7 +386,7 @@ notify:slack: ...@@ -387,7 +386,7 @@ notify:slack:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
script: script:
- ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/pipelines>" - ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>"
when: on_failure when: on_failure
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
......
...@@ -5,3 +5,13 @@ ...@@ -5,3 +5,13 @@
### Proposal ### Proposal
### Links / references ### Links / references
### Documentation blurb
(Write the start of the documentation of this feature here, include:
1. Why should someone use it; what's the underlying problem.
2. What is the solution.
3. How does someone use this
During implementation, this can then be copied and used as a starter for the documentation.)
...@@ -78,6 +78,13 @@ towards getting your issue resolved. ...@@ -78,6 +78,13 @@ towards getting your issue resolved.
Issues and merge requests should be in English and contain appropriate language Issues and merge requests should be in English and contain appropriate language
for audiences of all ages. for audiences of all ages.
If a contributor is no longer actively working on a submitted merge request
we can decide that the merge request will be finished by one of our
[Merge request coaches][team] or close the merge request. We make this decision
based on how important the change is for our product vision. If a Merge request
coach is going to finish the merge request we assign the
~"coach will finish" label.
## Helping others ## Helping others
Please help other GitLab users when you can. The channels people will reach out Please help other GitLab users when you can. The channels people will reach out
......
...@@ -363,4 +363,4 @@ gem 'vmstat', '~> 2.3.0' ...@@ -363,4 +363,4 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.2.1' gem 'gitaly', '~> 0.3.0'
...@@ -274,7 +274,7 @@ GEM ...@@ -274,7 +274,7 @@ GEM
json json
get_process_mem (0.2.0) get_process_mem (0.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.2.1) gitaly (0.3.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -337,7 +337,7 @@ GEM ...@@ -337,7 +337,7 @@ GEM
multi_json (~> 1.10) multi_json (~> 1.10)
retriable (~> 1.4) retriable (~> 1.4)
signet (~> 0.6) signet (~> 0.6)
google-protobuf (3.2.0.1) google-protobuf (3.2.0.2)
googleauth (0.5.1) googleauth (0.5.1)
faraday (~> 0.9) faraday (~> 0.9)
jwt (~> 1.4) jwt (~> 1.4)
...@@ -937,7 +937,7 @@ DEPENDENCIES ...@@ -937,7 +937,7 @@ DEPENDENCIES
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0) gemojione (~> 3.0)
gitaly (~> 0.2.1) gitaly (~> 0.3.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-elasticsearch-git (= 1.1.1) gitlab-elasticsearch-git (= 1.1.1)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import emojiMap from 'emojis/digests.json'; import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json'; import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from './behaviors/gl_emoji'; import { glEmojiTag } from './behaviors/gl_emoji';
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const requestAnimationFrame = window.requestAnimationFrame || const requestAnimationFrame = window.requestAnimationFrame ||
...@@ -454,14 +455,21 @@ AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji) ...@@ -454,14 +455,21 @@ AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji)
AwardsHandler AwardsHandler
.prototype .prototype
.addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) { .addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) {
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); if (isEmojiNameValid(emoji)) {
frequentlyUsedEmojis.push(emoji); this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
}
}; };
AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() { AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() {
const frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(','); return this.frequentlyUsedEmojis || (() => {
return _.compact(_.uniq(frequentlyUsedEmojis)); const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
inputName => isEmojiNameValid(inputName),
);
return this.frequentlyUsedEmojis;
})();
}; };
AwardsHandler.prototype.setupSearch = function setupSearch() { AwardsHandler.prototype.setupSearch = function setupSearch() {
......
...@@ -13,9 +13,14 @@ function emojiImageTag(name, src) { ...@@ -13,9 +13,14 @@ function emojiImageTag(name, src) {
} }
function assembleFallbackImageSrc(inputName) { function assembleFallbackImageSrc(inputName) {
const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName; emojiAliases[inputName] : inputName;
const emojiInfo = emojiMap[name]; let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`; const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
return fallbackImageSrc; return fallbackImageSrc;
...@@ -26,9 +31,15 @@ const glEmojiTagDefaults = { ...@@ -26,9 +31,15 @@ const glEmojiTagDefaults = {
}; };
function glEmojiTag(inputName, options) { function glEmojiTag(inputName, options) {
const opts = Object.assign({}, glEmojiTagDefaults, options); const opts = Object.assign({}, glEmojiTagDefaults, options);
const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName; emojiAliases[inputName] : inputName;
const emojiInfo = emojiMap[name]; let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = assembleFallbackImageSrc(name); const fallbackImageSrc = assembleFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`; const fallbackSpriteClass = `emoji-${name}`;
......
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
function isEmojiNameValid(inputName) {
const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
return name && emojiMap[name];
}
export default isEmojiNameValid;
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
/* global Vue */ /* global Vue */
/* global Sortable */ /* global Sortable */
require('./board_blank_state'); import boardBlankState from './board_blank_state';
require('./board_delete'); require('./board_delete');
require('./board_list'); require('./board_list');
...@@ -17,7 +18,7 @@ require('./board_list'); ...@@ -17,7 +18,7 @@ require('./board_list');
components: { components: {
'board-list': gl.issueBoards.BoardList, 'board-list': gl.issueBoards.BoardList,
'board-delete': gl.issueBoards.BoardDelete, 'board-delete': gl.issueBoards.BoardDelete,
'board-blank-state': gl.issueBoards.BoardBlankState boardBlankState,
}, },
props: { props: {
list: Object, list: Object,
......
/* eslint-disable space-before-function-paren, comma-dangle */
/* global Vue */
/* global ListLabel */ /* global ListLabel */
/* global Cookies */
const Store = gl.issueBoards.BoardsStore;
(() => { export default {
const Store = gl.issueBoards.BoardsStore; template: `
<div class="board-blank-state">
<p>
Add the following default lists to your Issue Board with one click:
</p>
<ul class="board-blank-state-list">
<li v-for="label in predefinedLabels">
<span
class="label-color"
:style="{ backgroundColor: label.color }">
</span>
{{ label.title }}
</li>
</ul>
<p>
Starting out with the default set of lists will get you right on the way to making the most of your board.
</p>
<button
class="btn btn-create btn-inverted btn-block"
type="button"
@click.stop="addDefaultLists">
Add default lists
</button>
<button
class="btn btn-default btn-block"
type="button"
@click.stop="clearBlankState">
Nevermind, I'll use my own
</button>
</div>
`,
data() {
return {
predefinedLabels: [
new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Doing', color: '#5CB85C' }),
],
};
},
methods: {
addDefaultLists() {
this.clearBlankState();
window.gl = window.gl || {}; this.predefinedLabels.forEach((label, i) => {
window.gl.issueBoards = window.gl.issueBoards || {}; Store.addList({
title: label.title,
gl.issueBoards.BoardBlankState = Vue.extend({ position: i,
data () { list_type: 'label',
return { label: {
predefinedLabels: [
new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Doing', color: '#5CB85C' })
]
};
},
methods: {
addDefaultLists () {
this.clearBlankState();
this.predefinedLabels.forEach((label, i) => {
Store.addList({
title: label.title, title: label.title,
position: i, color: label.color,
list_type: 'label', },
label: {
title: label.title,
color: label.color
}
});
}); });
});
Store.state.lists = _.sortBy(Store.state.lists, 'position'); Store.state.lists = _.sortBy(Store.state.lists, 'position');
// Save the labels // Save the labels
gl.boardService.generateDefaultLists() gl.boardService.generateDefaultLists()
.then((resp) => { .then((resp) => {
resp.json().forEach((listObj) => { resp.json().forEach((listObj) => {
const list = Store.findList('title', listObj.title); const list = Store.findList('title', listObj.title);
list.id = listObj.id; list.id = listObj.id;
list.label.id = listObj.label.id; list.label.id = listObj.label.id;
list.getIssues(); list.getIssues();
});
}); });
}, })
clearBlankState: Store.removeBlankState.bind(Store) .catch(() => {
} Store.removeList(undefined, 'label');
}); Cookies.remove('issue_board_welcome_hidden', {
})(); path: '',
});
Store.addBlankState();
});
},
clearBlankState: Store.removeBlankState.bind(Store),
},
};
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
if (this.activeTab === 'selected') { if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet'; obj.title = 'You haven\'t selected any issues yet';
obj.content = ` obj.content = `
Go back to <strong>All issues</strong> and select some issues Go back to <strong>Open issues</strong> and select some issues
to add to your board. to add to your board.
`; `;
} }
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
class="btn btn-default" class="btn btn-default"
@click="changeTab('all')" @click="changeTab('all')"
v-if="activeTab === 'selected'"> v-if="activeTab === 'selected'">
All issues Open issues
</button> </button>
</div> </div>
</div> </div>
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
href="#" href="#"
role="button" role="button"
@click.prevent="changeTab('all')"> @click.prevent="changeTab('all')">
All issues Open issues
<span class="badge"> <span class="badge">
{{ issuesCount }} {{ issuesCount }}
</span> </span>
......
/* eslint-disable no-new, no-param-reassign */ /* eslint-disable no-param-reassign */
/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ import CommitPipelinesTable from './pipelines_table';
window.Vue = require('vue'); window.Vue = require('vue');
require('./pipelines_table'); window.Vue.use(require('vue-resource'));
/** /**
* Commits View > Pipelines Tab > Pipelines Table. * Commits View > Pipelines Tab > Pipelines Table.
* Merge Request View > Pipelines Tab > Pipelines Table. * Merge Request View > Pipelines Tab > Pipelines Table.
...@@ -21,7 +22,7 @@ $(() => { ...@@ -21,7 +22,7 @@ $(() => {
} }
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView(); gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
......
/* globals Vue */
/* eslint-disable no-unused-vars, no-param-reassign */
/**
* Pipelines service.
*
* Used to fetch the data used to render the pipelines table.
* Uses Vue.Resource
*/
class PipelinesService {
/**
* 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);
}
/**
* Given the root param provided when the class is initialized, will
* make a GET request.
*
* @return {Promise}
*/
all() {
return this.pipelines.get();
}
}
window.gl = window.gl || {};
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesService = PipelinesService;
/* eslint-disable no-new, no-param-reassign */ /* eslint-disable no-new*/
/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ /* global Flash */
import Vue from 'vue';
window.Vue = require('vue'); import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
window.Vue.use(require('vue-resource')); import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
require('../../lib/utils/common_utils'); import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
require('../../vue_shared/vue_resource_interceptor'); import eventHub from '../../vue_pipelines_index/event_hub';
require('../../vue_shared/components/pipelines_table'); import '../../lib/utils/common_utils';
require('./pipelines_service'); import '../../vue_shared/vue_resource_interceptor';
const PipelineStore = require('./pipelines_store');
/** /**
* *
...@@ -20,48 +19,59 @@ const PipelineStore = require('./pipelines_store'); ...@@ -20,48 +19,59 @@ const PipelineStore = require('./pipelines_store');
* as soon as we have Webpack and can load them directly into JS files. * as soon as we have Webpack and can load them directly into JS files.
*/ */
(() => { export default Vue.component('pipelines-table', {
window.gl = window.gl || {}; components: {
gl.commits = gl.commits || {}; 'pipelines-table-component': PipelinesTableComponent,
gl.commits.pipelines = gl.commits.pipelines || {}; },
gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', { /**
* Accesses the DOM to provide the needed data.
* Returns the necessary props to render `pipelines-table-component` component.
*
* @return {Object}
*/
data() {
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
const store = new PipelineStore();
components: { return {
'pipelines-table-component': gl.pipelines.PipelinesTableComponent, endpoint: pipelinesTableData.endpoint,
}, store,
state: store.state,
isLoading: false,
};
},
/** /**
* Accesses the DOM to provide the needed data. * When the component is about to be mounted, tell the service to fetch the data
* Returns the necessary props to render `pipelines-table-component` component. *
* * A request to fetch the pipelines will be made.
* @return {Object} * 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.
data() { *
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; */
const store = new PipelineStore(); beforeMount() {
this.service = new PipelinesService(this.endpoint);
return { this.fetchPipelines();
endpoint: pipelinesTableData.endpoint,
store, eventHub.$on('refreshPipelines', this.fetchPipelines);
state: store.state, },
isLoading: false,
}; beforeUpdate() {
}, if (this.state.pipelines.length && this.$children) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
/** beforeDestroyed() {
* When the component is about to be mounted, tell the service to fetch the data eventHub.$off('refreshPipelines');
* },
* A request to fetch the pipelines will be made.
* 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.
*
*/
beforeMount() {
const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
methods: {
fetchPipelines() {
this.isLoading = true; this.isLoading = true;
return pipelinesService.all() return this.service.getPipelines()
.then(response => response.json()) .then(response => response.json())
.then((json) => { .then((json) => {
// depending of the endpoint the response can either bring a `pipelines` key or not. // depending of the endpoint the response can either bring a `pipelines` key or not.
...@@ -71,34 +81,30 @@ const PipelineStore = require('./pipelines_store'); ...@@ -71,34 +81,30 @@ const PipelineStore = require('./pipelines_store');
}) })
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); new Flash('An error occurred while fetching the pipelines, please reload the page again.');
}); });
}, },
},
beforeUpdate() { template: `
if (this.state.pipelines.length && this.$children) { <div class="pipelines">
PipelineStore.startTimeAgoLoops.call(this, Vue); <div class="realtime-loading" v-if="isLoading">
} <i class="fa fa-spinner fa-spin"></i>
}, </div>
template: `
<div class="pipelines">
<div class="realtime-loading" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="blank-state blank-state-no-icon" <div class="blank-state blank-state-no-icon"
v-if="!isLoading && state.pipelines.length === 0"> v-if="!isLoading && state.pipelines.length === 0">
<h2 class="blank-state-title js-blank-state-title"> <h2 class="blank-state-title js-blank-state-title">
No pipelines to show No pipelines to show
</h2> </h2>
</div> </div>
<div class="table-holder pipelines" <div class="table-holder pipelines"
v-if="!isLoading && state.pipelines.length > 0"> v-if="!isLoading && state.pipelines.length > 0">
<pipelines-table-component :pipelines="state.pipelines"/> <pipelines-table-component
</div> :pipelines="state.pipelines"
:service="service" />
</div> </div>
`, </div>
}); `,
})(); });
...@@ -18,7 +18,8 @@ window.CompareAutocomplete = (function() { ...@@ -18,7 +18,8 @@ window.CompareAutocomplete = (function() {
return $.ajax({ return $.ajax({
url: $dropdown.data('refs-url'), url: $dropdown.data('refs-url'),
data: { data: {
ref: $dropdown.data('ref') ref: $dropdown.data('ref'),
search: term,
} }
}).done(function(refs) { }).done(function(refs) {
return callback(refs); return callback(refs);
...@@ -26,7 +27,7 @@ window.CompareAutocomplete = (function() { ...@@ -26,7 +27,7 @@ window.CompareAutocomplete = (function() {
}, },
selectable: true, selectable: true,
filterable: true, filterable: true,
filterByText: true, filterRemote: true,
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]', filterInput: 'input[type="search"]',
renderRow: function(ref) { renderRow: function(ref) {
......
...@@ -118,10 +118,10 @@ const gfmRules = { ...@@ -118,10 +118,10 @@ const gfmRules = {
}, },
SyntaxHighlightFilter: { SyntaxHighlightFilter: {
'pre.code.highlight'(el, t) { 'pre.code.highlight'(el, t) {
const text = t.trim(); const text = t.trimRight();
let lang = el.getAttribute('lang'); let lang = el.getAttribute('lang');
if (lang === 'plaintext') { if (!lang || lang === 'plaintext') {
lang = ''; lang = '';
} }
...@@ -157,7 +157,7 @@ const gfmRules = { ...@@ -157,7 +157,7 @@ const gfmRules = {
const backticks = Array(backtickCount + 1).join('`'); const backticks = Array(backtickCount + 1).join('`');
const spaceOrNoSpace = backtickCount > 1 ? ' ' : ''; const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks; return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
}, },
'blockquote'(el, text) { 'blockquote'(el, text) {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
...@@ -273,28 +273,29 @@ const gfmRules = { ...@@ -273,28 +273,29 @@ const gfmRules = {
class CopyAsGFM { class CopyAsGFM {
constructor() { constructor() {
$(document).on('copy', '.md, .wiki', this.handleCopy); $(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('paste', '.js-gfm-input', this.handlePaste); $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
$(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this));
} }
handleCopy(e) { copyAsGFM(e, transformer) {
const clipboardData = e.originalEvent.clipboardData; const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return; if (!clipboardData) return;
const documentFragment = window.gl.utils.getSelectedFragment(); const documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) return; if (!documentFragment) return;
// If the documentFragment contains more than just Markdown, don't copy as GFM. const el = transformer(documentFragment.cloneNode(true));
if (documentFragment.querySelector('.md, .wiki')) return; if (!el) return;
e.preventDefault(); e.preventDefault();
clipboardData.setData('text/plain', documentFragment.textContent); e.stopPropagation();
const gfm = CopyAsGFM.nodeToGFM(documentFragment); clipboardData.setData('text/plain', el.textContent);
clipboardData.setData('text/x-gfm', gfm); clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el));
} }
handlePaste(e) { pasteGFM(e) {
const clipboardData = e.originalEvent.clipboardData; const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return; if (!clipboardData) return;
...@@ -306,7 +307,47 @@ class CopyAsGFM { ...@@ -306,7 +307,47 @@ class CopyAsGFM {
window.gl.utils.insertText(e.target, gfm); window.gl.utils.insertText(e.target, gfm);
} }
static transformGFMSelection(documentFragment) {
// If the documentFragment contains more than just Markdown, don't copy as GFM.
if (documentFragment.querySelector('.md, .wiki')) return null;
return documentFragment;
}
static transformCodeSelection(documentFragment) {
const lineEls = documentFragment.querySelectorAll('.line');
let codeEl;
if (lineEls.length > 1) {
codeEl = document.createElement('pre');
codeEl.className = 'code highlight';
const lang = lineEls[0].getAttribute('lang');
if (lang) {
codeEl.setAttribute('lang', lang);
}
} else {
codeEl = document.createElement('code');
}
if (lineEls.length > 0) {
for (let i = 0; i < lineEls.length; i += 1) {
const lineEl = lineEls[i];
codeEl.appendChild(lineEl);
codeEl.appendChild(document.createTextNode('\n'));
}
} else {
codeEl.appendChild(documentFragment);
}
return codeEl;
}
static nodeToGFM(node) { static nodeToGFM(node) {
if (node.nodeType === Node.COMMENT_NODE) {
return '';
}
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === Node.TEXT_NODE) {
return node.textContent; return node.textContent;
} }
......
...@@ -25,6 +25,7 @@ import collapseIcon from '../icons/collapse_icon.svg'; ...@@ -25,6 +25,7 @@ import collapseIcon from '../icons/collapse_icon.svg';
role="button" role="button"
data-container="body" data-container="body"
data-placement="top" data-placement="top"
data-html="true"
:data-line-type="lineType" :data-line-type="lineType"
:title="note.authorName + ': ' + note.noteTruncated" :title="note.authorName + ': ' + note.noteTruncated"
:src="note.authorAvatar" :src="note.authorAvatar"
......
...@@ -206,10 +206,13 @@ const UserCallout = require('./user_callout'); ...@@ -206,10 +206,13 @@ const UserCallout = require('./user_callout');
new gl.Diff(); new gl.Diff();
new ZenMode(); new ZenMode();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
break; break;
case 'projects:commit:pipelines': case 'projects:commit:pipelines':
new MiniPipelineGraph({ new MiniPipelineGraph({
container: '.js-pipeline-table', container: '.js-commit-pipeline-graph',
}).bindEvents(); }).bindEvents();
break; break;
case 'projects:commits:show': case 'projects:commits:show':
......
...@@ -132,7 +132,7 @@ class DueDateSelect { ...@@ -132,7 +132,7 @@ class DueDateSelect {
const selectedDateValue = this.datePayload[this.abilityName].due_date; const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
this.$loading.fadeIn(); this.$loading.removeClass('hidden').fadeIn();
if (isDropdown) { if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown'); this.$dropdown.trigger('loading.gl.dropdown');
......
/* eslint-disable no-param-reassign, no-new */ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table'; import EnvironmentTable from './environments_table';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination';
import '../../lib/utils/common_utils';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
export default Vue.component('environment-component', { export default Vue.component('environment-component', {
components: { components: {
'environment-table': EnvironmentTable, 'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination, 'table-pagination': TablePaginationComponent,
}, },
data() { data() {
...@@ -60,6 +57,7 @@ export default Vue.component('environment-component', { ...@@ -60,6 +57,7 @@ export default Vue.component('environment-component', {
canCreateEnvironmentParsed() { canCreateEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment); return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
}, },
<<<<<<< HEAD
/** /**
* Pagination should only be rendered when we have information about it and when the * Pagination should only be rendered when we have information about it and when the
...@@ -71,6 +69,8 @@ export default Vue.component('environment-component', { ...@@ -71,6 +69,8 @@ export default Vue.component('environment-component', {
return this.state.paginationInformation && this.state.paginationInformation.totalPages > 1; return this.state.paginationInformation && this.state.paginationInformation.totalPages > 1;
}, },
=======
>>>>>>> ce-com/master
}, },
/** /**
......
...@@ -5,20 +5,27 @@ ...@@ -5,20 +5,27 @@
*/ */
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions'; import ActionsComponent from './environment_actions';
import ExternalUrlComponent from './environment_external_url'; import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop'; import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback'; import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button'; import TerminalButtonComponent from './environment_terminal_button';
import '../../lib/utils/text_utility'; import CommitComponent from '../../vue_shared/components/commit';
import '../../vue_shared/components/commit';
<<<<<<< HEAD
=======
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
>>>>>>> ce-com/master
const timeagoInstance = new Timeago(); const timeagoInstance = new Timeago();
export default { export default {
components: { components: {
'commit-component': gl.CommitComponent, 'commit-component': CommitComponent,
'actions-component': ActionsComponent, 'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent, 'external-url-component': ExternalUrlComponent,
'stop-component': StopComponent, 'stop-component': StopComponent,
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
* Dumb component used to render top level environments and * Dumb component used to render top level environments and
* the folder view. * the folder view.
*/ */
<<<<<<< HEAD
import EnvironmentItem from './environment_item'; import EnvironmentItem from './environment_item';
import DeployBoard from './deploy_board_component'; import DeployBoard from './deploy_board_component';
...@@ -11,6 +12,13 @@ export default { ...@@ -11,6 +12,13 @@ export default {
components: { components: {
EnvironmentItem, EnvironmentItem,
DeployBoard, DeployBoard,
=======
import EnvironmentTableRowComponent from './environment_item';
export default {
components: {
'environment-item': EnvironmentTableRowComponent,
>>>>>>> ce-com/master
}, },
props: { props: {
......
/* eslint-disable no-param-reassign, no-new */ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table'; import EnvironmentTable from '../components/environments_table';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination';
const Vue = window.Vue = require('vue'); import '../../lib/utils/common_utils';
window.Vue.use(require('vue-resource')); import '../../vue_shared/vue_resource_interceptor';
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
export default Vue.component('environment-folder-view', { export default Vue.component('environment-folder-view', {
components: { components: {
'environment-table': EnvironmentTable, 'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination, 'table-pagination': TablePaginationComponent,
}, },
data() { data() {
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class EnvironmentsService { export default class EnvironmentsService {
constructor(endpoint) { constructor(endpoint) {
......
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
/** /**
* Environments Store. * Environments Store.
* *
......
...@@ -159,7 +159,7 @@ class FilteredSearchVisualTokens { ...@@ -159,7 +159,7 @@ class FilteredSearchVisualTokens {
const name = token.querySelector('.name'); const name = token.querySelector('.name');
const value = token.querySelector('.value'); const value = token.querySelector('.value');
if (token.classList.contains('filtered-search-token')) { if (token.classList.contains('filtered-search-token') && value) {
FilteredSearchVisualTokens.addFilterVisualToken(name.innerText); FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
input.value = value.innerText; input.value = value.innerText;
} else { } else {
......
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
if (!selected.length) { if (!selected.length) {
data[abilityName].label_ids = ['']; data[abilityName].label_ids = [''];
} }
$loading.fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
......
...@@ -197,7 +197,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -197,7 +197,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.opts.ci_sha = data.sha; _this.opts.ci_sha = data.sha;
_this.updateCommitUrls(data.sha); _this.updateCommitUrls(data.sha);
} }
if (showNotification) { if (showNotification && data.status) {
status = _this.ciLabelForStatus(data.status); status = _this.ciLabelForStatus(data.status);
if (status === "preparing") { if (status === "preparing") {
title = _this.opts.ci_title.preparing; title = _this.opts.ci_title.preparing;
......
...@@ -181,7 +181,7 @@ ...@@ -181,7 +181,7 @@
} }
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
$loading.fadeIn(); $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () { .then(function () {
...@@ -193,7 +193,7 @@ ...@@ -193,7 +193,7 @@
data = {}; data = {};
data[abilityName] = {}; data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null; data[abilityName].milestone_id = selected != null ? selected : null;
$loading.fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
......
...@@ -25,7 +25,6 @@ ...@@ -25,7 +25,6 @@
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);
......
...@@ -5,6 +5,7 @@ class Todos { ...@@ -5,6 +5,7 @@ class Todos {
constructor() { constructor() {
this.initFilters(); this.initFilters();
this.bindEvents(); this.bindEvents();
this.todo_ids = [];
this.cleanupWrapper = this.cleanup.bind(this); this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('beforeunload', this.cleanupWrapper); document.addEventListener('beforeunload', this.cleanupWrapper);
...@@ -17,16 +18,16 @@ class Todos { ...@@ -17,16 +18,16 @@ class Todos {
unbindEvents() { unbindEvents() {
$('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper); $('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper);
$('.js-todos-mark-all').off('click', this.allDoneClickedWrapper); $('.js-todos-mark-all', '.js-todos-undo-all').off('click', this.updateallStateClickedWrapper);
$('.todo').off('click', this.goToTodoUrl); $('.todo').off('click', this.goToTodoUrl);
} }
bindEvents() { bindEvents() {
this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this); this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this);
this.allDoneClickedWrapper = this.allDoneClicked.bind(this); this.updateAllStateClickedWrapper = this.updateAllStateClicked.bind(this);
$('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper); $('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper);
$('.js-todos-mark-all').on('click', this.allDoneClickedWrapper); $('.js-todos-mark-all, .js-todos-undo-all').on('click', this.updateAllStateClickedWrapper);
$('.todo').on('click', this.goToTodoUrl); $('.todo').on('click', this.goToTodoUrl);
} }
...@@ -57,14 +58,14 @@ class Todos { ...@@ -57,14 +58,14 @@ class Todos {
e.preventDefault(); e.preventDefault();
const target = e.target; const target = e.target;
target.setAttribute('disabled', ''); target.setAttribute('disabled', true);
target.classList.add('disabled'); target.classList.add('disabled');
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: target.getAttribute('href'), url: target.dataset.href,
dataType: 'json', dataType: 'json',
data: { data: {
'_method': target.getAttribute('data-method'), '_method': target.dataset.method,
}, },
success: (data) => { success: (data) => {
this.updateRowState(target); this.updateRowState(target);
...@@ -73,25 +74,6 @@ class Todos { ...@@ -73,25 +74,6 @@ class Todos {
}); });
} }
allDoneClicked(e) {
e.preventDefault();
const $target = $(e.currentTarget);
$target.disable();
$.ajax({
type: 'POST',
url: $target.attr('href'),
dataType: 'json',
data: {
'_method': 'delete',
},
success: (data) => {
$target.remove();
$('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
this.updateBadges(data);
},
});
}
updateRowState(target) { updateRowState(target) {
const row = target.closest('li'); const row = target.closest('li');
const restoreBtn = row.querySelector('.js-undo-todo'); const restoreBtn = row.querySelector('.js-undo-todo');
...@@ -112,6 +94,41 @@ class Todos { ...@@ -112,6 +94,41 @@ class Todos {
} }
} }
updateAllStateClicked(e) {
e.preventDefault();
const target = e.currentTarget;
const requestData = { '_method': target.dataset.method, ids: this.todo_ids };
target.setAttribute('disabled', true);
target.classList.add('disabled');
$.ajax({
type: 'POST',
url: target.dataset.href,
dataType: 'json',
data: requestData,
success: (data) => {
this.updateAllState(target, data);
return this.updateBadges(data);
},
});
}
updateAllState(target, data) {
const markAllDoneBtn = document.querySelector('.js-todos-mark-all');
const undoAllBtn = document.querySelector('.js-todos-undo-all');
const todoListContainer = document.querySelector('.js-todos-list-container');
const nothingHereContainer = document.querySelector('.js-nothing-here-container');
target.removeAttribute('disabled');
target.classList.remove('disabled');
this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : [];
undoAllBtn.classList.toggle('hidden');
markAllDoneBtn.classList.toggle('hidden');
todoListContainer.classList.toggle('hidden');
nothingHereContainer.classList.toggle('hidden');
}
updateBadges(data) { updateBadges(data) {
$(document).trigger('todo:toggle', data.count); $(document).trigger('todo:toggle', data.count);
document.querySelector('.todos-pending .badge').innerHTML = data.count; document.querySelector('.todos-pending .badge').innerHTML = data.count;
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
$loading = $block.find('.block-loading').fadeOut(); $loading = $block.find('.block-loading').fadeOut();
var updateIssueBoardsIssue = function () { var updateIssueBoardsIssue = function () {
$loading.fadeIn(); $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () { .then(function () {
$loading.fadeOut(); $loading.fadeOut();
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
data = {}; data = {};
data[abilityName] = {}; data[abilityName] = {};
data[abilityName].assignee_id = selected != null ? selected : null; data[abilityName].assignee_id = selected != null ? selected : null;
$loading.fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
......
/* eslint-disable no-new, no-alert */
/* global Flash */
import '~/flash';
import eventHub from '../event_hub';
export default {
props: {
endpoint: {
type: String,
required: true,
},
service: {
type: Object,
required: true,
},
title: {
type: String,
required: true,
},
icon: {
type: String,
required: true,
},
cssClass: {
type: String,
required: true,
},
confirmActionMessage: {
type: String,
required: false,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
iconClass() {
return `fa fa-${this.icon}`;
},
buttonClass() {
return `btn has-tooltip ${this.cssClass}`;
},
},
methods: {
onClick() {
if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
this.makeRequest();
} else if (!this.confirmActionMessage) {
this.makeRequest();
}
},
makeRequest() {
this.isLoading = true;
this.service.postAction(this.endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
},
template: `
<button
type="button"
@click="onClick"
:class="buttonClass"
:title="title"
:aria-label="title"
data-placement="top"
:disabled="isLoading">
<i :class="iconClass" aria-hidden="true"/>
<i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
</button>
`,
};
export default {
props: [
'pipeline',
],
computed: {
user() {
return !!this.pipeline.user;
},
},
template: `
<td>
<a
:href="pipeline.path"
class="js-pipeline-url-link">
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<a
class="js-pipeline-url-user"
v-if="user"
:href="pipeline.user.web_url">
<img
v-if="user"
class="avatar has-tooltip s20 "
:title="pipeline.user.name"
data-container="body"
:src="pipeline.user.avatar_url"
>
</a>
<span
v-if="!user"
class="js-pipeline-url-api api monospace">
API
</span>
<span
v-if="pipeline.flags.latest"
class="js-pipeline-url-lastest label label-success has-tooltip"
title="Latest pipeline for this branch"
data-original-title="Latest pipeline for this branch">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
class="js-pipeline-url-yaml label label-danger has-tooltip"
:title="pipeline.yaml_errors"
:data-original-title="pipeline.yaml_errors">
yaml invalid
</span>
<span
v-if="pipeline.flags.stuck"
class="js-pipeline-url-stuck label label-warning">
stuck
</span>
</td>
`,
};
/* eslint-disable no-new */
/* global Flash */
import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
export default {
props: {
actions: {
type: Array,
required: true,
},
service: {
type: Object,
required: true,
},
},
data() {
return {
playIconSvg,
isLoading: false,
};
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
},
template: `
<div class="btn-group" v-if="actions">
<button
type="button"
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
:disabled="isLoading">
${playIconSvg}
<i class="fa fa-caret-down" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-pipeline-action-link no-btn"
@click="onClickAction(action.path)">
${playIconSvg}
<span>{{action.name}}</span>
</button>
</li>
</ul>
</div>
`,
};
export default {
props: {
artifacts: {
type: Array,
required: true,
},
},
template: `
<div class="btn-group" role="group">
<button
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts">
<i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="artifact in artifacts">
<a
rel="nofollow"
:href="artifact.path">
<i class="fa fa-download" aria-hidden="true"></i>
<span>Download {{artifact.name}} artifacts</span>
</a>
</li>
</ul>
</div>
`,
};
/* global Flash */
import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
import createdSvg from 'icons/_icon_status_created_borderless.svg';
import failedSvg from 'icons/_icon_status_failed_borderless.svg';
import manualSvg from 'icons/_icon_status_manual_borderless.svg';
import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
import runningSvg from 'icons/_icon_status_running_borderless.svg';
import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
import successSvg from 'icons/_icon_status_success_borderless.svg';
import warningSvg from 'icons/_icon_status_warning_borderless.svg';
export default {
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
svg: svgsDictionary[this.stage.status.icon],
};
},
props: {
stage: {
type: Object,
required: true,
},
},
updated() {
if (this.builds) {
this.stopDropdownClickPropagation();
}
},
methods: {
fetchBuilds(e) {
const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
return flash;
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
e.stopPropagation();
});
},
},
computed: {
buildsOrSpinner() {
return this.builds ? this.builds : this.spinner;
},
dropdownClass() {
if (this.builds) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
},
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
},
},
template: `
<div>
<button
@click="fetchBuilds($event)"
:class="triggerButtonClass"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
:aria-label="stage.title">
<span v-html="svg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up" aria-hidden="true"></div>
<div
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner">
</div>
</ul>
</div>
`,
};
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
export default {
props: {
pipeline: {
type: Object,
required: true,
},
},
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
svg: svgsDictionary[this.pipeline.details.status.icon],
};
},
computed: {
cssClasses() {
return `ci-status ci-${this.pipeline.details.status.group}`;
},
detailsPath() {
const { status } = this.pipeline.details;
return status.has_details ? status.details_path : false;
},
content() {
return `${this.svg} ${this.pipeline.details.status.text}`;
},
},
template: `
<td class="commit-link">
<a
:class="cssClasses"
:href="detailsPath"
v-html="content">
</a>
</td>
`,
};
import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
export default {
data() {
return {
currentTime: new Date(),
iconTimerSvg,
};
},
props: ['pipeline'],
computed: {
timeAgo() {
return gl.utils.getTimeago();
},
localTimeFinished() {
return gl.utils.formatDate(this.pipeline.details.finished_at);
},
timeStopped() {
const changeTime = this.currentTime;
const options = {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
};
options.timeZoneName = 'short';
const finished = this.pipeline.details.finished_at;
if (!finished && changeTime) return false;
return ({ words: this.timeAgo.format(finished) });
},
duration() {
const { duration } = this.pipeline.details;
const date = new Date(duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
if (hh < 10) hh = `0${hh}`;
if (mm < 10) mm = `0${mm}`;
if (ss < 10) ss = `0${ss}`;
if (duration !== null) return `${hh}:${mm}:${ss}`;
return false;
},
},
methods: {
changeTime() {
this.currentTime = new Date();
},
},
template: `
<td class="pipelines-time-ago">
<p class="duration" v-if='duration'>
<span v-html="iconTimerSvg"></span>
{{duration}}
</p>
<p class="finished-at" v-if='timeStopped'>
<i class="fa fa-calendar"></i>
<time
data-toggle="tooltip"
data-placement="top"
data-container="body"
:data-original-title='localTimeFinished'>
{{timeStopped.words}}
</time>
</p>
</td>
`,
};
import Vue from 'vue';
export default new Vue();
/* eslint-disable no-param-reassign */ import PipelinesStore from './stores/pipelines_store';
/* global Vue, VueResource, gl */ import PipelinesComponent from './pipelines';
window.Vue = require('vue'); import '../vue_shared/vue_resource_interceptor';
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource')); window.Vue.use(require('vue-resource'));
require('../lib/utils/common_utils');
require('../vue_shared/vue_resource_interceptor');
require('./pipelines');
$(() => new Vue({ $(() => new Vue({
el: document.querySelector('.vue-pipelines-index'), el: document.querySelector('.vue-pipelines-index'),
data() { data() {
const project = document.querySelector('.pipelines'); const project = document.querySelector('.pipelines');
const store = new PipelinesStore();
return { return {
scope: project.dataset.url, store,
store: new gl.PipelineStore(), endpoint: project.dataset.url,
}; };
}, },
components: { components: {
'vue-pipelines': gl.VuePipelines, 'vue-pipelines': PipelinesComponent,
}, },
template: ` template: `
<vue-pipelines <vue-pipelines
:scope="scope" :endpoint="endpoint"
:store="store"> :store="store" />
</vue-pipelines>
`, `,
})); }));
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign, no-alert */
const playIconSvg = require('icons/_icon_play.svg');
((gl) => {
gl.VuePipelineActions = Vue.extend({
props: ['pipeline'],
computed: {
actions() {
return this.pipeline.details.manual_actions.length > 0;
},
artifacts() {
return this.pipeline.details.artifacts.length > 0;
},
},
methods: {
download(name) {
return `Download ${name} artifacts`;
},
/**
* Shows a dialog when the user clicks in the cancel button.
* We need to prevent the default behavior and stop propagation because the
* link relies on UJS.
*
* @param {Event} event
*/
confirmAction(event) {
if (!confirm('Are you sure you want to cancel this pipeline?')) {
event.preventDefault();
event.stopPropagation();
}
},
},
data() {
return { playIconSvg };
},
template: `
<td class="pipeline-actions">
<div class="pull-right">
<div class="btn-group">
<div class="btn-group" v-if="actions">
<button
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual job"
data-placement="top"
aria-label="Manual job">
<span v-html="playIconSvg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href="action.path" >
<span v-html="playIconSvg" aria-hidden="true"></span>
<span>{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group" v-if="artifacts">
<button
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts">
<i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href="artifact.path">
<i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group" v-if="pipeline.flags.retryable">
<a
class="btn btn-default btn-retry has-tooltip"
title="Retry"
rel="nofollow"
data-method="post"
data-placement="top"
data-toggle="dropdown"
:href='pipeline.retry_path'
aria-label="Retry">
<i class="fa fa-repeat" aria-hidden="true"></i>
</a>
</div>
<div class="btn-group" v-if="pipeline.flags.cancelable">
<a
class="btn btn-remove has-tooltip"
title="Cancel"
rel="nofollow"
data-method="post"
data-placement="top"
data-toggle="dropdown"
:href='pipeline.cancel_path'
aria-label="Cancel">
<i class="fa fa-remove" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VuePipelineUrl = Vue.extend({
props: [
'pipeline',
],
computed: {
user() {
return !!this.pipeline.user;
},
},
template: `
<td>
<a :href='pipeline.path'>
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<a
v-if='user'
:href='pipeline.user.web_url'
>
<img
v-if='user'
class="avatar has-tooltip s20 "
:title='pipeline.user.name'
data-container="body"
:src='pipeline.user.avatar_url'
>
</a>
<span
v-if='!user'
class="api monospace"
>
API
</span>
<span
v-if='pipeline.flags.latest'
class="label label-success has-tooltip"
title="Latest pipeline for this branch"
data-original-title="Latest pipeline for this branch"
>
latest
</span>
<span
v-if='pipeline.flags.yaml_errors'
class="label label-danger has-tooltip"
:title='pipeline.yaml_errors'
:data-original-title='pipeline.yaml_errors'
>
yaml invalid
</span>
<span
v-if='pipeline.flags.stuck'
class="label label-warning"
>
stuck
</span>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, gl */ /* global Flash */
/* eslint-disable no-param-reassign */ /* eslint-disable no-new */
import '~/flash';
import Vue from 'vue';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
import TablePaginationComponent from '../vue_shared/components/table_pagination';
window.Vue = require('vue'); export default {
require('../vue_shared/components/table_pagination'); props: {
require('./store'); endpoint: {
require('../vue_shared/components/pipelines_table'); type: String,
const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store'); required: true,
((gl) => {
gl.VuePipelines = Vue.extend({
components: {
'gl-pagination': gl.VueGlPagination,
'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
}, },
data() { store: {
return { type: Object,
pipelines: [], required: true,
timeLoopInterval: '',
intervalId: '',
apiScope: 'all',
pageInfo: {},
pagenum: 1,
count: {},
pageRequest: false,
};
},
props: ['scope', 'store'],
created() {
const pagenum = gl.utils.getParameterByName('page');
const scope = gl.utils.getParameterByName('scope');
if (pagenum) this.pagenum = pagenum;
if (scope) this.apiScope = scope;
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
}, },
},
components: {
'gl-pagination': TablePaginationComponent,
'pipelines-table-component': PipelinesTableComponent,
},
data() {
return {
state: this.store.state,
apiScope: 'all',
pagenum: 1,
pageRequest: false,
};
},
created() {
this.service = new PipelinesService(this.endpoint);
this.fetchPipelines();
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeUpdate() {
if (this.state.pipelines.length && this.$children) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
beforeUpdate() { beforeDestroyed() {
if (this.pipelines.length && this.$children) { eventHub.$off('refreshPipelines');
CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue); },
}
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
change(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
}, },
methods: { fetchPipelines() {
/** const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
* Will change the page number and update the URL. const scope = gl.utils.getParameterByName('scope') || this.apiScope;
*
* @param {Number} pageNumber desired page to go to. this.pageRequest = true;
*/ return this.service.getPipelines(scope, pageNumber)
change(pageNumber) { .then(resp => ({
const param = gl.utils.setParamInURL('page', pageNumber); headers: resp.headers,
body: resp.json(),
gl.utils.visitUrl(param); }))
return param; .then((response) => {
}, this.store.storeCount(response.body.count);
this.store.storePipelines(response.body.pipelines);
this.store.storePagination(response.headers);
})
.then(() => {
this.pageRequest = false;
})
.catch(() => {
this.pageRequest = false;
new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
}, },
template: ` },
<div> template: `
<div class="pipelines realtime-loading" v-if='pageRequest'> <div>
<i class="fa fa-spinner fa-spin"></i> <div class="pipelines realtime-loading" v-if="pageRequest">
</div> <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
<div class="blank-state blank-state-no-icon"
v-if="!pageRequest && pipelines.length === 0"> <div class="blank-state blank-state-no-icon"
<h2 class="blank-state-title js-blank-state-title"> v-if="!pageRequest && state.pipelines.length === 0">
No pipelines to show <h2 class="blank-state-title js-blank-state-title">
</h2> No pipelines to show
</div> </h2>
</div>
<div class="table-holder" v-if='!pageRequest && pipelines.length'>
<pipelines-table-component :pipelines='pipelines'/> <div class="table-holder" v-if="!pageRequest && state.pipelines.length">
</div> <pipelines-table-component
:pipelines="state.pipelines"
<gl-pagination :service="service"/>
v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
:pagenum='pagenum'
:change='change'
:count='count.all'
:pageInfo='pageInfo'
>
</gl-pagination>
</div> </div>
`,
}); <gl-pagination
})(window.gl || (window.gl = {})); v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
:pagenum="pagenum"
:change="change"
:count="state.count.all"
:pageInfo="state.pageInfo"
>
</gl-pagination>
</div>
`,
};
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class PipelinesService {
/**
* Commits and merge request endpoints need to be requested with `.json`.
*
* The url provided to request the pipelines in the new merge request
* page already has `.json`.
*
* @param {String} root
*/
constructor(root) {
let endpoint;
if (root.indexOf('.json') === -1) {
endpoint = `${root}.json`;
} else {
endpoint = root;
}
this.pipelines = Vue.resource(endpoint);
}
getPipelines(scope, page) {
return this.pipelines.get({ scope, page });
}
/**
* Post request for all pipelines actions.
* Endpoint content type needs to be:
* `Content-Type:application/x-www-form-urlencoded`
*
* @param {String} endpoint
* @return {Promise}
*/
postAction(endpoint) {
return Vue.http.post(endpoint, {}, { emulateJSON: true });
}
}
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */
import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
import createdSvg from 'icons/_icon_status_created_borderless.svg';
import failedSvg from 'icons/_icon_status_failed_borderless.svg';
import manualSvg from 'icons/_icon_status_manual_borderless.svg';
import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
import runningSvg from 'icons/_icon_status_running_borderless.svg';
import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
import successSvg from 'icons/_icon_status_success_borderless.svg';
import warningSvg from 'icons/_icon_status_warning_borderless.svg';
((gl) => {
gl.VueStage = Vue.extend({
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
svg: svgsDictionary[this.stage.status.icon],
};
},
props: {
stage: {
type: Object,
required: true,
},
},
updated() {
if (this.builds) {
this.stopDropdownClickPropagation();
}
},
methods: {
fetchBuilds(e) {
const areaExpanded = e.currentTarget.attributes['aria-expanded'];
if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
return flash;
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (e) => {
e.stopPropagation();
});
},
},
computed: {
buildsOrSpinner() {
return this.builds ? this.builds : this.spinner;
},
dropdownClass() {
if (this.builds) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
},
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
},
},
template: `
<div>
<button
@click="fetchBuilds($event)"
:class="triggerButtonClass"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
:aria-label="stage.title">
<span v-html="svg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up" aria-hidden="true"></div>
<div
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner">
</div>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, gl */
/* eslint-disable no-param-reassign */
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
((gl) => {
gl.VueStatusScope = Vue.extend({
props: [
'pipeline',
],
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
svg: svgsDictionary[this.pipeline.details.status.icon],
};
},
computed: {
cssClasses() {
const cssObject = { 'ci-status': true };
cssObject[`ci-${this.pipeline.details.status.group}`] = true;
return cssObject;
},
detailsPath() {
const { status } = this.pipeline.details;
return status.has_details ? status.details_path : false;
},
content() {
return `${this.svg} ${this.pipeline.details.status.text}`;
},
},
template: `
<td class="commit-link">
<a
:class="cssClasses"
:href="detailsPath"
v-html="content">
</a>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global gl, Flash */
/* eslint-disable no-param-reassign */
((gl) => {
const pageValues = (headers) => {
const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = gl.utils.parseIntPagination(normalized);
return paginationInfo;
};
gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) {
this.pageRequest = true;
return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => {
const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
const res = JSON.parse(response.body);
this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
this.pageRequest = false;
}, () => {
this.pageRequest = false;
return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
}
};
})(window.gl || (window.gl = {}));
/* eslint-disable no-underscore-dangle*/ /* eslint-disable no-underscore-dangle*/
/** import '../../vue_realtime_listener';
* Pipelines' Store for commits view.
* export default class PipelinesStore {
* Used to store the Pipelines rendered in the commit view in the pipelines table.
*/
require('../../vue_realtime_listener');
class PipelinesStore {
constructor() { constructor() {
this.state = {}; this.state = {};
this.state.pipelines = []; this.state.pipelines = [];
this.state.count = {};
this.state.pageInfo = {};
} }
storePipelines(pipelines = []) { storePipelines(pipelines = []) {
this.state.pipelines = pipelines; this.state.pipelines = pipelines;
}
return pipelines; storeCount(count = {}) {
this.state.count = count;
}
storePagination(pagination = {}) {
let paginationInfo;
if (Object.keys(pagination).length) {
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
} else {
paginationInfo = pagination;
}
this.state.pageInfo = paginationInfo;
} }
/** /**
* FIXME: Move this inside the component.
*
* Once the data is received we will start the time ago loops. * Once the data is received we will start the time ago loops.
* *
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
* update the time to show how long as passed. * update the time to show how long as passed.
* *
*/ */
static startTimeAgoLoops() { 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,5 +59,3 @@ class PipelinesStore { ...@@ -44,5 +59,3 @@ class PipelinesStore {
gl.VueRealtimeListener(removeIntervals, startIntervals); gl.VueRealtimeListener(removeIntervals, startIntervals);
} }
} }
module.exports = PipelinesStore;
/* global Vue, gl */
/* eslint-disable no-param-reassign */
window.Vue = require('vue');
require('../lib/utils/datetime_utility');
const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
((gl) => {
gl.VueTimeAgo = Vue.extend({
data() {
return {
currentTime: new Date(),
iconTimerSvg,
};
},
props: ['pipeline'],
computed: {
timeAgo() {
return gl.utils.getTimeago();
},
localTimeFinished() {
return gl.utils.formatDate(this.pipeline.details.finished_at);
},
timeStopped() {
const changeTime = this.currentTime;
const options = {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
};
options.timeZoneName = 'short';
const finished = this.pipeline.details.finished_at;
if (!finished && changeTime) return false;
return ({ words: this.timeAgo.format(finished) });
},
duration() {
const { duration } = this.pipeline.details;
const date = new Date(duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
if (hh < 10) hh = `0${hh}`;
if (mm < 10) mm = `0${mm}`;
if (ss < 10) ss = `0${ss}`;
if (duration !== null) return `${hh}:${mm}:${ss}`;
return false;
},
},
methods: {
changeTime() {
this.currentTime = new Date();
},
},
template: `
<td class="pipelines-time-ago">
<p class="duration" v-if='duration'>
<span v-html="iconTimerSvg"></span>
{{duration}}
</p>
<p class="finished-at" v-if='timeStopped'>
<i class="fa fa-calendar"></i>
<time
data-toggle="tooltip"
data-placement="top"
data-container="body"
:data-original-title='localTimeFinished'>
{{timeStopped.words}}
</time>
</p>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue */ import commitIconSvg from 'icons/_icon_commit.svg';
window.Vue = require('vue');
const commitIconSvg = require('icons/_icon_commit.svg'); export default {
props: {
(() => { /**
window.gl = window.gl || {}; * Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
window.gl.CommitComponent = Vue.component('commit-component', { * if false will render `fa-code-fork` icon.
*/
props: { tag: {
/** type: Boolean,
* Indicates the existance of a tag. required: false,
* Used to render the correct icon, if true will render `fa-tag` icon, default: false,
* if false will render `fa-code-fork` icon.
*/
tag: {
type: Boolean,
required: false,
default: false,
},
/**
* If provided is used to render the branch name and url.
* Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
},
/**
* Used to show the commit short sha that links to the commit url.
*/
shortSha: {
type: String,
required: false,
default: '',
},
/**
* If provided shows the commit tile.
*/
title: {
type: String,
required: false,
default: '',
},
/**
* If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
},
}, },
computed: { /**
/** * If provided is used to render the branch name and url.
* Used to verify if all the properties needed to render the commit * Should contain the following properties:
* ref section were provided. * name
* * ref_url
* TODO: Improve this! Use lodash _.has when we have it. */
* commitRef: {
* @returns {Boolean} type: Object,
*/ required: false,
hasCommitRef() { default: () => ({}),
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.web_url &&
this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author &&
this.author.username ? `${this.author.username}'s avatar` : null;
},
}, },
data() { /**
return { commitIconSvg }; * Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
}, },
template: ` /**
<div class="branch-commit"> * Used to show the commit short sha that links to the commit url.
*/
<div v-if="hasCommitRef" class="icon-container"> shortSha: {
<i v-if="tag" class="fa fa-tag"></i> type: String,
<i v-if="!tag" class="fa fa-code-fork"></i> required: false,
</div> default: '',
},
<a v-if="hasCommitRef"
class="monospace branch-name" /**
:href="commitRef.ref_url"> * If provided shows the commit tile.
{{commitRef.name}} */
</a> title: {
type: String,
<div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> required: false,
default: '',
<a class="commit-id monospace" },
:href="commitUrl">
{{shortSha}} /**
</a> * If provided renders information about the author of the commit.
* When provided should include:
<p class="commit-title"> * `avatar_url` to render the avatar icon
<span v-if="title"> * `web_url` to link to user profile
<a v-if="hasAuthor" * `username` to render alt and title tags
class="avatar-image-container" */
:href="author.web_url"> author: {
<img type: Object,
class="avatar has-tooltip s20" required: false,
:src="author.avatar_url" default: () => ({}),
:alt="userImageAltDescription" },
:title="author.username" /> },
</a>
computed: {
<a class="commit-row-message" /**
:href="commitUrl"> * Used to verify if all the properties needed to render the commit
{{title}} * ref section were provided.
</a> *
</span> * TODO: Improve this! Use lodash _.has when we have it.
<span v-else> *
Cant find HEAD commit for this branch * @returns {Boolean}
</span> */
</p> hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.web_url &&
this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author &&
this.author.username ? `${this.author.username}'s avatar` : null;
},
},
data() {
return { commitIconSvg };
},
template: `
<div class="branch-commit">
<div v-if="hasCommitRef" class="icon-container">
<i v-if="tag" class="fa fa-tag"></i>
<i v-if="!tag" class="fa fa-code-fork"></i>
</div> </div>
`,
}); <a v-if="hasCommitRef"
})(); class="monospace branch-name"
:href="commitRef.ref_url">
{{commitRef.name}}
</a>
<div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
<a class="commit-id monospace"
:href="commitUrl">
{{shortSha}}
</a>
<p class="commit-title">
<span v-if="title">
<a v-if="hasAuthor"
class="avatar-image-container"
:href="author.web_url">
<img
class="avatar has-tooltip s20"
:src="author.avatar_url"
:alt="userImageAltDescription"
:title="author.username" />
</a>
<a class="commit-row-message"
:href="commitUrl">
{{title}}
</a>
</span>
<span v-else>
Cant find HEAD commit for this branch
</span>
</p>
</div>
`,
};
/* eslint-disable no-param-reassign */ import PipelinesTableRowComponent from './pipelines_table_row';
/* global Vue */
require('./pipelines_table_row');
/** /**
* Pipelines Table Component. * Pipelines Table Component.
* *
* Given an array of objects, renders a table. * Given an array of objects, renders a table.
*/ */
export default {
(() => { props: {
window.gl = window.gl || {}; pipelines: {
gl.pipelines = gl.pipelines || {}; type: Array,
required: true,
gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', { default: () => ([]),
props: {
pipelines: {
type: Array,
required: true,
default: () => ([]),
},
}, },
components: { service: {
'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent, type: Object,
required: true,
}, },
},
components: {
'pipelines-table-row-component': PipelinesTableRowComponent,
},
template: ` template: `
<table class="table ci-table"> <table class="table ci-table">
<thead> <thead>
<tr> <tr>
<th class="js-pipeline-status pipeline-status">Status</th> <th class="js-pipeline-status pipeline-status">Status</th>
<th class="js-pipeline-info pipeline-info">Pipeline</th> <th class="js-pipeline-info pipeline-info">Pipeline</th>
<th class="js-pipeline-commit pipeline-commit">Commit</th> <th class="js-pipeline-commit pipeline-commit">Commit</th>
<th class="js-pipeline-stages pipeline-stages">Stages</th> <th class="js-pipeline-stages pipeline-stages">Stages</th>
<th class="js-pipeline-date pipeline-date"></th> <th class="js-pipeline-date pipeline-date"></th>
<th class="js-pipeline-actions pipeline-actions"></th> <th class="js-pipeline-actions pipeline-actions"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="model in pipelines" <template v-for="model in pipelines"
v-bind:model="model"> v-bind:model="model">
<tr is="pipelines-table-row-component" <tr is="pipelines-table-row-component"
:pipeline="model"></tr> :pipeline="model"
</template> :service="service"></tr>
</tbody> </template>
</table> </tbody>
`, </table>
}); `,
})(); };
/* global Vue, gl */ const PAGINATION_UI_BUTTON_LIMIT = 4;
/* eslint-disable no-param-reassign, no-plusplus */ const UI_LIMIT = 6;
const SPREAD = '...';
window.Vue = require('vue'); const PREV = 'Prev';
const NEXT = 'Next';
((gl) => { const FIRST = '<< First';
const PAGINATION_UI_BUTTON_LIMIT = 4; const LAST = 'Last >>';
const UI_LIMIT = 6;
const SPREAD = '...'; export default {
const PREV = 'Prev'; props: {
const NEXT = 'Next'; /**
const FIRST = '<< First'; This function will take the information given by the pagination component
const LAST = 'Last >>';
Here is an example `change` method:
gl.VueGlPagination = Vue.extend({
props: { change(pagenum) {
gl.utils.visitUrl(`?page=${pagenum}`);
// TODO: Consider refactoring in light of turbolinks removal.
/**
This function will take the information given by the pagination component
Here is an example `change` method:
change(pagenum) {
gl.utils.visitUrl(`?page=${pagenum}`);
},
*/
change: {
type: Function,
required: true,
}, },
*/
change: {
type: Function,
required: true,
},
/** /**
pageInfo will come from the headers of the API call pageInfo will come from the headers of the API call
in the `.then` clause of the VueResource API call in the `.then` clause of the VueResource API call
there should be a function that contructs the pageInfo for this component there should be a function that contructs the pageInfo for this component
This is an example: This is an example:
const pageInfo = headers => ({ const pageInfo = headers => ({
perPage: +headers['X-Per-Page'], perPage: +headers['X-Per-Page'],
page: +headers['X-Page'], page: +headers['X-Page'],
total: +headers['X-Total'], total: +headers['X-Total'],
totalPages: +headers['X-Total-Pages'], totalPages: +headers['X-Total-Pages'],
nextPage: +headers['X-Next-Page'], nextPage: +headers['X-Next-Page'],
previousPage: +headers['X-Prev-Page'], previousPage: +headers['X-Prev-Page'],
}); });
*/ */
pageInfo: {
pageInfo: { type: Object,
type: Object, required: true,
required: true,
},
}, },
methods: { },
changePage(e) { methods: {
const text = e.target.innerText; changePage(e) {
const { totalPages, nextPage, previousPage } = this.pageInfo; const text = e.target.innerText;
const { totalPages, nextPage, previousPage } = this.pageInfo;
switch (text) {
case SPREAD: switch (text) {
break; case SPREAD:
case LAST: break;
this.change(totalPages); case LAST:
break; this.change(totalPages);
case NEXT: break;
this.change(nextPage); case NEXT:
break; this.change(nextPage);
case PREV: break;
this.change(previousPage); case PREV:
break; this.change(previousPage);
case FIRST: break;
this.change(1); case FIRST:
break; this.change(1);
default: break;
this.change(+text); default:
break; this.change(+text);
} break;
}, }
}, },
computed: { },
prev() { computed: {
return this.pageInfo.previousPage; prev() {
}, return this.pageInfo.previousPage;
next() { },
return this.pageInfo.nextPage; next() {
}, return this.pageInfo.nextPage;
getItems() { },
const total = this.pageInfo.totalPages; getItems() {
const page = this.pageInfo.page; const total = this.pageInfo.totalPages;
const items = []; const page = this.pageInfo.page;
const items = [];
if (page > 1) items.push({ title: FIRST }); if (page > 1) items.push({ title: FIRST });
if (page > 1) { if (page > 1) {
items.push({ title: PREV, prev: true }); items.push({ title: PREV, prev: true });
} else { } else {
items.push({ title: PREV, disabled: true, prev: true }); items.push({ title: PREV, disabled: true, prev: true });
} }
if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
for (let i = start; i <= end; i++) { for (let i = start; i <= end; i += 1) {
const isActive = i === page; const isActive = i === page;
items.push({ title: i, active: isActive, page: true }); items.push({ title: i, active: isActive, page: true });
} }
if (total - page > PAGINATION_UI_BUTTON_LIMIT) { if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
items.push({ title: SPREAD, separator: true, page: true }); items.push({ title: SPREAD, separator: true, page: true });
} }
if (page === total) { if (page === total) {
items.push({ title: NEXT, disabled: true, next: true }); items.push({ title: NEXT, disabled: true, next: true });
} else if (total - page >= 1) { } else if (total - page >= 1) {
items.push({ title: NEXT, next: true }); items.push({ title: NEXT, next: true });
} }
if (total - page >= 1) items.push({ title: LAST, last: true }); if (total - page >= 1) items.push({ title: LAST, last: true });
return items; return items;
},
}, },
template: ` },
<div class="gl-pagination"> template: `
<ul class="pagination clearfix"> <div class="gl-pagination">
<li v-for='item in getItems' <ul class="pagination clearfix">
:class='{ <li v-for='item in getItems'
page: item.page, :class='{
prev: item.prev, page: item.page,
next: item.next, prev: item.prev,
separator: item.separator, next: item.next,
active: item.active, separator: item.separator,
disabled: item.disabled active: item.active,
}' disabled: item.disabled
> }'
<a @click="changePage($event)">{{item.title}}</a> >
</li> <a @click="changePage($event)">{{item.title}}</a>
</ul> </li>
</div> </ul>
`, </div>
}); `,
})(window.gl || (window.gl = {})); };
/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, /* eslint-disable no-param-reassign, no-plusplus */
no-param-reassign, no-plusplus */ import Vue from 'vue';
/* global Vue */ import VueResource from 'vue-resource';
Vue.use(VueResource);
Vue.http.interceptors.push((request, next) => { Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
next((response) => { next(() => {
Vue.activeResources--; Vue.activeResources--;
}); });
}); });
......
...@@ -96,11 +96,9 @@ ...@@ -96,11 +96,9 @@
.award-control { .award-control {
margin: 3px 5px 3px 0; margin: 3px 5px 3px 0;
padding: 5px 6px; padding: .35em .4em;
outline: 0; outline: 0;
line-height: 1;
&.disabled { &.disabled {
cursor: default; cursor: default;
...@@ -140,10 +138,12 @@ ...@@ -140,10 +138,12 @@
} }
.icon, .icon,
gl-emoji,
.award-control-icon { .award-control-icon {
float: left; vertical-align: middle;
margin-right: 5px; margin-right: 0.15em;
font-size: 18px; font-size: 1.5em;
line-height: 1;
} }
.award-control-icon-loading { .award-control-icon-loading {
...@@ -154,4 +154,8 @@ ...@@ -154,4 +154,8 @@
color: $border-gray-normal; color: $border-gray-normal;
margin-top: 1px; margin-top: 1px;
} }
.award-control-text {
vertical-align: middle;
}
} }
...@@ -2,5 +2,6 @@ gl-emoji { ...@@ -2,5 +2,6 @@ gl-emoji {
display: inline-block; display: inline-block;
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1.5em; font-size: 1.5em;
} }
...@@ -144,7 +144,7 @@ ...@@ -144,7 +144,7 @@
.scroll-container { .scroll-container {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
overflow-x: scroll; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 100%;
} }
......
...@@ -138,7 +138,6 @@ ...@@ -138,7 +138,6 @@
.nav-links { .nav-links {
display: inline-block; display: inline-block;
width: 50%;
margin-bottom: 0; margin-bottom: 0;
border-bottom: none; border-bottom: none;
......
...@@ -306,6 +306,11 @@ a > code { ...@@ -306,6 +306,11 @@ a > code {
* Textareas intended for GFM * Textareas intended for GFM
* *
*/ */
textarea.js-gfm-input {
font-family: $monospace_font;
font-size: 13px;
}
.strikethrough { .strikethrough {
text-decoration: line-through; text-decoration: line-through;
} }
......
...@@ -38,6 +38,38 @@ ...@@ -38,6 +38,38 @@
} }
} }
.pipeline-info {
.status-icon-container {
display: inline-block;
vertical-align: middle;
margin-right: 3px;
svg {
display: block;
width: 22px;
height: 22px;
}
}
.mr-widget-pipeline-graph {
display: inline-block;
vertical-align: middle;
margin: 0 -6px 0 0;
.dropdown-menu {
margin-top: 11px;
}
}
}
.branch-info .commit-icon {
margin-right: 3px;
svg {
top: 3px;
}
}
/* /*
* Commit message textarea for web editor and * Commit message textarea for web editor and
* custom merge request message * custom merge request message
......
...@@ -369,13 +369,11 @@ ...@@ -369,13 +369,11 @@
// Custom CSS for components // Custom CSS for components
.item-conmmit-component { .item-conmmit-component {
.commit-icon { .commit-icon {
position: relative;
top: 3px;
left: 1px;
display: inline-block;
svg { svg {
float: left; display: inline-block;
width: 20px;
height: 20px;
vertical-align: bottom;
} }
} }
} }
......
...@@ -56,7 +56,10 @@ ul.related-merge-requests > li { ...@@ -56,7 +56,10 @@ ul.related-merge-requests > li {
.merge-request-id { .merge-request-id {
display: inline-block; display: inline-block;
width: 3em; }
.merge-request-info {
margin-left: 5px;
} }
.merge-request-status { .merge-request-status {
......
...@@ -72,11 +72,6 @@ ...@@ -72,11 +72,6 @@
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
font-size: 14px; font-size: 14px;
} }
svg,
.fa {
margin-right: 0;
}
} }
.btn-group { .btn-group {
...@@ -925,3 +920,22 @@ ...@@ -925,3 +920,22 @@
} }
} }
} }
/**
* Play button with icon in dropdowns
*/
.ci-table .no-btn {
border: none;
background: none;
outline: none;
width: 100%;
text-align: left;
.icon-play {
position: relative;
top: 2px;
margin-right: 5px;
height: 13px;
width: 12px;
}
}
...@@ -22,12 +22,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -22,12 +22,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end end
def destroy_all def destroy_all
TodoService.new.mark_todos_as_done(@todos, current_user) updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok } format.js { head :ok }
format.json { render json: todos_counts } format.json { render json: todos_counts.merge(updated_ids: updated_ids) }
end end
end end
...@@ -37,6 +37,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -37,6 +37,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
render json: todos_counts render json: todos_counts
end end
def bulk_restore
TodoService.new.mark_todos_as_pending_by_ids(params[:ids], current_user)
render json: todos_counts
end
# Used in TodosHelper also # Used in TodosHelper also
def self.todos_count_format(count) def self.todos_count_format(count)
count >= 100 ? '99+' : count count >= 100 ? '99+' : count
...@@ -45,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -45,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
private private
def find_todos def find_todos
@todos ||= TodosFinder.new(current_user, params).execute @todos ||= TodosFinder.new(current_user, params.merge(include_associations: true)).execute
end end
def todos_counts def todos_counts
......
...@@ -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, :notified_of_own_activity) params.require(:user).permit(:notification_email)
end end
end end
...@@ -8,9 +8,12 @@ class Projects::BlameController < Projects::ApplicationController ...@@ -8,9 +8,12 @@ class Projects::BlameController < Projects::ApplicationController
def show def show
@blob = @repository.blob_at(@commit.id, @path) @blob = @repository.blob_at(@commit.id, @path)
return render_404 unless @blob return render_404 unless @blob
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@blame_groups = Gitlab::Blame.new(@blob, @commit).groups @blame_groups = Gitlab::Blame.new(@blob, @commit).groups
end end
end end
...@@ -11,16 +11,18 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -11,16 +11,18 @@ class Projects::BranchesController < Projects::ApplicationController
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@branches = BranchesFinder.new(@repository, params).execute @branches = BranchesFinder.new(@repository, params).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page]) unless params[:show_all].present?
respond_to do |format| respond_to do |format|
format.html do format.html do
paginate_branches
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@max_commits = @branches.reduce(0) do |memo, branch| @max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch) diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end end
end end
format.json do format.json do
paginate_branches unless params[:show_all]
render json: @branches.map(&:name) render json: @branches.map(&:name)
end end
end end
...@@ -91,6 +93,10 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -91,6 +93,10 @@ class Projects::BranchesController < Projects::ApplicationController
end end
end end
def paginate_branches
@branches = Kaminari.paginate_array(@branches).page(params[:page])
end
def url_to_autodeploy_setup(project, branch_name) def url_to_autodeploy_setup(project, branch_name)
namespace_project_new_blob_path( namespace_project_new_blob_path(
project.namespace, project.namespace,
......
...@@ -14,7 +14,9 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -14,7 +14,9 @@ class Projects::TagsController < Projects::ApplicationController
@tags = TagsFinder.new(@repository, params).execute @tags = TagsFinder.new(@repository, params).execute
@tags = Kaminari.paginate_array(@tags).page(params[:page]) @tags = Kaminari.paginate_array(@tags).page(params[:page])
@releases = project.releases.where(tag: @tags.map(&:name)) tag_names = @tags.map(&:name)
@tags_pipelines = @project.pipelines.latest_successful_for_refs(tag_names)
@releases = project.releases.where(tag: tag_names)
end end
def show def show
......
...@@ -45,10 +45,12 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -45,10 +45,12 @@ class Projects::WikisController < Projects::ApplicationController
return render('empty') unless can?(current_user, :create_wiki, @project) return render('empty') unless can?(current_user, :create_wiki, @project)
@page = @project_wiki.find_page(params[:id]) @page = @project_wiki.find_page(params[:id])
@page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page) if @page.valid?
# Triggers repository update on secondary nodes when Geo is enabled # Triggers repository update on secondary nodes when Geo is enabled
Gitlab::Geo.notify_wiki_update(@project) if Gitlab::Geo.primary? Gitlab::Geo.notify_wiki_update(@project) if Gitlab::Geo.primary?
redirect_to( redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page), namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.' notice: 'Wiki was successfully updated.'
......
...@@ -268,8 +268,9 @@ class ProjectsController < Projects::ApplicationController ...@@ -268,8 +268,9 @@ class ProjectsController < Projects::ApplicationController
@project_wiki = @project.wiki @project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id]) @wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user) elsif @project.feature_available?(:issues, current_user)
@issues = issues_collection @issues = issues_collection.page(params[:page])
@issues = @issues.page(params[:page]) @collection_type = 'Issue'
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
end end
render :show render :show
...@@ -316,6 +317,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -316,6 +317,7 @@ class ProjectsController < Projects::ApplicationController
:namespace_id, :namespace_id,
:only_allow_merge_if_all_discussions_are_resolved, :only_allow_merge_if_all_discussions_are_resolved,
:only_allow_merge_if_pipeline_succeeds, :only_allow_merge_if_pipeline_succeeds,
:printing_merge_request_link_enabled,
:path, :path,
:public_builds, :public_builds,
:request_access_enabled, :request_access_enabled,
......
...@@ -24,6 +24,7 @@ class TodosFinder ...@@ -24,6 +24,7 @@ class TodosFinder
def execute def execute
items = current_user.todos items = current_user.todos
items = include_associations(items)
items = by_action_id(items) items = by_action_id(items)
items = by_action(items) items = by_action(items)
items = by_author(items) items = by_author(items)
...@@ -38,6 +39,17 @@ class TodosFinder ...@@ -38,6 +39,17 @@ class TodosFinder
private private
def include_associations(items)
return items unless params[:include_associations]
items.includes(
[
target: { project: [:route, namespace: :route] },
author: { namespace: :route },
]
)
end
def action_id? def action_id?
action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i) action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i)
end end
......
...@@ -203,4 +203,18 @@ module BlobHelper ...@@ -203,4 +203,18 @@ module BlobHelper
'blob-language' => @blob && @blob.language.try(:ace_mode) 'blob-language' => @blob && @blob.language.try(:ace_mode)
} }
end end
def copy_file_path_button(file_path)
clipboard_button(clipboard_text: file_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
end
def copy_blob_content_button(blob)
return if markup?(blob.name)
clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard")
end
def open_raw_file_button(path)
link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', title: 'Open raw', data: { container: 'body' }
end
end end
...@@ -172,7 +172,9 @@ module GitlabMarkdownHelper ...@@ -172,7 +172,9 @@ module GitlabMarkdownHelper
# text hasn't already been truncated, then append "..." to the node contents # text hasn't already been truncated, then append "..." to the node contents
# and return true. Otherwise return false. # and return true. Otherwise return false.
def truncate_if_block(node, truncated) def truncate_if_block(node, truncated)
if node.element? && node.description&.block? && !truncated return true if truncated
if node.element? && (node.description&.block? || node.matches?('pre > code > .line'))
node.inner_html = "#{node.inner_html}..." if node.next_sibling node.inner_html = "#{node.inner_html}..." if node.next_sibling
true true
else else
......
...@@ -82,12 +82,13 @@ module MilestonesHelper ...@@ -82,12 +82,13 @@ module MilestonesHelper
def milestone_remaining_days(milestone) def milestone_remaining_days(milestone)
if milestone.expired? if milestone.expired?
content_tag(:strong, 'Past due') content_tag(:strong, 'Past due')
elsif milestone.due_date
days = milestone.remaining_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} remaining"
elsif milestone.upcoming? elsif milestone.upcoming?
content_tag(:strong, 'Upcoming') content_tag(:strong, 'Upcoming')
elsif milestone.due_date
time_ago = time_ago_in_words(milestone.due_date)
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
content.slice!("about ")
content << " remaining"
elsif milestone.start_date && milestone.start_date.past? elsif milestone.start_date && milestone.start_date.past?
days = milestone.elapsed_days days = milestone.elapsed_days
content = content_tag(:strong, days) content = content_tag(:strong, days)
......
...@@ -16,6 +16,7 @@ module NavHelper ...@@ -16,6 +16,7 @@ module NavHelper
"page-gutter build-sidebar right-sidebar-expanded" "page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') || elsif current_path?('wikis#show') ||
current_path?('wikis#edit') || current_path?('wikis#edit') ||
current_path?('wikis#update') ||
current_path?('wikis#history') || current_path?('wikis#history') ||
current_path?('wikis#git_access') current_path?('wikis#git_access')
"page-gutter wiki-sidebar right-sidebar-expanded" "page-gutter wiki-sidebar right-sidebar-expanded"
......
...@@ -39,9 +39,13 @@ module TodosHelper ...@@ -39,9 +39,13 @@ module TodosHelper
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor) todo.target, anchor: anchor)
else else
path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] if todo.build_failed?
# associated namespace and route would be loaded from the db again if todo.project was used
path.unshift(:pipelines) if todo.build_failed? project = todo.target.project
path = [:pipelines, project.namespace.becomes(Namespace), project, todo.target]
else
path = [todo.target]
end
polymorphic_path(path, anchor: anchor) polymorphic_path(path, anchor: anchor)
end end
...@@ -99,8 +103,7 @@ module TodosHelper ...@@ -99,8 +103,7 @@ module TodosHelper
end end
def todo_projects_options def todo_projects_options
projects = current_user.authorized_projects.sorted_by_activity.non_archived projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route
projects = projects.includes(:namespace)
projects = projects.map do |project| projects = projects.map do |project|
{ id: project.id, text: project.name_with_namespace } { id: project.id, text: project.name_with_namespace }
......
...@@ -540,6 +540,16 @@ module Ci ...@@ -540,6 +540,16 @@ module Ci
Gitlab::Ci::Build::Credentials::Factory.new(self).create! Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end end
def dependencies
depended_jobs = depends_on_builds
return depended_jobs unless options[:dependencies].present?
depended_jobs.select do |job|
options[:dependencies].include?(job.name)
end
end
private private
def update_artifacts_size def update_artifacts_size
......
...@@ -113,6 +113,12 @@ module Ci ...@@ -113,6 +113,12 @@ module Ci
success.latest(ref).order(id: :desc).first success.latest(ref).order(id: :desc).first
end end
def self.latest_successful_for_refs(refs)
success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
hash[pipeline.ref] ||= pipeline
end
end
def self.truncate_sha(sha) def self.truncate_sha(sha)
sha[0...8] sha[0...8]
end end
......
...@@ -231,6 +231,10 @@ class Commit ...@@ -231,6 +231,10 @@ class Commit
project.pipelines.where(sha: sha) project.pipelines.where(sha: sha)
end end
def latest_pipeline
pipelines.last
end
def status(ref = nil) def status(ref = nil)
@statuses ||= {} @statuses ||= {}
...@@ -317,7 +321,14 @@ class Commit ...@@ -317,7 +321,14 @@ class Commit
end end
def raw_diffs(*args) def raw_diffs(*args)
raw.diffs(*args) use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
if use_gitaly && !deltas_only
Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
else
raw.diffs(*args)
end
end end
def diffs(diff_options = nil) def diffs(diff_options = nil)
......
...@@ -272,6 +272,7 @@ module Issuable ...@@ -272,6 +272,7 @@ module Issuable
user: user.hook_attrs, user: user.hook_attrs,
project: project.hook_attrs, project: project.hook_attrs,
object_attributes: hook_attrs, object_attributes: hook_attrs,
labels: labels.map(&:hook_attrs),
# DEPRECATED # DEPRECATED
repository: project.hook_attrs.slice(:name, :url, :description, :homepage) repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
} }
......
...@@ -51,11 +51,13 @@ module Routable ...@@ -51,11 +51,13 @@ module Routable
paths.each do |path| paths.each do |path|
path = connection.quote(path) path = connection.quote(path)
where = "(routes.path = #{path})"
if cast_lower where =
where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))" if cast_lower
end "(LOWER(routes.path) = LOWER(#{path}))"
else
"(routes.path = #{path})"
end
wheres << where wheres << where
end end
......
...@@ -66,7 +66,13 @@ class Issue < ActiveRecord::Base ...@@ -66,7 +66,13 @@ class Issue < ActiveRecord::Base
end end
def hook_attrs def hook_attrs
attributes attrs = {
total_time_spent: total_time_spent,
human_total_time_spent: human_total_time_spent,
human_time_estimate: human_time_estimate
}
attributes.merge!(attrs)
end end
def self.reference_prefix def self.reference_prefix
......
...@@ -169,6 +169,10 @@ class Label < ActiveRecord::Base ...@@ -169,6 +169,10 @@ class Label < ActiveRecord::Base
end end
end end
def hook_attrs
attributes
end
private private
def issues_count(user, params = {}) def issues_count(user, params = {})
......
...@@ -9,6 +9,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -9,6 +9,7 @@ class MergeRequest < ActiveRecord::Base
belongs_to :target_project, class_name: "Project" belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project" belongs_to :source_project, class_name: "Project"
belongs_to :project, foreign_key: :target_project_id
belongs_to :merge_user, class_name: "User" belongs_to :merge_user, class_name: "User"
has_many :approvals, dependent: :destroy has_many :approvals, dependent: :destroy
...@@ -546,7 +547,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -546,7 +547,10 @@ class MergeRequest < ActiveRecord::Base
source: source_project.try(:hook_attrs), source: source_project.try(:hook_attrs),
target: target_project.hook_attrs, target: target_project.hook_attrs,
last_commit: nil, last_commit: nil,
work_in_progress: work_in_progress? work_in_progress: work_in_progress?,
total_time_spent: total_time_spent,
human_total_time_spent: human_total_time_spent,
human_time_estimate: human_time_estimate
} }
if diff_head_commit if diff_head_commit
...@@ -560,10 +564,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -560,10 +564,6 @@ class MergeRequest < ActiveRecord::Base
target_project != source_project target_project != source_project
end end
def project
target_project
end
# If the merge request closes any issues, save this information in the # If the merge request closes any issues, save this information in the
# `MergeRequestsClosingIssues` model. This is a performance optimization. # `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires # Calculating this information for a number of merge requests requires
......
...@@ -149,7 +149,13 @@ class WikiPage ...@@ -149,7 +149,13 @@ class WikiPage
end end
# Returns boolean True or False if this instance # Returns boolean True or False if this instance
# has been fully saved to disk or not. # is the latest commit version of the page.
def latest?
!historical?
end
# Returns boolean True or False if this instance
# has been fully created on disk or not.
def persisted? def persisted?
@persisted == true @persisted == true
end end
...@@ -220,6 +226,8 @@ class WikiPage ...@@ -220,6 +226,8 @@ class WikiPage
end end
def save(method, *args) def save(method, *args)
saved = false
project_wiki = wiki project_wiki = wiki
if valid? && project_wiki.send(method, *args) if valid? && project_wiki.send(method, *args)
...@@ -237,10 +245,10 @@ class WikiPage ...@@ -237,10 +245,10 @@ class WikiPage
set_attributes set_attributes
@persisted = true @persisted = true
saved = true
else else
errors.add(:base, project_wiki.error_message) if project_wiki.error_message errors.add(:base, project_wiki.error_message) if project_wiki.error_message
@persisted = false
end end
@persisted saved
end end
end end
...@@ -6,7 +6,7 @@ module MergeRequests ...@@ -6,7 +6,7 @@ module MergeRequests
merge_request.source_project = find_source_project merge_request.source_project = find_source_project
merge_request.target_project = find_target_project merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch merge_request.target_branch = find_target_branch
merge_request.can_be_created = branches_valid? && source_branch_specified? && target_branch_specified? merge_request.can_be_created = branches_valid?
compare_branches if branches_present? compare_branches if branches_present?
assign_title_and_description if merge_request.can_be_created assign_title_and_description if merge_request.can_be_created
......
...@@ -7,6 +7,8 @@ module MergeRequests ...@@ -7,6 +7,8 @@ module MergeRequests
end end
def execute(changes) def execute(changes)
return [] unless project.printing_merge_request_link_enabled
branches = get_branches(changes) branches = get_branches(changes)
merge_requests_map = opened_merge_requests_from_source_branches(branches) merge_requests_map = opened_merge_requests_from_source_branches(branches)
branches.map do |branch| branches.map do |branch|
...@@ -23,10 +25,7 @@ module MergeRequests ...@@ -23,10 +25,7 @@ module MergeRequests
def opened_merge_requests_from_source_branches(branches) def opened_merge_requests_from_source_branches(branches)
merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches) merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches)
merge_requests.inject({}) do |hash, mr| merge_requests.index_by(&:source_branch)
hash[mr.source_branch] = mr
hash
end
end end
def get_branches(changes) def get_branches(changes)
......
#
# Used by NotificationService to determine who should receive notification
#
class NotificationRecipientService
attr_reader :project
def initialize(project)
@project = project
end
def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
recipients = add_project_watchers(recipients)
end
recipients = add_custom_notifications(recipients, custom_action)
recipients = reject_mention_users(recipients)
# Re-assign is considered as a mention of the new assignee so we add the
# new assignee to the list of recipients after we rejected users with
# the "on mention" notification level
if [:reassign_merge_request, :reassign_issue].include?(custom_action)
recipients << previous_assignee if previous_assignee
recipients << target.assignee
end
recipients = reject_muted_users(recipients)
recipients = add_subscribed_users(recipients, target)
if [:new_issue, :new_merge_request].include?(custom_action)
recipients = add_labels_subscribers(recipients, target)
end
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user) if skip_current_user
recipients.uniq
end
def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user)
recipients.uniq
end
def build_new_note_recipients(note)
target = note.noteable
ability, subject = if note.for_personal_snippet?
[:read_personal_snippet, note.noteable]
else
[:read_project, note.project]
end
mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }
# Add all users participating in the thread (author, assignee, comment authors)
recipients =
if target.respond_to?(:participants)
target.participants(note.author)
else
mentioned_users
end
unless note.for_personal_snippet?
# Merge project watchers
recipients = add_project_watchers(recipients)
# Merge project with custom notification
recipients = add_custom_notifications(recipients, :new_note)
end
# Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - mentioned_users)
recipients = recipients + mentioned_users
recipients = reject_muted_users(recipients)
recipients = add_subscribed_users(recipients, note.noteable)
recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients = reject_users_without_access(recipients, note.noteable)
recipients.delete(note.author)
recipients.uniq
end
# Remove users with disabled notifications from array
# Also remove duplications and nil recipients
def reject_muted_users(users)
reject_users(users, :disabled)
end
protected
# Get project/group users with CUSTOM notification level
def add_custom_notifications(recipients, action)
user_ids = []
# Users with a notification setting on group or project
user_ids += user_ids_notifiable_on(project, :custom, action)
user_ids += user_ids_notifiable_on(project.group, :custom, action)
# Users with global level custom
user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global)
global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
user_ids += user_ids_with_global_level_custom(global_users_ids, action)
recipients.concat(User.find(user_ids))
end
def add_project_watchers(recipients)
recipients.concat(project_watchers).compact
end
# Get project users with WATCH notification level
def project_watchers
project_members_ids = user_ids_notifiable_on(project)
user_ids_with_project_global = user_ids_notifiable_on(project, :global)
user_ids_with_group_global = user_ids_notifiable_on(project.group, :global)
user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq)
user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids)
user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids)
User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a
end
# Remove users with notification level 'Mentioned'
def reject_mention_users(users)
reject_users(users, :mention)
end
def add_subscribed_users(recipients, target)
return recipients unless target.respond_to? :subscribers
recipients + target.subscribers(project)
end
def user_ids_notifiable_on(resource, notification_level = nil, action = nil)
return [] unless resource
if notification_level
settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
settings = settings.select { |setting| setting.events[action] } if action.present?
settings.map(&:user_id)
else
resource.notification_settings.pluck(:user_id)
end
end
# Build a list of user_ids based on project notification settings
def select_project_members_ids(project, global_setting, user_ids_global_level_watch)
user_ids = user_ids_notifiable_on(project, :watch)
# If project setting is global, add to watch list if global setting is watch
global_setting.each do |user_id|
if user_ids_global_level_watch.include?(user_id)
user_ids << user_id
end
end
user_ids
end
# Build a list of user_ids based on group notification settings
def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch)
uids = user_ids_notifiable_on(group, :watch)
# Group setting is watch, add to user_ids list if user is not project member
user_ids = []
uids.each do |user_id|
if project_members.exclude?(user_id)
user_ids << user_id
end
end
# Group setting is global, add to user_ids list if global setting is watch
global_setting.each do |user_id|
if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id)
user_ids << user_id
end
end
user_ids
end
def user_ids_with_global_level_watch(ids)
settings_with_global_level_of(:watch, ids).pluck(:user_id)
end
def user_ids_with_global_level_custom(ids, action)
settings = settings_with_global_level_of(:custom, ids)
settings = settings.select { |setting| setting.events[action] }
settings.map(&:user_id)
end
def settings_with_global_level_of(level, ids)
NotificationSetting.where(
user_id: ids,
source_type: nil,
level: NotificationSetting.levels[level]
)
end
# Reject users which has certain notification level
#
# Example:
# reject_users(users, :watch, project)
#
def reject_users(users, level)
level = level.to_s
unless NotificationSetting.levels.keys.include?(level)
raise 'Invalid notification level'
end
users = users.to_a.compact.uniq
users = users.select { |u| u.can?(:receive_notifications) }
users.reject do |user|
global_notification_setting = user.global_notification_setting
next global_notification_setting.level == level unless project
setting = user.notification_settings_for(project)
if project.group && (setting.nil? || setting.global?)
setting = user.notification_settings_for(project.group)
end
# reject users who globally set mention notification and has no setting per project/group
next global_notification_setting.level == level unless setting
# reject users who set mention notification in project
next true if setting.level == level
# reject users who have mention level in project and disabled in global settings
setting.global? && global_notification_setting.level == level
end
end
def reject_unsubscribed_users(recipients, target)
return recipients unless target.respond_to? :subscriptions
recipients.reject do |user|
subscription = target.subscriptions.find_by_user_id(user.id)
subscription && !subscription.subscribed
end
end
def reject_users_without_access(recipients, target)
ability = case target
when Issuable
:"read_#{target.to_ability_name}"
when Ci::Pipeline
:read_build # We have build trace in pipeline emails
end
return recipients unless ability
recipients.select do |user|
user.can?(ability, target)
end
end
def add_labels_subscribers(recipients, target, labels: nil)
return recipients unless target.respond_to? :labels
(labels || target.labels).each do |label|
recipients += label.subscribers(project)
end
recipients
end
# Build event key to search on custom notification level
# Check NotificationSetting::EMAIL_EVENTS
def build_custom_key(action, object)
"#{action}_#{object.class.model_name.name.underscore}".to_sym
end
end
This diff is collapsed.
...@@ -209,10 +209,12 @@ class TodoService ...@@ -209,10 +209,12 @@ class TodoService
def update_todos_state_by_ids(ids, current_user, state) def update_todos_state_by_ids(ids, current_user, state)
todos = current_user.todos.where(id: ids) todos = current_user.todos.where(id: ids)
# Only return those that are not really on that state # Only update those that are not really on that state
marked_todos = todos.where.not(state: state).update_all(state: state) todos = todos.where.not(state: state)
todos_ids = todos.pluck(:id)
todos.update_all(state: state)
current_user.update_todos_count_cache current_user.update_todos_count_cache
marked_todos todos_ids
end end
def create_todos(users, attributes) def create_todos(users, attributes)
......
...@@ -93,9 +93,7 @@ module Users ...@@ -93,9 +93,7 @@ module Users
end end
def current_authorizations_per_project def current_authorizations_per_project
current_authorizations.each_with_object({}) do |row, hash| current_authorizations.index_by(&:project_id)
hash[row.project_id] = row
end
end end
def current_authorizations def current_authorizations
......
...@@ -36,14 +36,14 @@ ...@@ -36,14 +36,14 @@
- if todo.pending? - if todo.pending?
.todo-actions .todo-actions
= link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-todo' do = link_to dashboard_todo_path(todo), method: :delete, class: 'btn btn-loading js-done-todo', data: { href: dashboard_todo_path(todo) } do
Done Done
= icon('spinner spin') = icon('spinner spin')
= link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' do = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden', data: { href: restore_dashboard_todo_path(todo) } do
Undo Undo
= icon('spinner spin') = icon('spinner spin')
- else - else
.todo-actions .todo-actions
= link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo' do = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do
Add todo Add todo
= icon('spinner spin') = icon('spinner spin')
...@@ -19,9 +19,12 @@ ...@@ -19,9 +19,12 @@
.nav-controls .nav-controls
- if @todos.any?(&:pending?) - if @todos.any?(&:pending?)
= link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
Mark all as done Mark all as done
= icon('spinner spin') = icon('spinner spin')
= link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
Undo mark all as done
= icon('spinner spin')
.todos-filters .todos-filters
.row-content-block.second-block .row-content-block.second-block
...@@ -67,12 +70,16 @@ ...@@ -67,12 +70,16 @@
.js-todos-all .js-todos-all
- if @todos.any? - if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } .js-todos-list-container
.panel.panel-default.panel-small.panel-without-border .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
%ul.content-list.todos-list .panel.panel-default.panel-small.panel-without-border
= render @todos %ul.content-list.todos-list
= paginate @todos, theme: "gitlab" = render @todos
= paginate @todos, theme: "gitlab"
.js-nothing-here-container.todos-all-done.hidden
= render "shared/empty_states/icons/todos_all_done.svg"
%h4.text-center
You're all done!
- elsif current_user.todos.any? - elsif current_user.todos.any?
.todos-all-done .todos-all-done
= render "shared/empty_states/icons/todos_all_done.svg" = render "shared/empty_states/icons/todos_all_done.svg"
......
...@@ -11,5 +11,3 @@ ...@@ -11,5 +11,3 @@
= render 'groups' = render 'groups'
- else - else
.nothing-here-block No public groups .nothing-here-block No public groups
= paginate @groups, theme: "gitlab"
- page_title "Projects"
= render "groups/settings_head" = render "groups/settings_head"
.panel.panel-default.prepend-top-default .panel.panel-default.prepend-top-default
......
...@@ -34,11 +34,6 @@ ...@@ -34,11 +34,6 @@
.clearfix .clearfix
= form_for @user, url: profile_notifications_path, method: :put do |f|
%label{ for: 'user_notified_of_own_activity' }
= f.check_box :notified_of_own_activity
%span Receive notifications about your own activity
%hr %hr
%h5 %h5
Groups (#{@group_notifications.count}) Groups (#{@group_notifications.count})
......
...@@ -13,3 +13,7 @@ ...@@ -13,3 +13,7 @@
= form.label :only_allow_merge_if_all_discussions_are_resolved do = form.label :only_allow_merge_if_all_discussions_are_resolved do
= form.check_box :only_allow_merge_if_all_discussions_are_resolved = form.check_box :only_allow_merge_if_all_discussions_are_resolved
%strong Only allow merge requests to be merged if all discussions are resolved %strong Only allow merge requests to be merged if all discussions are resolved
.checkbox
= form.label :printing_merge_request_link_enabled do
= form.check_box :printing_merge_request_link_enabled
%strong Show link to create/view merge request when pushing from the command line
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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