Commit 62127dc9 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into artifacts-from-ref-and-build-name

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