Commit 839279f0 authored by Rémy Coutable's avatar Rémy Coutable

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

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents a0b78db4 6619772f
...@@ -4,3 +4,4 @@ lib/gitlab/diff/position_tracer.rb ...@@ -4,3 +4,4 @@ lib/gitlab/diff/position_tracer.rb
app/controllers/projects/approver_groups_controller.rb app/controllers/projects/approver_groups_controller.rb
app/controllers/projects/approvers_controller.rb app/controllers/projects/approvers_controller.rb
app/policies/project_policy.rb app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb
...@@ -7,9 +7,12 @@ cache: ...@@ -7,9 +7,12 @@ cache:
variables: variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1" MYSQL_ALLOW_EMPTY_PASSWORD: "1"
<<<<<<< HEAD
# retry tests only in CI environment # retry tests only in CI environment
RSPEC_RETRY_RETRY_COUNT: "3" RSPEC_RETRY_RETRY_COUNT: "3"
ELASTIC_URL: "http://elasticsearch:9200" ELASTIC_URL: "http://elasticsearch:9200"
=======
>>>>>>> ce/master
RAILS_ENV: "test" RAILS_ENV: "test"
SIMPLECOV: "true" SIMPLECOV: "true"
SETUP_DB: "true" SETUP_DB: "true"
...@@ -74,9 +77,11 @@ stages: ...@@ -74,9 +77,11 @@ stages:
- knapsack rspec "--color --format documentation" - knapsack rspec "--color --format documentation"
artifacts: artifacts:
expire_in: 31d expire_in: 31d
when: always
paths: paths:
- knapsack/
- coverage/ - coverage/
- knapsack/
- tmp/capybara/
.spinach-knapsack: &spinach-knapsack .spinach-knapsack: &spinach-knapsack
stage: test stage: test
...@@ -92,9 +97,11 @@ stages: ...@@ -92,9 +97,11 @@ stages:
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts: artifacts:
expire_in: 31d expire_in: 31d
when: always
paths: paths:
- knapsack/
- coverage/ - coverage/
- knapsack/
- tmp/capybara/
# Prepare and merge knapsack tests # Prepare and merge knapsack tests
...@@ -207,6 +214,14 @@ rake db:migrate:reset: ...@@ -207,6 +214,14 @@ rake db:migrate:reset:
script: script:
- bundle exec rake db:migrate:reset - bundle exec rake db:migrate:reset
rake db:rollback:
stage: test
<<: *use-db
<<: *dedicated-runner
script:
- bundle exec rake db:rollback STEP=120
- bundle exec rake db:migrate
rake db:seed_fu: rake db:seed_fu:
stage: test stage: test
<<: *use-db <<: *use-db
......
...@@ -399,6 +399,12 @@ There are a few rules to get your merge request accepted: ...@@ -399,6 +399,12 @@ There are a few rules to get your merge request accepted:
1. Contains functionality we think other users will benefit from too 1. Contains functionality we think other users will benefit from too
1. Doesn't add configuration options or settings options since they complicate 1. Doesn't add configuration options or settings options since they complicate
making and testing future changes making and testing future changes
1. Changes do not adversely degrade performance.
- Avoid repeated polling of endpoints that require a significant amount of overhead
- Check for N+1 queries via the SQL log or [`QueryRecorder`](https://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
- Avoid repeated access of filesystem
1. If you need polling to support real-time features, please use
[polling with ETag caching][polling-etag].
1. Changes after submitting the merge request should be in separate commits 1. Changes after submitting the merge request should be in separate commits
(no squashing). If necessary, you will be asked to squash when the review is (no squashing). If necessary, you will be asked to squash when the review is
over, before merging. over, before merging.
...@@ -434,6 +440,7 @@ the feature you contribute through all of these steps. ...@@ -434,6 +440,7 @@ the feature you contribute through all of these steps.
1. Description explaining the relevancy (see following item) 1. Description explaining the relevancy (see following item)
1. Working and clean code that is commented where needed 1. Working and clean code that is commented where needed
1. Unit and integration tests that pass on the CI server 1. Unit and integration tests that pass on the CI server
1. Performance/scalability implications have been considered, addressed, and tested
1. [Documented][doc-styleguide] in the /doc directory 1. [Documented][doc-styleguide] in the /doc directory
1. Changelog entry added 1. Changelog entry added
1. Reviewed and any concerns are addressed 1. Reviewed and any concerns are addressed
...@@ -540,6 +547,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -540,6 +547,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/ [UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md [license-finder-doc]: doc/development/licensing.md
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[^1]: Specs other than JavaScript specs are considered backend code. Haml [^1]: Specs other than JavaScript specs are considered backend code. Haml
changes are considered backend code if they include Ruby code other than just changes are considered backend code if they include Ruby code other than just
......
...@@ -337,7 +337,7 @@ GEM ...@@ -337,7 +337,7 @@ GEM
multi_json (~> 1.10) multi_json (~> 1.10)
retriable (~> 1.4) retriable (~> 1.4)
signet (~> 0.6) signet (~> 0.6)
google-protobuf (3.2.0) google-protobuf (3.2.0.1)
googleauth (0.5.1) googleauth (0.5.1)
faraday (~> 0.9) faraday (~> 0.9)
jwt (~> 1.4) jwt (~> 1.4)
......
...@@ -10,7 +10,10 @@ var Api = { ...@@ -10,7 +10,10 @@ var Api = {
licensePath: "/api/:version/templates/licenses/:key", licensePath: "/api/:version/templates/licenses/:key",
gitignorePath: "/api/:version/templates/gitignores/:key", gitignorePath: "/api/:version/templates/gitignores/:key",
gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key", gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
<<<<<<< HEAD
ldapGroupsPath: "/api/:version/ldap/:provider/groups.json", ldapGroupsPath: "/api/:version/ldap/:provider/groups.json",
=======
>>>>>>> ce/master
dockerfilePath: "/api/:version/templates/dockerfiles/:key", dockerfilePath: "/api/:version/templates/dockerfiles/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
group: function(group_id, callback) { group: function(group_id, callback) {
...@@ -143,6 +146,7 @@ var Api = { ...@@ -143,6 +146,7 @@ var Api = {
url = gon.relative_url_root + url; url = gon.relative_url_root + url;
} }
return url.replace(':version', gon.api_version); return url.replace(':version', gon.api_version);
<<<<<<< HEAD
}, },
ldap_groups: function(query, provider, callback) { ldap_groups: function(query, provider, callback) {
var url; var url;
...@@ -160,6 +164,8 @@ var Api = { ...@@ -160,6 +164,8 @@ var Api = {
}).done(function(groups) { }).done(function(groups) {
return callback(groups); return callback(groups);
}); });
=======
>>>>>>> ce/master
} }
}; };
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form // "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
// is submitted. // is submitted.
// //
require('../extensions/jquery'); import '../commons/bootstrap';
// //
// ### Example Markup // ### Example Markup
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
// When called on a form with input fields with the `required` attribute, the // When called on a form with input fields with the `required` attribute, the
// form's submit button will be disabled until all required fields have values. // form's submit button will be disabled until all required fields have values.
// //
require('../extensions/jquery'); import '../commons/bootstrap';
// //
// ### Example Markup // ### Example Markup
......
...@@ -21,8 +21,13 @@ ...@@ -21,8 +21,13 @@
// %a.js-toggle-button // %a.js-toggle-button
// %div.js-toggle-content // %div.js-toggle-content
// //
$('body').on('click', '.js-toggle-button', function() { $('body').on('click', '.js-toggle-button', function(e) {
toggleContainer($(this).closest('.js-toggle-container')); toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.target.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button') {
e.preventDefault();
}
}); });
// If we're accessing a permalink, ensure it is not inside a // If we're accessing a permalink, ensure it is not inside a
......
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
this.removeFile(file); this.removeFile(file);
}); });
return this.on('sending', function(file, xhr, formData) { return this.on('sending', function(file, xhr, formData) {
formData.append('target_branch', form.find('.js-target-branch').val()); formData.append('target_branch', form.find('input[name="target_branch"]').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val()); formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val()); formData.append('commit_message', form.find('.js-commit-message').val());
}); });
......
const lineNumberRe = /^L[0-9]+/;
const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
const hash = gl.utils.getLocationHash();
if (hash && lineNumberRe.test(hash)) {
const hashUrlString = `#${hash}`;
[].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => {
const baseHref = permalinkButton.getAttribute('data-original-href') || (() => {
const href = permalinkButton.getAttribute('href');
permalinkButton.setAttribute('data-original-href', href);
return href;
})();
permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);
});
}
};
function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, elementsToUpdate) {
const updateBlameAndBlobPermalinkCb = () => {
// Wait for the hash to update from the LineHighlighter callback
setTimeout(() => {
updateLineNumbersOnBlobPermalinks(elementsToUpdate);
}, 0);
};
blobContentHolder.addEventListener('click', (e) => {
if (e.target.matches(lineNumberSelector)) {
updateBlameAndBlobPermalinkCb();
}
});
updateBlameAndBlobPermalinkCb();
}
export default BlobLinePermalinkUpdater;
class CreateBranchDropdown {
constructor(el, targetBranchDropdown) {
this.targetBranchDropdown = targetBranchDropdown;
this.el = el;
this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back');
this.cancelButton = this.el.querySelector('.js-cancel-branch-btn');
this.newBranchField = this.el.querySelector('#new_branch_name');
this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn');
this.newBranchCreateButton.setAttribute('disabled', '');
this.addBindings();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('beforeunload', this.cleanupWrapper);
}
cleanup() {
this.cleanBindings();
document.removeEventListener('beforeunload', this.cleanupWrapper);
}
cleanBindings() {
this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper);
this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper);
this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper);
this.dropdownBack.removeEventListener('click', this.resetFormWrapper);
this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper);
this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper);
}
addBindings() {
this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this);
this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this);
this.resetFormWrapper = this.resetForm.bind(this);
this.handleCancelClickWrapper = this.handleCancelClick.bind(this);
this.createBranchWrapper = this.createBranch.bind(this);
this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper);
this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper);
this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper);
this.dropdownBack.addEventListener('click', this.resetFormWrapper);
this.cancelButton.addEventListener('click', this.handleCancelClickWrapper);
this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper);
}
handleCancelClick(e) {
e.preventDefault();
e.stopPropagation();
this.resetForm();
this.dropdownBack.click();
}
handleNewBranchKeydown(e) {
const keyCode = e.which;
const ENTER_KEYCODE = 13;
if (keyCode === ENTER_KEYCODE) {
this.createBranch(e);
}
}
enableBranchCreateButton() {
if (this.newBranchField.value !== '') {
this.newBranchCreateButton.removeAttribute('disabled');
} else {
this.newBranchCreateButton.setAttribute('disabled', '');
}
}
resetForm() {
this.newBranchField.value = '';
this.enableBranchCreateButtonWrapper();
}
createBranch(e) {
e.preventDefault();
if (this.newBranchCreateButton.getAttribute('disabled') === '') {
return;
}
const newBranchName = this.newBranchField.value;
this.targetBranchDropdown.setNewBranch(newBranchName);
this.resetForm();
}
}
window.gl = window.gl || {};
gl.CreateBranchDropdown = CreateBranchDropdown;
/* eslint-disable class-methods-use-this */
const SELECT_ITEM_MSG = 'Select';
class TargetBranchDropDown {
constructor(dropdown) {
this.dropdown = dropdown;
this.$dropdown = $(dropdown);
this.fieldName = this.dropdown.getAttribute('data-field-name');
this.form = this.dropdown.closest('form');
this.createDropdown();
}
static bootstrap() {
const dropdowns = document.querySelectorAll('.js-project-branches-dropdown');
[].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown));
}
createDropdown() {
const self = this;
this.$dropdown.glDropdown({
selectable: true,
filterable: true,
search: {
fields: ['title'],
},
data: (term, callback) => $.ajax({
url: self.dropdown.getAttribute('data-refs-url'),
data: {
ref: self.dropdown.getAttribute('data-ref'),
show_all: true,
},
dataType: 'json',
}).done(refs => callback(self.dropdownData(refs))),
toggleLabel(item, el) {
if (el.is('.is-active')) {
return item.text;
}
return SELECT_ITEM_MSG;
},
clicked(item, el, e) {
e.preventDefault();
self.onClick.call(self);
},
fieldName: self.fieldName,
});
return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this);
}
onClick() {
this.enableSubmit();
this.$dropdown.trigger('change.branch');
}
enableSubmit() {
const submitBtn = this.form.querySelector('[type="submit"]');
if (this.branchInput && this.branchInput.value) {
submitBtn.removeAttribute('disabled');
} else {
submitBtn.setAttribute('disabled', '');
}
}
dropdownData(refs) {
const branchList = this.dropdownItems(refs);
this.cachedRefs = refs;
this.addDefaultBranch(branchList);
this.addNewBranch(branchList);
return { Branches: branchList };
}
dropdownItems(refs) {
return refs.map(this.dropdownItem);
}
dropdownItem(ref) {
return { id: ref, text: ref, title: ref };
}
addDefaultBranch(branchList) {
// when no branch is selected do nothing
if (!this.branchInput) {
return;
}
const branchInputVal = this.branchInput.value;
const currentBranchIndex = this.searchBranch(branchList, branchInputVal);
if (currentBranchIndex === -1) {
this.unshiftBranch(branchList, this.dropdownItem(branchInputVal));
}
}
addNewBranch(branchList) {
if (this.newBranch) {
this.unshiftBranch(branchList, this.newBranch);
}
}
searchBranch(branchList, branchName) {
return _.findIndex(branchList, el => branchName === el.id);
}
unshiftBranch(branchList, branch) {
const branchIndex = this.searchBranch(branchList, branch.id);
if (branchIndex === -1) {
branchList.unshift(branch);
}
}
setNewBranch(newBranchName) {
this.newBranch = this.dropdownItem(newBranchName);
this.refreshData();
this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName));
}
refreshData() {
this.glDropdown.fullData = this.dropdownData(this.cachedRefs);
this.clearFilter();
}
clearFilter() {
// apply an empty filter in order to refresh the data
this.glDropdown.filter.filter('');
this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = '';
}
selectBranch(index) {
const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index];
if (!branch.classList.contains('is-active')) {
branch.click();
} else {
this.closeDropdown();
}
}
closeDropdown() {
this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click();
}
get branchInput() {
return this.form.querySelector(`input[name="${this.fieldName}"]`);
}
get glDropdown() {
return this.$dropdown.data('glDropdown');
}
}
window.gl = window.gl || {};
gl.TargetBranchDropDown = TargetBranchDropDown;
...@@ -75,7 +75,11 @@ $(() => { ...@@ -75,7 +75,11 @@ $(() => {
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
<<<<<<< HEAD
this.filterManager = new FilteredSearchBoards(Store.filter, true, [(this.milestoneTitle ? 'milestone' : null)]); this.filterManager = new FilteredSearchBoards(Store.filter, true, [(this.milestoneTitle ? 'milestone' : null)]);
=======
this.filterManager = new FilteredSearchBoards(Store.filter, true);
>>>>>>> ce/master
// Listen for updateTokens event // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('updateTokens', this.updateTokens);
......
export default class FilteredSearchBoards extends gl.FilteredSearchManager { export default class FilteredSearchBoards extends gl.FilteredSearchManager {
<<<<<<< HEAD
constructor(store, updateUrl = false, cantEdit = []) { constructor(store, updateUrl = false, cantEdit = []) {
=======
constructor(store, updateUrl = false) {
>>>>>>> ce/master
super('boards'); super('boards');
this.store = store; this.store = store;
...@@ -8,7 +12,10 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -8,7 +12,10 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async // Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests // instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true; this.isHandledAsync = true;
<<<<<<< HEAD
this.cantEdit = cantEdit; this.cantEdit = cantEdit;
=======
>>>>>>> ce/master
} }
updateObject(path) { updateObject(path) {
...@@ -32,10 +39,13 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -32,10 +39,13 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Get the placeholder back if search is empty // Get the placeholder back if search is empty
this.filteredSearchInput.dispatchEvent(new Event('input')); this.filteredSearchInput.dispatchEvent(new Event('input'));
} }
<<<<<<< HEAD
canEdit(token) { canEdit(token) {
const tokenName = token.querySelector('.name').textContent.trim(); const tokenName = token.querySelector('.name').textContent.trim();
return this.cantEdit.indexOf(tokenName) === -1; return this.cantEdit.indexOf(tokenName) === -1;
} }
=======
>>>>>>> ce/master
} }
...@@ -22,11 +22,14 @@ ...@@ -22,11 +22,14 @@
create () { create () {
this.state.lists = []; this.state.lists = [];
this.filter.path = gl.utils.getUrlParamsArray().join('&'); this.filter.path = gl.utils.getUrlParamsArray().join('&');
<<<<<<< HEAD
}, },
createNewListDropdownData() { createNewListDropdownData() {
this.state.currentBoard = {}; this.state.currentBoard = {};
this.state.currentPage = ''; this.state.currentPage = '';
this.state.reload = false; this.state.reload = false;
=======
>>>>>>> ce/master
}, },
addList (listObj) { addList (listObj) {
const list = new List(listObj); const list = new List(listObj);
...@@ -124,12 +127,17 @@ ...@@ -124,12 +127,17 @@
return list[key] === val && byType; return list[key] === val && byType;
})[0]; })[0];
}, },
<<<<<<< HEAD
updateFiltersUrl (replaceState = false) { updateFiltersUrl (replaceState = false) {
if (replaceState) { if (replaceState) {
history.replaceState(null, null, `?${this.filter.path}`); history.replaceState(null, null, `?${this.filter.path}`);
} else { } else {
history.pushState(null, null, `?${this.filter.path}`); history.pushState(null, null, `?${this.filter.path}`);
} }
=======
updateFiltersUrl () {
history.pushState(null, null, `?${this.filter.path}`);
>>>>>>> ce/master
} }
}; };
})(); })();
import 'jquery'; import $ from 'jquery';
// bootstrap jQuery plugins // bootstrap jQuery plugins
import 'bootstrap-sass/assets/javascripts/bootstrap/affix'; import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
...@@ -8,3 +8,9 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; ...@@ -8,3 +8,9 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
// custom jQuery functions
$.fn.extend({
disable() { return $(this).attr('disabled', 'disabled').addClass('disabled'); },
enable() { return $(this).removeAttr('disabled').removeClass('disabled'); },
});
import './polyfills';
import './jquery'; import './jquery';
import './bootstrap'; import './bootstrap';
// ECMAScript polyfills
import 'core-js/fn/array/find';
import 'core-js/fn/object/assign';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
// Browser polyfills
import './polyfills/custom_event';
import './polyfills/element';
if (typeof window.CustomEvent !== 'function') {
window.CustomEvent = function CustomEvent(event, params) {
const evt = document.createEvent('CustomEvent');
const evtParams = params || { bubbles: false, cancelable: false, detail: undefined };
evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail);
return evt;
};
window.CustomEvent.prototype = Event;
}
/* global Element */ Element.prototype.closest = Element.prototype.closest ||
/* eslint-disable consistent-return, max-len, no-empty, func-names */ function closest(selector, selectedElement = this) {
if (!selectedElement) return null;
Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) { return selectedElement.matches(selector) ?
if (!selectedElement) return; selectedElement :
return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); Element.prototype.closest(selector, selectedElement.parentElement);
}; };
Element.prototype.matches = Element.prototype.matches || Element.prototype.matches = Element.prototype.matches ||
Element.prototype.matchesSelector || Element.prototype.matchesSelector ||
...@@ -12,9 +12,9 @@ Element.prototype.matches = Element.prototype.matches || ...@@ -12,9 +12,9 @@ Element.prototype.matches = Element.prototype.matches ||
Element.prototype.msMatchesSelector || Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector || Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector || Element.prototype.webkitMatchesSelector ||
function (s) { function matches(selector) {
const matches = (this.document || this.ownerDocument).querySelectorAll(s); const elms = (this.document || this.ownerDocument).querySelectorAll(selector);
let i = matches.length - 1; let i = elms.length - 1;
while (i >= 0 && matches.item(i) !== this) { i -= 1; } while (i >= 0 && elms.item(i) !== this) { i -= 1; }
return i > -1; return i > -1;
}; };
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
<<<<<<< HEAD
window.ConfirmDangerModal = (function() { window.ConfirmDangerModal = (function() {
function ConfirmDangerModal(form, text, arg) { function ConfirmDangerModal(form, text, arg) {
...@@ -30,5 +31,34 @@ window.ConfirmDangerModal = (function() { ...@@ -30,5 +31,34 @@ window.ConfirmDangerModal = (function() {
})(this)); })(this));
} }
=======
window.ConfirmDangerModal = (function() {
function ConfirmDangerModal(form, text) {
var project_path, submit;
this.form = form;
$('.js-confirm-text').text(text || '');
$('.js-confirm-danger-input').val('');
$('#modal-confirm-danger').modal('show');
project_path = $('.js-confirm-danger-match').text();
submit = $('.js-confirm-danger-submit');
submit.disable();
$('.js-confirm-danger-input').off('input');
$('.js-confirm-danger-input').on('input', function() {
if (gl.utils.rstrip($(this).val()) === project_path) {
return submit.enable();
} else {
return submit.disable();
}
});
$('.js-confirm-danger-submit').off('click');
$('.js-confirm-danger-submit').on('click', (function(_this) {
return function() {
return _this.form.submit();
};
})(this));
}
>>>>>>> ce/master
return ConfirmDangerModal; return ConfirmDangerModal;
})(); })();
/* global Vue */
/* global CommentsStore */
(() => {
const NewIssueForDiscussion = Vue.extend({
props: {
discussionId: {
type: String,
required: true,
},
},
data() {
return {
discussions: CommentsStore.state,
};
},
computed: {
discussion() {
return this.discussions[this.discussionId];
},
showButton() {
if (this.discussion) return !this.discussion.isResolved();
return false;
},
},
});
Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
})();
...@@ -15,10 +15,11 @@ require('./components/resolve_btn'); ...@@ -15,10 +15,11 @@ require('./components/resolve_btn');
require('./components/resolve_count'); require('./components/resolve_count');
require('./components/resolve_discussion_btn'); require('./components/resolve_discussion_btn');
require('./components/diff_note_avatars'); require('./components/diff_note_avatars');
require('./components/new_issue_for_discussion');
$(() => { $(() => {
const projectPath = document.querySelector('.merge-request').dataset.projectPath; const projectPath = document.querySelector('.merge-request').dataset.projectPath;
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn'; const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.diffNoteApps = {}; window.gl.diffNoteApps = {};
......
...@@ -39,9 +39,11 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make ...@@ -39,9 +39,11 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make
import Issue from './issue'; import Issue from './issue';
import BindInOut from './behaviors/bind_in_out'; import BindInOut from './behaviors/bind_in_out';
import GroupName from './group_name';
import GroupsList from './groups_list'; import GroupsList from './groups_list';
import ProjectsList from './projects_list'; import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout'); const UserCallout = require('./user_callout');
...@@ -61,13 +63,32 @@ const UserCallout = require('./user_callout'); ...@@ -61,13 +63,32 @@ const UserCallout = require('./user_callout');
} }
Dispatcher.prototype.initPageScripts = function() { Dispatcher.prototype.initPageScripts = function() {
var page, path, shortcut_handler; var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
page = $('body').attr('data-page'); page = $('body').attr('data-page');
if (!page) { if (!page) {
return false; return false;
} }
path = page.split(':'); path = page.split(':');
shortcut_handler = null; shortcut_handler = null;
function initBlob() {
new LineHighlighter();
new BlobLinePermalinkUpdater(
document.querySelector('#blob-content-holder'),
'.diff-line-num[data-line-number]',
document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
);
shortcut_handler = new ShortcutsNavigation();
fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
new ShortcutsBlob({
skipResetBindings: true,
fileBlobPermalinkUrl,
});
}
switch (page) { switch (page) {
case 'sessions:new': case 'sessions:new':
new UsernameValidator(); new UsernameValidator();
...@@ -248,20 +269,26 @@ const UserCallout = require('./user_callout'); ...@@ -248,20 +269,26 @@ const UserCallout = require('./user_callout');
case 'projects:tree:show': case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new TreeView(); new TreeView();
gl.TargetBranchDropDown.bootstrap();
break; break;
case 'projects:find_file:show': case 'projects:find_file:show':
shortcut_handler = true; shortcut_handler = true;
break; break;
case 'projects:blob:new':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:create':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:show': case 'projects:blob:show':
gl.TargetBranchDropDown.bootstrap();
initBlob();
break;
case 'projects:blob:edit':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blame:show': case 'projects:blame:show':
new LineHighlighter(); initBlob();
shortcut_handler = new ShortcutsNavigation();
const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
new ShortcutsBlob({
skipResetBindings: true,
fileBlobPermalinkUrl,
});
break; break;
case 'groups:labels:new': case 'groups:labels:new':
case 'groups:labels:edit': case 'groups:labels:edit':
...@@ -355,6 +382,9 @@ const UserCallout = require('./user_callout'); ...@@ -355,6 +382,9 @@ const UserCallout = require('./user_callout');
shortcut_handler = new ShortcutsDashboardNavigation(); shortcut_handler = new ShortcutsDashboardNavigation();
new UserCallout(); new UserCallout();
break; break;
case 'groups':
new GroupName();
break;
case 'profiles': case 'profiles':
new NotificationsForm(); new NotificationsForm();
new NotificationsDropdown(); new NotificationsDropdown();
...@@ -362,6 +392,7 @@ const UserCallout = require('./user_callout'); ...@@ -362,6 +392,7 @@ const UserCallout = require('./user_callout');
case 'projects': case 'projects':
new Project(); new Project();
new ProjectAvatar(); new ProjectAvatar();
new GroupName();
switch (path[1]) { switch (path[1]) {
case 'compare': case 'compare':
new CompareAutocomplete(); new CompareAutocomplete();
......
...@@ -74,6 +74,9 @@ require('../window')(function(w){ ...@@ -74,6 +74,9 @@ require('../window')(function(w){
this._loadUrlData(config.endpoint) this._loadUrlData(config.endpoint)
.then(function(d) { .then(function(d) {
self._loadData(d, config, self); self._loadData(d, config, self);
}, function(xhrError) {
// TODO: properly handle errors due to XHR cancellation
return;
}).catch(function(e) { }).catch(function(e) {
throw new droplabAjaxException(e.message || e); throw new droplabAjaxException(e.message || e);
}); });
......
...@@ -82,6 +82,9 @@ require('../window')(function(w){ ...@@ -82,6 +82,9 @@ require('../window')(function(w){
this._loadUrlData(url) this._loadUrlData(url)
.then(function(data) { .then(function(data) {
self._loadData(data, config, self); self._loadData(data, config, self);
}, function(xhrError) {
// TODO: properly handle errors due to XHR cancellation
return;
}); });
} }
}, },
......
/* eslint-disable no-param-reassign, no-new */ /* eslint-disable no-param-reassign, no-new */
/* global Flash */ /* global Flash */
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table';
import EnvironmentsStore from '../stores/environments_store';
import eventHub from '../event_hub';
const Vue = window.Vue = require('vue'); const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource')); window.Vue.use(require('vue-resource'));
<<<<<<< HEAD
const EnvironmentsService = require('~/environments/services/environments_service'); const EnvironmentsService = require('~/environments/services/environments_service');
const EnvironmentTable = require('./environments_table'); const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('~/environments/stores/environments_store'); const EnvironmentsStore = require('~/environments/stores/environments_store');
require('~/vue_shared/components/table_pagination'); require('~/vue_shared/components/table_pagination');
require('~/lib/utils/common_utils'); require('~/lib/utils/common_utils');
require('~/vue_shared/vue_resource_interceptor'); require('~/vue_shared/vue_resource_interceptor');
=======
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
>>>>>>> ce/master
module.exports = Vue.component('environment-component', { export default Vue.component('environment-component', {
components: { components: {
'environment-table': EnvironmentTable, 'environment-table': EnvironmentTable,
...@@ -77,6 +87,7 @@ module.exports = Vue.component('environment-component', { ...@@ -77,6 +87,7 @@ module.exports = Vue.component('environment-component', {
* Toggles loading property. * Toggles loading property.
*/ */
created() { created() {
<<<<<<< HEAD
const scope = gl.utils.getParameterByName('scope') || this.visibility; const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
...@@ -104,6 +115,17 @@ module.exports = Vue.component('environment-component', { ...@@ -104,6 +115,17 @@ module.exports = Vue.component('environment-component', {
this.isLoading = false; this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert'); new Flash('An error occurred while fetching the environments.', 'alert');
}); });
=======
this.service = new EnvironmentsService(this.endpoint);
this.fetchEnvironments();
eventHub.$on('refreshEnvironments', this.fetchEnvironments);
},
beforeDestroyed() {
eventHub.$off('refreshEnvironments');
>>>>>>> ce/master
}, },
methods: { methods: {
...@@ -130,6 +152,32 @@ module.exports = Vue.component('environment-component', { ...@@ -130,6 +152,32 @@ module.exports = Vue.component('environment-component', {
gl.utils.visitUrl(param); gl.utils.visitUrl(param);
return param; return param;
}, },
fetchEnvironments() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
this.isLoading = true;
return this.service.get(scope, pageNumber)
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.');
});
},
}, },
template: ` template: `
...@@ -162,7 +210,7 @@ module.exports = Vue.component('environment-component', { ...@@ -162,7 +210,7 @@ module.exports = Vue.component('environment-component', {
<div class="content-list environments-container"> <div class="content-list environments-container">
<div class="environments-list-loading text-center" v-if="isLoading"> <div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i> <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div> </div>
<div class="blank-state blank-state-no-icon" <div class="blank-state blank-state-no-icon"
...@@ -192,8 +240,11 @@ module.exports = Vue.component('environment-component', { ...@@ -192,8 +240,11 @@ module.exports = Vue.component('environment-component', {
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
<<<<<<< HEAD
:toggleDeployBoard="toggleDeployBoard" :toggleDeployBoard="toggleDeployBoard"
:store="store" :store="store"
=======
>>>>>>> ce/master
:service="service"/> :service="service"/>
</div> </div>
......
const Vue = require('vue'); /* global Flash */
const playIconSvg = require('icons/_icon_play.svg'); /* eslint-disable no-new */
module.exports = Vue.component('actions-component', { import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
export default {
props: { props: {
actions: { actions: {
type: Array, type: Array,
required: false, required: false,
default: () => [], default: () => [],
}, },
service: {
type: Object,
required: true,
},
}, },
data() { data() {
return { playIconSvg }; return {
playIconSvg,
isLoading: false,
};
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
}, },
template: ` template: `
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button class="dropdown btn btn-default dropdown-new" data-toggle="dropdown"> <button
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
data-toggle="dropdown"
:disabled="isLoading">
<span> <span>
<span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span> <span v-html="playIconSvg"></span>
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</span> </span>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions"> <li v-for="action in actions">
<a :href="action.play_path" <button
data-method="post" @click="onClickAction(action.play_path)"
rel="nofollow" class="js-manual-action-link no-btn">
class="js-manual-action-link">
${playIconSvg} ${playIconSvg}
<span> <span>
{{action.name}} {{action.name}}
</span> </span>
</a> </button>
</li> </li>
</ul> </ul>
</button> </button>
</div> </div>
`, `,
}); };
/** /**
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
const Vue = require('vue'); export default {
module.exports = Vue.component('external-url-component', {
props: { props: {
externalUrl: { externalUrl: {
type: String, type: String,
...@@ -12,8 +10,12 @@ module.exports = Vue.component('external-url-component', { ...@@ -12,8 +10,12 @@ module.exports = Vue.component('external-url-component', {
}, },
template: ` template: `
<a class="btn external_url" :href="externalUrl" target="_blank"> <a
<i class="fa fa-external-link"></i> class="btn external_url"
:href="externalUrl"
target="_blank"
title="Environment external URL">
<i class="fa fa-external-link" aria-hidden="true"></i>
</a> </a>
`, `,
}); };
<<<<<<< HEAD
/** /**
* Environment Item Component * Environment Item Component
* *
...@@ -14,10 +15,20 @@ const ExternalUrlComponent = require('./environment_external_url'); ...@@ -14,10 +15,20 @@ const ExternalUrlComponent = require('./environment_external_url');
const StopComponent = require('./environment_stop'); const StopComponent = require('./environment_stop');
const RollbackComponent = require('./environment_rollback'); const RollbackComponent = require('./environment_rollback');
const TerminalButtonComponent = require('./environment_terminal_button'); const TerminalButtonComponent = require('./environment_terminal_button');
=======
import Timeago from 'timeago.js';
import ActionsComponent from './environment_actions';
import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button';
import '../../lib/utils/text_utility';
import '../../vue_shared/components/commit';
>>>>>>> ce/master
const timeagoInstance = new Timeago(); const timeagoInstance = new Timeago();
module.exports = Vue.component('environment-item', { export default {
components: { components: {
'commit-component': gl.CommitComponent, 'commit-component': gl.CommitComponent,
...@@ -47,9 +58,15 @@ module.exports = Vue.component('environment-item', { ...@@ -47,9 +58,15 @@ module.exports = Vue.component('environment-item', {
default: false, default: false,
}, },
<<<<<<< HEAD
toggleDeployBoard: { toggleDeployBoard: {
type: Function, type: Function,
required: false, required: false,
=======
service: {
type: Object,
required: true,
>>>>>>> ce/master
}, },
}, },
...@@ -509,22 +526,25 @@ module.exports = Vue.component('environment-item', { ...@@ -509,22 +526,25 @@ module.exports = Vue.component('environment-item', {
<td class="environments-actions"> <td class="environments-actions">
<div v-if="!model.isFolder" class="btn-group pull-right" role="group"> <div v-if="!model.isFolder" class="btn-group pull-right" role="group">
<actions-component v-if="hasManualActions && canCreateDeployment" <actions-component v-if="hasManualActions && canCreateDeployment"
:service="service"
:actions="manualActions"/> :actions="manualActions"/>
<external-url-component v-if="externalURL && canReadEnvironment" <external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/> :external-url="externalURL"/>
<stop-component v-if="hasStopAction && canCreateDeployment" <stop-component v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"/> :stop-url="model.stop_path"
:service="service"/>
<terminal-button-component v-if="model && model.terminal_path" <terminal-button-component v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/> :terminal-path="model.terminal_path"/>
<rollback-component v-if="canRetry && canCreateDeployment" <rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment" :is-last-deployment="isLastDeployment"
:retry-url="retryUrl"/> :retry-url="retryUrl"
:service="service"/>
</div> </div>
</td> </td>
</tr> </tr>
`, `,
}); };
/* global Flash */
/* eslint-disable no-new */
/** /**
* Renders Rollback or Re deploy button in environments table depending * Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment` * of the provided property `isLastDeployment`.
*
* Makes a post request when the button is clicked.
*/ */
const Vue = require('vue'); import eventHub from '../event_hub';
module.exports = Vue.component('rollback-component', { export default {
props: { props: {
retryUrl: { retryUrl: {
type: String, type: String,
...@@ -15,16 +19,49 @@ module.exports = Vue.component('rollback-component', { ...@@ -15,16 +19,49 @@ module.exports = Vue.component('rollback-component', {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
service: {
type: Object,
required: true,
},
},
data() {
return {
isLoading: false,
};
},
methods: {
onClick() {
this.isLoading = true;
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
}, },
template: ` template: `
<a class="btn" :href="retryUrl" data-method="post" rel="nofollow"> <button type="button"
class="btn"
@click="onClick"
:disabled="isLoading">
<span v-if="isLastDeployment"> <span v-if="isLastDeployment">
Re-deploy Re-deploy
</span> </span>
<span v-else> <span v-else>
Rollback Rollback
</span> </span>
</a>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
`, `,
}); };
/* global Flash */
/* eslint-disable no-new, no-alert */
/** /**
* Renders the stop "button" that allows stop an environment. * Renders the stop "button" that allows stop an environment.
* Used in environments table. * Used in environments table.
*/ */
const Vue = require('vue'); import eventHub from '../event_hub';
module.exports = Vue.component('stop-component', { export default {
props: { props: {
stopUrl: { stopUrl: {
type: String, type: String,
default: '', default: '',
}, },
service: {
type: Object,
required: true,
},
},
data() {
return {
isLoading: false,
};
},
methods: {
onClick() {
if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.', 'alert');
});
}
},
}, },
template: ` template: `
<a class="btn stop-env-link" <button type="button"
:href="stopUrl" class="btn stop-env-link"
data-confirm="Are you sure you want to stop this environment?" @click="onClick"
data-method="post" :disabled="isLoading"
rel="nofollow"> title="Stop Environment">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i> <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
</a> <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
`, `,
}); };
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
* Renders a terminal button to open a web terminal. * Renders a terminal button to open a web terminal.
* Used in environments table. * Used in environments table.
*/ */
const Vue = require('vue'); import terminalIconSvg from 'icons/_icon_terminal.svg';
const terminalIconSvg = require('icons/_icon_terminal.svg');
module.exports = Vue.component('terminal-button-component', { export default {
props: { props: {
terminalPath: { terminalPath: {
type: String, type: String,
required: false,
default: '', default: '',
}, },
}, },
...@@ -19,8 +19,9 @@ module.exports = Vue.component('terminal-button-component', { ...@@ -19,8 +19,9 @@ module.exports = Vue.component('terminal-button-component', {
template: ` template: `
<a class="btn terminal-button" <a class="btn terminal-button"
title="Open web terminal"
:href="terminalPath"> :href="terminalPath">
${terminalIconSvg} ${terminalIconSvg}
</a> </a>
`, `,
}); };
...@@ -4,12 +4,17 @@ ...@@ -4,12 +4,17 @@
* Dumb component used to render top level environments and * Dumb component used to render top level environments and
* the folder view. * the folder view.
*/ */
<<<<<<< HEAD
const Vue = require('vue'); const Vue = require('vue');
const EnvironmentItem = require('./environment_item'); const EnvironmentItem = require('./environment_item');
const DeployBoard = require('./deploy_board_component').default; const DeployBoard = require('./deploy_board_component').default;
module.exports = Vue.component('environment-table-component', { module.exports = Vue.component('environment-table-component', {
=======
import EnvironmentItem from './environment_item';
>>>>>>> ce/master
export default {
components: { components: {
EnvironmentItem, EnvironmentItem,
DeployBoard, DeployBoard,
...@@ -34,6 +39,7 @@ module.exports = Vue.component('environment-table-component', { ...@@ -34,6 +39,7 @@ module.exports = Vue.component('environment-table-component', {
default: false, default: false,
}, },
<<<<<<< HEAD
toggleDeployBoard: { toggleDeployBoard: {
type: Function, type: Function,
required: false, required: false,
...@@ -50,6 +56,11 @@ module.exports = Vue.component('environment-table-component', { ...@@ -50,6 +56,11 @@ module.exports = Vue.component('environment-table-component', {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), default: () => ({}),
=======
service: {
type: Object,
required: true,
>>>>>>> ce/master
}, },
}, },
...@@ -73,6 +84,7 @@ module.exports = Vue.component('environment-table-component', { ...@@ -73,6 +84,7 @@ module.exports = Vue.component('environment-table-component', {
:model="model" :model="model"
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
<<<<<<< HEAD
:toggleDeployBoard="toggleDeployBoard"></tr> :toggleDeployBoard="toggleDeployBoard"></tr>
<tr v-if="model.hasDeployBoard && model.isDeployBoardVisible" class="js-deploy-board-row"> <tr v-if="model.hasDeployBoard && model.isDeployBoardVisible" class="js-deploy-board-row">
...@@ -86,8 +98,11 @@ module.exports = Vue.component('environment-table-component', { ...@@ -86,8 +98,11 @@ module.exports = Vue.component('environment-table-component', {
</deploy-board> </deploy-board>
</td> </td>
</tr> </tr>
=======
:service="service"></tr>
>>>>>>> ce/master
</template> </template>
</tbody> </tbody>
</table> </table>
`, `,
}); };
const EnvironmentsComponent = require('./components/environment'); import EnvironmentsComponent from './components/environment';
$(() => { $(() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
......
import Vue from 'vue';
export default new Vue();
const EnvironmentsFolderComponent = require('./environments_folder_view'); import EnvironmentsFolderComponent from './environments_folder_view';
$(() => { $(() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
......
<<<<<<< HEAD
/* eslint-disable no-new */ /* eslint-disable no-new */
const Vue = window.Vue = require('vue'); const Vue = window.Vue = require('vue');
...@@ -9,8 +10,21 @@ const Flash = require('~/flash'); ...@@ -9,8 +10,21 @@ const Flash = require('~/flash');
require('~/vue_shared/components/table_pagination'); require('~/vue_shared/components/table_pagination');
require('~/lib/utils/common_utils'); require('~/lib/utils/common_utils');
require('~/vue_shared/vue_resource_interceptor'); require('~/vue_shared/vue_resource_interceptor');
=======
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table';
import EnvironmentsStore from '../stores/environments_store';
module.exports = Vue.component('environment-folder-view', { const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
>>>>>>> ce/master
export default Vue.component('environment-folder-view', {
components: { components: {
'environment-table': EnvironmentTable, 'environment-table': EnvironmentTable,
...@@ -181,15 +195,18 @@ module.exports = Vue.component('environment-folder-view', { ...@@ -181,15 +195,18 @@ module.exports = Vue.component('environment-folder-view', {
:play-icon-svg="playIconSvg" :play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg" :terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg" :commit-icon-svg="commitIconSvg"
<<<<<<< HEAD
:toggleDeployBoard="toggleDeployBoard" :toggleDeployBoard="toggleDeployBoard"
:store="store" :store="store"
:service="service"> :service="service">
</environment-table> </environment-table>
=======
:service="service"/>
>>>>>>> ce/master
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage" :change="changePage"
:pageInfo="state.paginationInformation"> :pageInfo="state.paginationInformation"/>
</table-pagination>
</div> </div>
</div> </div>
</div> </div>
......
<<<<<<< HEAD
/* eslint-disable class-methods-use-this*/ /* eslint-disable class-methods-use-this*/
const Vue = require('vue'); const Vue = require('vue');
=======
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
>>>>>>> ce/master
class EnvironmentsService { export default class EnvironmentsService {
constructor(endpoint) { constructor(endpoint) {
this.environments = Vue.resource(endpoint); this.environments = Vue.resource(endpoint);
} }
get() { get(scope, page) {
return this.environments.get(); return this.environments.get({ scope, page });
} }
<<<<<<< HEAD
getDeployBoard(endpoint) { getDeployBoard(endpoint) {
return Vue.http.get(endpoint); return Vue.http.get(endpoint);
} }
} }
=======
>>>>>>> ce/master
module.exports = EnvironmentsService; postAction(endpoint) {
return Vue.http.post(endpoint, {}, { emulateJSON: true });
}
}
require('~/lib/utils/common_utils'); import '~/lib/utils/common_utils';
/** /**
* Environments Store. * Environments Store.
* *
* Stores received environments, count of stopped environments and count of * Stores received environments, count of stopped environments and count of
* available environments. * available environments.
*/ */
class EnvironmentsStore { export default class EnvironmentsStore {
constructor() { constructor() {
this.state = {}; this.state = {};
this.state.environments = []; this.state.environments = [];
...@@ -156,5 +157,3 @@ class EnvironmentsStore { ...@@ -156,5 +157,3 @@ class EnvironmentsStore {
return this.state.environments; return this.state.environments;
} }
} }
module.exports = EnvironmentsStore;
/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */ // TODO: remove this
'use strict'; // eslint-disable-next-line no-extend-native
Array.prototype.first = function first() {
Array.prototype.first = function() {
return this[0]; return this[0];
}; };
Array.prototype.last = function() { // eslint-disable-next-line no-extend-native
return this[this.length-1]; Array.prototype.last = function last() {
}; return this[this.length - 1];
Array.prototype.find = Array.prototype.find || function(predicate, ...args) {
if (!this) throw new TypeError('Array.prototype.find called on null or undefined');
if (typeof predicate !== 'function') throw new TypeError('predicate must be a function');
const list = Object(this);
const thisArg = args[1];
let value = {};
for (let i = 0; i < list.length; i += 1) {
value = list[i];
if (predicate.call(thisArg, value, i, list)) return value;
}
return undefined;
}; };
/* global CustomEvent */
/* eslint-disable no-global-assign */
// Custom event support for IE
CustomEvent = function CustomEvent(event, parameters) {
const params = parameters || { bubbles: false, cancelable: false, detail: undefined };
const evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
};
CustomEvent.prototype = window.Event.prototype;
/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */
// Disable an element and add the 'disabled' Bootstrap class
(function() {
$.fn.extend({
disable: function() {
return $(this).attr('disabled', 'disabled').addClass('disabled');
}
});
// Enable an element and remove the 'disabled' Bootstrap class
$.fn.extend({
enable: function() {
return $(this).removeAttr('disabled').removeClass('disabled');
}
});
}).call(window);
/* eslint-disable no-restricted-syntax */
// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
if (typeof Object.assign !== 'function') {
Object.assign = function assign(target, ...args) {
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
const to = Object(target);
for (let index = 0; index < args.length; index += 1) {
const nextSource = args[index];
if (nextSource != null) { // Skip over if undefined or null
for (const nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
};
}
import 'string.prototype.codepointat';
import 'string.fromcodepoint';
...@@ -174,6 +174,10 @@ ...@@ -174,6 +174,10 @@
} }
resetDropdowns() { resetDropdowns() {
if (!this.currentDropdown) {
return;
}
// Force current dropdown to hide // Force current dropdown to hide
this.mapping[this.currentDropdown].reference.hideDropdown(); this.mapping[this.currentDropdown].reference.hideDropdown();
......
...@@ -42,7 +42,8 @@ ...@@ -42,7 +42,8 @@
this.editTokenWrapper = this.editToken.bind(this); this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this); this.tokenChange = this.tokenChange.bind(this);
this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInputForm = this.filteredSearchInput.form;
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper); this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
...@@ -60,7 +61,7 @@ ...@@ -60,7 +61,7 @@
} }
unbindEvents() { unbindEvents() {
this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper); this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
......
...@@ -274,6 +274,7 @@ GitLabDropdown = (function() { ...@@ -274,6 +274,7 @@ GitLabDropdown = (function() {
selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) { if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector; selector = ".dropdown-page-one " + selector;
<<<<<<< HEAD
} }
return $(selector); return $(selector);
}; };
...@@ -298,6 +299,32 @@ GitLabDropdown = (function() { ...@@ -298,6 +299,32 @@ GitLabDropdown = (function() {
currentIndex = 0; currentIndex = 0;
} }
} }
=======
}
return $(selector);
};
})(this),
data: (function(_this) {
return function() {
return _this.fullData;
};
})(this),
callback: (function(_this) {
return function(data) {
_this.parseData(data);
if (_this.filterInput.val() !== '') {
selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
if ($(_this.el).is('input')) {
currentIndex = -1;
} else {
$(selector, _this.dropdown).first().find('a').addClass('is-focused');
currentIndex = 0;
}
}
>>>>>>> ce/master
}; };
})(this) })(this)
}); });
...@@ -454,6 +481,7 @@ GitLabDropdown = (function() { ...@@ -454,6 +481,7 @@ GitLabDropdown = (function() {
} else { } else {
this.focusTextInput(); this.focusTextInput();
} }
<<<<<<< HEAD
if (this.options.showMenuAbove) { if (this.options.showMenuAbove) {
this.positionMenuAbove(); this.positionMenuAbove();
...@@ -500,6 +528,54 @@ GitLabDropdown = (function() { ...@@ -500,6 +528,54 @@ GitLabDropdown = (function() {
for (var i = 0; i < html.length; i += 1) { for (var i = 0; i < html.length; i += 1) {
var el = html[i]; var el = html[i];
=======
if (this.options.showMenuAbove) {
this.positionMenuAbove();
}
if (this.options.opened) {
this.options.opened.call(this, e);
}
return this.dropdown.trigger('shown.gl.dropdown');
};
GitLabDropdown.prototype.positionMenuAbove = function() {
var $button = $(this.el);
var $menu = this.dropdown.find('.dropdown-menu');
$menu.css('top', ($button.height() + $menu.height()) * -1);
};
GitLabDropdown.prototype.hidden = function(e) {
var $input;
this.resetRows();
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) {
$input.blur();
}
if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
}
if (this.options.hidden) {
this.options.hidden.call(this, e);
}
return this.dropdown.trigger('hidden.gl.dropdown');
};
// Render the full menu
GitLabDropdown.prototype.renderMenu = function(html) {
if (this.options.renderMenu) {
return this.options.renderMenu(html);
} else {
var ul = document.createElement('ul');
for (var i = 0; i < html.length; i += 1) {
var el = html[i];
>>>>>>> ce/master
if (el instanceof jQuery) { if (el instanceof jQuery) {
el = el.get(0); el = el.get(0);
} }
...@@ -514,6 +590,7 @@ GitLabDropdown = (function() { ...@@ -514,6 +590,7 @@ GitLabDropdown = (function() {
return ul; return ul;
} }
}; };
<<<<<<< HEAD
// Append the menu into the dropdown // Append the menu into the dropdown
GitLabDropdown.prototype.appendMenu = function(html) { GitLabDropdown.prototype.appendMenu = function(html) {
...@@ -575,6 +652,57 @@ GitLabDropdown = (function() { ...@@ -575,6 +652,57 @@ GitLabDropdown = (function() {
if (!selected) { if (!selected) {
fieldName = this.options.fieldName; fieldName = this.options.fieldName;
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
selected = true;
}
=======
// Append the menu into the dropdown
GitLabDropdown.prototype.appendMenu = function(html) {
return this.clearMenu().append(html);
};
GitLabDropdown.prototype.clearMenu = function() {
var selector;
selector = '.dropdown-content';
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one .dropdown-content";
}
return $(selector, this.dropdown).empty();
};
GitLabDropdown.prototype.renderItem = function(data, group, index) {
var field, fieldName, html, selected, text, url, value;
if (group == null) {
group = false;
}
if (index == null) {
// Render the row
index = false;
}
html = document.createElement('li');
if (data === 'divider' || data === 'separator') {
html.className = data;
return html;
}
// Header
if (data.header != null) {
html.className = 'dropdown-header';
html.innerHTML = data.header;
return html;
}
if (this.options.renderRow) {
// Call the render function
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
if (value) { value = value.toString().replace(/'/g, '\\\''); }
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) { if (field.length) {
selected = true; selected = true;
...@@ -605,6 +733,55 @@ GitLabDropdown = (function() { ...@@ -605,6 +733,55 @@ GitLabDropdown = (function() {
link.className = 'is-active'; link.className = 'is-active';
} }
if (group) {
link.dataset.group = group;
link.dataset.index = index;
>>>>>>> ce/master
}
// Set URL
if (this.options.url != null) {
url = this.options.url(data);
} else {
url = data.url != null ? data.url : '#';
}
// Set Text
if (this.options.text != null) {
text = this.options.text(data);
} else {
text = data.text != null ? data.text : '';
}
if (this.highlight) {
text = this.highlightTextMatches(text, this.filterInput.val());
}
// Create the list item & the link
var link = document.createElement('a');
<<<<<<< HEAD
link.href = url;
link.innerHTML = text;
if (selected) {
link.className = 'is-active';
=======
html.appendChild(link);
}
return html;
};
GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
var occurrences;
occurrences = fuzzaldrinPlus.match(text, term);
return text.split('').map(function(character, i) {
if (indexOf.call(occurrences, i) !== -1) {
return "<b>" + character + "</b>";
} else {
return character;
>>>>>>> ce/master
}
}).join('');
};
<<<<<<< HEAD
if (group) { if (group) {
link.dataset.group = group; link.dataset.group = group;
link.dataset.index = index; link.dataset.index = index;
...@@ -635,6 +812,16 @@ GitLabDropdown = (function() { ...@@ -635,6 +812,16 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.rowClicked = function(el) { GitLabDropdown.prototype.rowClicked = function(el) {
var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
=======
GitLabDropdown.prototype.noResults = function() {
var html;
return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
};
GitLabDropdown.prototype.rowClicked = function(el) {
var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
>>>>>>> ce/master
fieldName = this.options.fieldName; fieldName = this.options.fieldName;
isInput = $(this.el).is('input'); isInput = $(this.el).is('input');
if (this.renderedData) { if (this.renderedData) {
...@@ -845,12 +1032,21 @@ GitLabDropdown = (function() { ...@@ -845,12 +1032,21 @@ GitLabDropdown = (function() {
instance = null; instance = null;
} }
return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance)); return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
<<<<<<< HEAD
};
GitLabDropdown.prototype.clearField = function(field, isInput) {
return isInput ? field.val('') : field.remove();
};
=======
}; };
GitLabDropdown.prototype.clearField = function(field, isInput) { GitLabDropdown.prototype.clearField = function(field, isInput) {
return isInput ? field.val('') : field.remove(); return isInput ? field.val('') : field.remove();
}; };
>>>>>>> ce/master
return GitLabDropdown; return GitLabDropdown;
})(); })();
......
const GROUP_LIMIT = 2;
export default class GroupName {
constructor() {
this.titleContainer = document.querySelector('.title');
this.groups = document.querySelectorAll('.group-path');
this.groupTitle = document.querySelector('.group-title');
this.toggle = null;
this.isHidden = false;
this.init();
}
init() {
if (this.groups.length > GROUP_LIMIT) {
this.groups[this.groups.length - 1].classList.remove('hidable');
this.addToggle();
}
this.render();
}
addToggle() {
const header = document.querySelector('.header-content');
this.toggle = document.createElement('button');
this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...';
this.toggle.addEventListener('click', this.toggleGroups.bind(this));
header.insertBefore(this.toggle, this.titleContainer);
this.toggleGroups();
}
toggleGroups() {
this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden');
}
render() {
this.titleContainer.classList.remove('initializing');
}
}
...@@ -67,17 +67,7 @@ require('vendor/jquery.scrollTo'); ...@@ -67,17 +67,7 @@ require('vendor/jquery.scrollTo');
} }
LineHighlighter.prototype.bindEvents = function() { LineHighlighter.prototype.bindEvents = function() {
$('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler); $('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler);
// While it may seem odd to bind to the mousedown event and then throw away
// the click event, there is a method to our madness.
//
// If not done this way, the line number anchor will sometimes keep its
// active state even when the event is cancelled, resulting in an ugly border
// around the link and/or a persisted underline text decoration.
$('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
event.preventDefault();
event.stopPropagation();
});
}; };
LineHighlighter.prototype.clickHandler = function(event) { LineHighlighter.prototype.clickHandler = function(event) {
......
...@@ -16,17 +16,9 @@ import Sortable from 'vendor/Sortable'; ...@@ -16,17 +16,9 @@ import Sortable from 'vendor/Sortable';
import 'mousetrap'; import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause'; import 'mousetrap/plugins/pause/mousetrap-pause';
import 'vendor/fuzzaldrin-plus'; import 'vendor/fuzzaldrin-plus';
import promisePolyfill from 'es6-promise';
// extensions // extensions
import './extensions/string';
import './extensions/array'; import './extensions/array';
import './extensions/custom_event';
import './extensions/element';
import './extensions/jquery';
import './extensions/object';
promisePolyfill.polyfill();
// expose common libraries as globals (TODO: remove these) // expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery; window.jQuery = jQuery;
...@@ -66,6 +58,8 @@ import './blob/blob_gitignore_selectors'; ...@@ -66,6 +58,8 @@ import './blob/blob_gitignore_selectors';
import './blob/blob_license_selector'; import './blob/blob_license_selector';
import './blob/blob_license_selectors'; import './blob/blob_license_selectors';
import './blob/template_selector'; import './blob/template_selector';
import './blob/create_branch_dropdown';
import './blob/target_branch_dropdown';
// templates // templates
import './templates/issuable_template_selector'; import './templates/issuable_template_selector';
...@@ -204,6 +198,7 @@ import './visibility_select'; ...@@ -204,6 +198,7 @@ import './visibility_select';
import './wikis'; import './wikis';
import './zen_mode'; import './zen_mode';
<<<<<<< HEAD
// EE-only scripts // EE-only scripts
require('./admin_email_select'); require('./admin_email_select');
require('./application_settings'); require('./application_settings');
...@@ -212,6 +207,8 @@ require('./ldap_groups_select'); ...@@ -212,6 +207,8 @@ require('./ldap_groups_select');
require('./path_locks'); require('./path_locks');
require('./weight_select'); require('./weight_select');
=======
>>>>>>> ce/master
document.addEventListener('beforeunload', function () { document.addEventListener('beforeunload', function () {
// Unbind scroll events // Unbind scroll events
$(document).off('scroll'); $(document).off('scroll');
...@@ -301,6 +298,7 @@ $(function () { ...@@ -301,6 +298,7 @@ $(function () {
if ($flash.length > 0) { if ($flash.length > 0) {
$flash.click(function () { $flash.click(function () {
return $(this).fadeOut(); return $(this).fadeOut();
<<<<<<< HEAD
}); });
$flash.show(); $flash.show();
} }
...@@ -364,6 +362,68 @@ $(function () { ...@@ -364,6 +362,68 @@ $(function () {
return new ConfirmDangerModal(form, text, { return new ConfirmDangerModal(form, text, {
warningMessage: warningMessage warningMessage: warningMessage
}); });
=======
});
$flash.show();
}
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
var buttons;
buttons = $('[type="submit"]', this);
switch (e.type) {
case 'ajax:beforeSend':
case 'submit':
return buttons.disable();
default:
return buttons.enable();
}
});
$(document).ajaxError(function (e, xhrObj) {
var ref = xhrObj.status;
if (xhrObj.status === 401) {
return new Flash('You need to be logged in.', 'alert');
} else if (ref === 404 || ref === 500) {
return new Flash('Something went wrong on our end.', 'alert');
}
});
$('.account-box').hover(function () {
// Show/Hide the profile menu when hovering the account box
return $(this).toggleClass('hover');
});
$document.on('click', '.diff-content .js-show-suppressed-diff', function () {
var $container;
$container = $(this).parent();
$container.next('table').show();
return $container.remove();
// Commit show suppressed diff
});
$('.navbar-toggle').on('click', function () {
$('.header-content .title').toggle();
$('.header-content .header-logo').toggle();
$('.header-content .navbar-collapse').toggle();
return $('.navbar-toggle').toggleClass('active');
});
// Show/hide comments on diff
$body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this);
var notesHolders = $this.closest('.diff-file').find('.notes_holder');
$this.toggleClass('active');
if ($this.hasClass('active')) {
notesHolders.show().find('.hide, .content').show();
} else {
notesHolders.hide().find('.content').hide();
}
$(document).trigger('toggle.comments');
return e.preventDefault();
});
$document.off('click', '.js-confirm-danger');
$document.on('click', '.js-confirm-danger', function (e) {
var btn = $(e.target);
var form = btn.closest('form');
var text = btn.data('confirm-danger-message');
e.preventDefault();
return new ConfirmDangerModal(form, text);
>>>>>>> ce/master
}); });
$('input[type="search"]').each(function () { $('input[type="search"]').each(function () {
var $this = $(this); var $this = $(this);
......
/* eslint-disable no-new*/ /* eslint-disable no-new */
/* global Flash */
import d3 from 'd3'; import d3 from 'd3';
import _ from 'underscore'; import _ from 'underscore';
import statusCodes from '~/lib/utils/http_status'; import statusCodes from '~/lib/utils/http_status';
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
import Flash from '~/flash'; import '~/flash';
const prometheusGraphsContainer = '.prometheus-graph'; const prometheusGraphsContainer = '.prometheus-graph';
const metricsEndpoint = 'metrics.json'; const metricsEndpoint = 'metrics.json';
......
...@@ -3,19 +3,23 @@ ...@@ -3,19 +3,23 @@
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.NewCommitForm = (function() { this.NewCommitForm = (function() {
function NewCommitForm(form) { function NewCommitForm(form, targetBranchName = 'target_branch') {
this.form = form;
this.targetBranchName = targetBranchName;
this.renderDestination = bind(this.renderDestination, this); this.renderDestination = bind(this.renderDestination, this);
this.newBranch = form.find('.js-target-branch'); this.targetBranchDropdown = form.find('button.js-target-branch');
this.originalBranch = form.find('.js-original-branch'); this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request'); this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
this.targetBranchDropdown.on('change.branch', this.renderDestination);
this.renderDestination(); this.renderDestination();
this.newBranch.keyup(this.renderDestination);
} }
NewCommitForm.prototype.renderDestination = function() { NewCommitForm.prototype.renderDestination = function() {
var different; var different;
different = this.newBranch.val() !== this.originalBranch.val(); var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`);
different = targetBranch.val() !== this.originalBranch.val();
if (different) { if (different) {
this.createMergeRequestContainer.show(); this.createMergeRequestContainer.show();
if (!this.wasDifferent) { if (!this.wasDifferent) {
......
/* eslint-disable class-methods-use-this, no-new, func-names, no-unneeded-ternary, object-shorthand, quote-props, no-param-reassign, max-len */ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
/* global UsersSelect */ /* global UsersSelect */
((global) => { class Todos {
class Todos { constructor() {
constructor() { this.initFilters();
this.initFilters(); this.bindEvents();
this.bindEvents();
this.cleanupWrapper = this.cleanup.bind(this); this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('beforeunload', this.cleanupWrapper); document.addEventListener('beforeunload', this.cleanupWrapper);
} }
cleanup() { cleanup() {
this.unbindEvents(); this.unbindEvents();
document.removeEventListener('beforeunload', this.cleanupWrapper); document.removeEventListener('beforeunload', this.cleanupWrapper);
} }
unbindEvents() { unbindEvents() {
$('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper); $('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper);
$('.js-todos-mark-all').off('click', this.allDoneClickedWrapper); $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper);
$('.todo').off('click', this.goToTodoUrl); $('.todo').off('click', this.goToTodoUrl);
} }
bindEvents() { bindEvents() {
this.updateStateClickedWrapper = this.updateStateClicked.bind(this); this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this);
this.allDoneClickedWrapper = this.allDoneClicked.bind(this); this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
$('.js-done-todo, .js-undo-todo').on('click', this.updateStateClickedWrapper); $('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper);
$('.js-todos-mark-all').on('click', this.allDoneClickedWrapper); $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper);
$('.todo').on('click', this.goToTodoUrl); $('.todo').on('click', this.goToTodoUrl);
} }
initFilters() { initFilters() {
new UsersSelect(); this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); this.initFilterDropdown($('.js-type-search'), 'type');
this.initFilterDropdown($('.js-type-search'), 'type'); this.initFilterDropdown($('.js-action-search'), 'action_id');
this.initFilterDropdown($('.js-action-search'), 'action_id');
$('form.filter-form').on('submit', function (event) { $('form.filter-form').on('submit', function applyFilters(event) {
event.preventDefault(); event.preventDefault();
gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`); gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
}); });
} return new UsersSelect();
}
initFilterDropdown($dropdown, fieldName, searchFields) { initFilterDropdown($dropdown, fieldName, searchFields) {
$dropdown.glDropdown({ $dropdown.glDropdown({
fieldName, fieldName,
selectable: true, selectable: true,
filterable: searchFields ? true : false, filterable: searchFields ? true : false,
search: { fields: searchFields }, search: { fields: searchFields },
data: $dropdown.data('data'), data: $dropdown.data('data'),
clicked: function () { clicked: () => $dropdown.closest('form.filter-form').submit(),
return $dropdown.closest('form.filter-form').submit(); });
}, }
});
}
updateStateClicked(e) { updateRowStateClicked(e) {
e.preventDefault(); e.preventDefault();
const target = e.target;
target.setAttribute('disabled', ''); const target = e.target;
target.classList.add('disabled'); target.setAttribute('disabled', '');
$.ajax({ target.classList.add('disabled');
type: 'POST', $.ajax({
url: target.getAttribute('href'), type: 'POST',
dataType: 'json', url: target.getAttribute('href'),
data: { dataType: 'json',
'_method': target.getAttribute('data-method'), data: {
}, '_method': target.getAttribute('data-method'),
success: (data) => { },
this.updateState(target); success: (data) => {
this.updateBadges(data); this.updateRowState(target);
}, return this.updateBadges(data);
}); },
} });
}
allDoneClicked(e) { allDoneClicked(e) {
e.preventDefault(); e.preventDefault();
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
$target.disable(); $target.disable();
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: $target.attr('href'), url: $target.attr('href'),
dataType: 'json', dataType: 'json',
data: { data: {
'_method': 'delete', '_method': 'delete',
}, },
success: (data) => { success: (data) => {
$target.remove(); $target.remove();
$('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>'); $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
this.updateBadges(data); this.updateBadges(data);
}, },
}); });
}
updateRowState(target) {
const row = target.closest('li');
const restoreBtn = row.querySelector('.js-undo-todo');
const doneBtn = row.querySelector('.js-done-todo');
target.classList.add('hidden');
target.removeAttribute('disabled');
target.classList.remove('disabled');
if (target === doneBtn) {
row.classList.add('done-reversible');
restoreBtn.classList.remove('hidden');
} else if (target === restoreBtn) {
row.classList.remove('done-reversible');
doneBtn.classList.remove('hidden');
} else {
row.parentNode.removeChild(row);
} }
}
updateState(target) { updateBadges(data) {
const row = target.closest('li'); $(document).trigger('todo:toggle', data.count);
const restoreBtn = row.querySelector('.js-undo-todo'); document.querySelector('.todos-pending .badge').innerHTML = data.count;
const doneBtn = row.querySelector('.js-done-todo'); document.querySelector('.todos-done .badge').innerHTML = data.done_count;
}
target.removeAttribute('disabled'); goToTodoUrl(e) {
target.classList.remove('disabled'); const todoLink = this.dataset.url;
target.classList.add('hidden');
if (target === doneBtn) { if (!todoLink) {
row.classList.add('done-reversible'); return;
restoreBtn.classList.remove('hidden');
} else {
row.classList.remove('done-reversible');
doneBtn.classList.remove('hidden');
}
}
updateBadges(data) {
$(document).trigger('todo:toggle', data.count);
$('.todos-pending .badge').text(data.count);
$('.todos-done .badge').text(data.done_count);
} }
goToTodoUrl(e) { if (gl.utils.isMetaClick(e)) {
const todoLink = this.dataset.url; const windowTarget = '_blank';
const selected = e.target;
if (!todoLink) { e.preventDefault();
return;
}
if (gl.utils.isMetaClick(e)) { if (selected.tagName === 'IMG') {
const windowTarget = '_blank'; const avatarUrl = selected.parentElement.getAttribute('href');
const selected = e.target; window.open(avatarUrl, windowTarget);
e.preventDefault();
if (selected.tagName === 'IMG') {
const avatarUrl = selected.parentElement.getAttribute('href');
window.open(avatarUrl, windowTarget);
} else {
window.open(todoLink, windowTarget);
}
} else { } else {
gl.utils.visitUrl(todoLink); window.open(todoLink, windowTarget);
} }
} else {
gl.utils.visitUrl(todoLink);
} }
} }
}
global.Todos = Todos; window.gl = window.gl || {};
})(window.gl || (window.gl = {})); gl.Todos = Todos;
...@@ -69,7 +69,7 @@ import warningSvg from 'icons/_icon_status_warning_borderless.svg'; ...@@ -69,7 +69,7 @@ import warningSvg from 'icons/_icon_status_warning_borderless.svg';
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => { $(this.$el).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (e) => {
e.stopPropagation(); e.stopPropagation();
}); });
}, },
......
...@@ -6,10 +6,6 @@ Vue.http.interceptors.push((request, next) => { ...@@ -6,10 +6,6 @@ Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
next((response) => { next((response) => {
if (typeof response.data === 'string') {
response.data = JSON.parse(response.data);
}
Vue.activeResources--; Vue.activeResources--;
}); });
}); });
......
...@@ -164,11 +164,25 @@ header { ...@@ -164,11 +164,25 @@ header {
} }
} }
.group-name-toggle {
margin: 0 5px;
vertical-align: sub;
}
.group-title {
&.is-hidden {
.hidable:not(:last-of-type) {
display: none;
}
}
}
.title { .title {
position: relative; position: relative;
padding-right: 20px; padding-right: 20px;
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
max-width: 385px;
display: inline-block; display: inline-block;
line-height: $header-height; line-height: $header-height;
font-weight: normal; font-weight: normal;
...@@ -178,6 +192,14 @@ header { ...@@ -178,6 +192,14 @@ header {
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
&.initializing {
display: none;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
max-width: 300px;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
max-width: 190px; max-width: 190px;
} }
......
...@@ -57,8 +57,13 @@ ...@@ -57,8 +57,13 @@
visibility: hidden; visibility: hidden;
} }
&:hover i { &:hover,
visibility: visible; &:focus {
outline: none;
& i {
visibility: visible;
}
} }
} }
} }
......
...@@ -141,6 +141,14 @@ ...@@ -141,6 +141,14 @@
margin-right: 0; margin-right: 0;
} }
} }
.no-btn {
border: none;
background: none;
outline: none;
width: 100%;
text-align: left;
}
} }
} }
......
...@@ -178,8 +178,25 @@ ...@@ -178,8 +178,25 @@
padding-right: 5px; padding-right: 5px;
} }
&:last-child { }
padding-left: 5px;
.discussion-actions {
display: table;
.new-issue-for-discussion path {
fill: $gray-darkest;
}
.btn-group {
display: table-cell;
&:first-child {
padding-right: 0;
}
&:first-child:not(:last-child) > div {
border-right: 0;
}
} }
} }
......
...@@ -384,7 +384,7 @@ ul.notes { ...@@ -384,7 +384,7 @@ ul.notes {
top: 0; top: 0;
.note-action-button { .note-action-button {
margin-left: 10px; margin-left: 8px;
} }
} }
...@@ -400,8 +400,7 @@ ul.notes { ...@@ -400,8 +400,7 @@ ul.notes {
} }
.note-action-button { .note-action-button {
display: inline-block; display: inline;
margin-left: 0;
line-height: 20px; line-height: 20px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
...@@ -510,6 +509,7 @@ ul.notes { ...@@ -510,6 +509,7 @@ ul.notes {
} }
.line-resolve-all-container { .line-resolve-all-container {
.btn-group { .btn-group {
margin-left: -4px; margin-left: -4px;
} }
...@@ -518,6 +518,27 @@ ul.notes { ...@@ -518,6 +518,27 @@ ul.notes {
border-top-left-radius: 0; border-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
.btn.discussion-create-issue-btn {
margin-left: -4px;
border-radius: 0;
border-right: 0;
a {
padding: 0;
line-height: 0;
&:hover {
text-decoration: none;
border: 0;
}
}
.new-issue-for-discussion path {
fill: $gray-darkest;
}
}
} }
.line-resolve-all { .line-resolve-all {
...@@ -540,7 +561,6 @@ ul.notes { ...@@ -540,7 +561,6 @@ ul.notes {
} }
.line-resolve-btn { .line-resolve-btn {
display: inline-block;
position: relative; position: relative;
top: 2px; top: 2px;
padding: 0; padding: 0;
...@@ -563,8 +583,9 @@ ul.notes { ...@@ -563,8 +583,9 @@ ul.notes {
} }
svg { svg {
position: relative;
fill: $gray-darkest; fill: $gray-darkest;
height: 15px;
width: 15px;
} }
} }
......
...@@ -812,7 +812,8 @@ a.allowed-to-push { ...@@ -812,7 +812,8 @@ a.allowed-to-push {
} }
.project-refs-form .dropdown-menu, .project-refs-form .dropdown-menu,
.dropdown-menu-projects { .dropdown-menu-projects,
.dropdown-menu-branches {
width: 300px; width: 300px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
......
...@@ -29,11 +29,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -29,11 +29,7 @@ class Admin::UsersController < Admin::ApplicationController
end end
def impersonate def impersonate
if user.blocked? if can?(user, :log_in)
flash[:alert] = "You cannot impersonate a blocked user"
redirect_to admin_user_path(user)
else
session[:impersonator_id] = current_user.id session[:impersonator_id] = current_user.id
warden.set_user(user, scope: :user) warden.set_user(user, scope: :user)
...@@ -43,6 +39,17 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -43,6 +39,17 @@ class Admin::UsersController < Admin::ApplicationController
flash[:alert] = "You are now impersonating #{user.username}" flash[:alert] = "You are now impersonating #{user.username}"
redirect_to root_path redirect_to root_path
else
flash[:alert] =
if user.blocked?
"You cannot impersonate a blocked user"
elsif user.internal?
"You cannot impersonate an internal user"
else
"You cannot impersonate a user who cannot log in"
end
redirect_to admin_user_path(user)
end end
end end
......
...@@ -67,7 +67,7 @@ class ApplicationController < ActionController::Base ...@@ -67,7 +67,7 @@ class ApplicationController < ActionController::Base
token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string) user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
if user if user && can?(user, :log_in)
# Notice we are passing store false, so the user is not # Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed # actually stored in the session and a token is needed
# for every request. If you want the token to work as a # for every request. If you want the token to work as a
...@@ -94,7 +94,7 @@ class ApplicationController < ActionController::Base ...@@ -94,7 +94,7 @@ class ApplicationController < ActionController::Base
end end
end end
def can?(object, action, subject) def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject) Ability.allowed?(object, action, subject)
end end
......
...@@ -23,7 +23,7 @@ module AuthenticatesWithTwoFactor ...@@ -23,7 +23,7 @@ module AuthenticatesWithTwoFactor
# #
# Returns nil # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
return locked_user_redirect(user) if user.access_locked? return locked_user_redirect(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
setup_u2f_authentication(user) setup_u2f_authentication(user)
...@@ -37,10 +37,9 @@ module AuthenticatesWithTwoFactor ...@@ -37,10 +37,9 @@ module AuthenticatesWithTwoFactor
def authenticate_with_two_factor def authenticate_with_two_factor
user = self.resource = find_user user = self.resource = find_user
return locked_user_redirect(user) unless user.can?(:log_in)
if user.access_locked? if user_params[:otp_attempt].present? && session[:otp_user_id]
locked_user_redirect(user)
elsif user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user) authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id] elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user) authenticate_with_two_factor_via_u2f(user)
......
...@@ -5,6 +5,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController ...@@ -5,6 +5,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
@milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page]) @milestones = Kaminari.paginate_array(milestones).page(params[:page])
end end
format.json do format.json do
......
...@@ -6,6 +6,7 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -6,6 +6,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
@milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page]) @milestones = Kaminari.paginate_array(milestones).page(params[:page])
end end
end end
......
...@@ -118,7 +118,7 @@ class GroupsController < Groups::ApplicationController ...@@ -118,7 +118,7 @@ class GroupsController < Groups::ApplicationController
end end
def authorize_create_group! def authorize_create_group!
unless can?(current_user, :create_group, nil) unless can?(current_user, :create_group)
return render_404 return render_404
end end
end end
......
...@@ -23,6 +23,8 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -23,6 +23,8 @@ class Projects::BlobController < Projects::ApplicationController
end end
def create def create
update_ref
create_commit(Files::CreateService, success_notice: "The file has been successfully created.", create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }, success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) },
failure_view: :new, failure_view: :new,
...@@ -87,6 +89,11 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -87,6 +89,11 @@ class Projects::BlobController < Projects::ApplicationController
private private
def update_ref
branch_exists = @repository.find_branch(@target_branch)
@ref = @target_branch if branch_exists
end
def blob def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path)) @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
......
...@@ -10,15 +10,16 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -10,15 +10,16 @@ class Projects::BranchesController < Projects::ApplicationController
def index def index
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@branches = BranchesFinder.new(@repository, params).execute @branches = BranchesFinder.new(@repository, params).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page])
@max_commits = @branches.reduce(0) do |memo, branch| @branches = Kaminari.paginate_array(@branches).page(params[:page]) unless params[:show_all].present?
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
respond_to do |format| respond_to do |format|
format.html format.html do
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
end
format.json do format.json do
render json: @branches.map(&:name) render json: @branches.map(&:name)
end end
......
...@@ -67,8 +67,15 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -67,8 +67,15 @@ class Projects::IssuesController < Projects::ApplicationController
params[:issue] ||= ActionController::Parameters.new( params[:issue] ||= ActionController::Parameters.new(
assignee_id: "" assignee_id: ""
) )
build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) build_params = issue_params.merge(
@issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve]
)
service = Issues::BuildService.new(project, current_user, build_params)
@issue = @noteable = service.execute
@merge_request_to_resolve_discussions_of = service.merge_request_to_resolve_discussions_of
@discussion_to_resolve = service.discussions_to_resolve.first if params[:discussion_to_resolve]
# Set Issue description based on project template # Set Issue description based on project template
if @project.issues_template.present? if @project.issues_template.present?
...@@ -102,11 +109,21 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -102,11 +109,21 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def create def create
create_params = issue_params create_params = issue_params.merge(spammable_params).merge(
.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
.merge(spammable_params) discussion_to_resolve: params[:discussion_to_resolve]
)
service = Issues::CreateService.new(project, current_user, create_params)
@issue = service.execute
@issue = Issues::CreateService.new(project, current_user, create_params).execute if service.discussions_to_resolve.count(&:resolved?) > 0
flash[:notice] = if service.discussion_to_resolve_id
"Resolved 1 discussion."
else
"Resolved all discussions."
end
end
respond_to do |format| respond_to do |format|
format.html do format.html do
...@@ -201,14 +218,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -201,14 +218,6 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :awardable, :issue alias_method :awardable, :issue
alias_method :spammable, :issue alias_method :spammable, :issue
def merge_request_for_resolving_discussions
return unless merge_request_iid = params[:merge_request_for_resolving_discussions]
@merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id).
execute.
find_by(iid: merge_request_iid)
end
def authorize_read_issue! def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
end end
......
...@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController ...@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob? return if cached_blob?
if @blob.lfs_pointer? if @blob.lfs_pointer? && project.lfs_enabled?
send_lfs_object send_lfs_object
else else
send_git_blob @repository, @blob send_git_blob @repository, @blob
......
...@@ -41,13 +41,27 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -41,13 +41,27 @@ class Projects::TagsController < Projects::ApplicationController
end end
def destroy def destroy
Tags::DestroyService.new(project, current_user).execute(params[:id]) result = Tags::DestroyService.new(project, current_user).execute(params[:id])
respond_to do |format| respond_to do |format|
format.html do if result[:status] == :success
redirect_to namespace_project_tags_path(@project.namespace, @project) format.html do
redirect_to namespace_project_tags_path(@project.namespace, @project)
end
format.js
else
@error = result[:message]
format.html do
redirect_to namespace_project_tags_path(@project.namespace, @project),
alert: @error
end
format.js do
render status: :unprocessable_entity
end
end end
format.js
end end
end end
end end
...@@ -118,7 +118,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -118,7 +118,7 @@ class ProjectsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :remove_project, @project) return access_denied! unless can?(current_user, :remove_project, @project)
::Projects::DestroyService.new(@project, current_user, {}).async_execute ::Projects::DestroyService.new(@project, current_user, {}).async_execute
flash[:alert] = "Project '#{@project.name}' will be deleted." flash[:alert] = "Project '#{@project.name_with_namespace}' will be deleted."
redirect_to dashboard_projects_path redirect_to dashboard_projects_path
rescue Projects::DestroyService::DestroyError => ex rescue Projects::DestroyService::DestroyError => ex
......
...@@ -165,8 +165,8 @@ module EventsHelper ...@@ -165,8 +165,8 @@ module EventsHelper
sanitize( sanitize(
text, text,
tags: %w(a img b pre code p span), tags: %w(a img gl-emoji b pre code p span),
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style'] attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-name', 'data-unicode-version']
) )
end end
......
...@@ -172,7 +172,7 @@ module GitlabMarkdownHelper ...@@ -172,7 +172,7 @@ module GitlabMarkdownHelper
# text hasn't already been truncated, then append "..." to the node contents # text hasn't already been truncated, then append "..." to the node contents
# and return true. Otherwise return false. # and return true. Otherwise return false.
def truncate_if_block(node, truncated) def truncate_if_block(node, truncated)
if node.element? && node.description.block? && !truncated if node.element? && node.description&.block? && !truncated
node.inner_html = "#{node.inner_html}..." if node.next_sibling node.inner_html = "#{node.inner_html}..." if node.next_sibling
true true
else else
......
...@@ -12,17 +12,18 @@ module GroupsHelper ...@@ -12,17 +12,18 @@ module GroupsHelper
end end
def group_title(group, name = nil, url = nil) def group_title(group, name = nil, url = nil)
@has_group_title = true
full_title = '' full_title = ''
group.ancestors.each do |parent| group.ancestors.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent)) full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable')
full_title += ' / '.html_safe full_title += '<span class="hidable"> / </span>'.html_safe
end end
full_title += link_to(simple_sanitize(group.name), group_path(group)) full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path')
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name
content_tag :span do content_tag :span, class: 'group-title' do
full_title.html_safe full_title.html_safe
end end
end end
......
module IssuablesHelper module IssuablesHelper
include GitlabRoutingHelper
def sidebar_gutter_toggle_icon def sidebar_gutter_toggle_icon
sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' }) sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' })
end end
...@@ -95,8 +97,23 @@ module IssuablesHelper ...@@ -95,8 +97,23 @@ module IssuablesHelper
h(milestone_title.presence || default_label) h(milestone_title.presence || default_label)
end end
def to_url_reference(issuable)
case issuable
when Issue
link_to issuable.to_reference, issue_url(issuable)
when MergeRequest
link_to issuable.to_reference, merge_request_url(issuable)
else
issuable.to_reference
end
end
def issuable_meta(issuable, project, text) def issuable_meta(issuable, project, text)
output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier" output = content_tag(:strong, class: "identifier") do
concat("#{text} ")
concat(to_url_reference(issuable))
end
output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
......
...@@ -134,6 +134,20 @@ module IssuesHelper ...@@ -134,6 +134,20 @@ module IssuesHelper
options_from_collection_for_select(options, 'name', 'title', params[:due_date]) options_from_collection_for_select(options, 'name', 'title', params[:due_date])
end end
def link_to_discussions_to_resolve(merge_request, single_discussion = nil)
link_text = merge_request.to_reference
link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion
path = if single_discussion
Gitlab::UrlBuilder.build(single_discussion.first_note)
else
project = merge_request.project
namespace_project_merge_request_path(project.namespace, project, merge_request)
end
link_to link_text, path
end
# Required for Banzai::Filter::IssueReferenceFilter # Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue module_function :url_for_issue
end end
...@@ -56,15 +56,16 @@ class Ability ...@@ -56,15 +56,16 @@ class Ability
end end
end end
def allowed?(user, action, subject) def allowed?(user, action, subject = :global)
allowed(user, subject).include?(action) allowed(user, subject).include?(action)
end end
def allowed(user, subject) def allowed(user, subject = :global)
return BasePolicy::RuleSet.none if subject.nil?
return uncached_allowed(user, subject) unless RequestStore.active? return uncached_allowed(user, subject) unless RequestStore.active?
user_key = user ? user.id : 'anonymous' user_key = user ? user.id : 'anonymous'
subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global' subject_key = subject == :global ? 'global' : "#{subject.class.name}/#{subject.id}"
key = "/ability/#{user_key}/#{subject_key}" key = "/ability/#{user_key}/#{subject_key}"
RequestStore[key] ||= uncached_allowed(user, subject).freeze RequestStore[key] ||= uncached_allowed(user, subject).freeze
end end
......
...@@ -54,9 +54,13 @@ class Blob < SimpleDelegator ...@@ -54,9 +54,13 @@ class Blob < SimpleDelegator
UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.')) UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
end end
def to_partial_path def to_partial_path(project)
if lfs_pointer? if lfs_pointer?
'download' if project.lfs_enabled?
'download'
else
'text'
end
elsif image? || svg? elsif image? || svg?
'image' 'image'
elsif text? elsif text?
......
...@@ -2,16 +2,14 @@ module RelativePositioning ...@@ -2,16 +2,14 @@ module RelativePositioning
extend ActiveSupport::Concern extend ActiveSupport::Concern
MIN_POSITION = 0 MIN_POSITION = 0
START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2
MAX_POSITION = Gitlab::Database::MAX_INT_VALUE MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
IDEAL_DISTANCE = 500
included do included do
after_save :save_positionable_neighbours after_save :save_positionable_neighbours
end end
def min_relative_position
self.class.in_projects(project.id).minimum(:relative_position)
end
def max_relative_position def max_relative_position
self.class.in_projects(project.id).maximum(:relative_position) self.class.in_projects(project.id).maximum(:relative_position)
end end
...@@ -26,7 +24,7 @@ module RelativePositioning ...@@ -26,7 +24,7 @@ module RelativePositioning
maximum(:relative_position) maximum(:relative_position)
end end
prev_pos || MIN_POSITION prev_pos
end end
def next_relative_position def next_relative_position
...@@ -39,55 +37,95 @@ module RelativePositioning ...@@ -39,55 +37,95 @@ module RelativePositioning
minimum(:relative_position) minimum(:relative_position)
end end
next_pos || MAX_POSITION next_pos
end end
def move_between(before, after) def move_between(before, after)
return move_after(before) unless after return move_after(before) unless after
return move_before(after) unless before return move_before(after) unless before
# If there is no place to insert an issue we need to create one by moving the before issue closer
# to its predecessor. This process will recursively move all the predecessors until we have a place
if (after.relative_position - before.relative_position) < 2
before.move_before
@positionable_neighbours = [before]
end
self.relative_position = position_between(before.relative_position, after.relative_position)
end
def move_after(before = self)
pos_before = before.relative_position pos_before = before.relative_position
pos_after = before.next_relative_position
if before.shift_after?
issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after)
issue_to_move.move_after
@positionable_neighbours = [issue_to_move]
pos_after = issue_to_move.relative_position
end
self.relative_position = position_between(pos_before, pos_after)
end
def move_before(after = self)
pos_after = after.relative_position pos_after = after.relative_position
pos_before = after.prev_relative_position
if pos_after && (pos_before == pos_after) if after.shift_before?
self.relative_position = pos_before issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before)
before.move_before(self) issue_to_move.move_before
after.move_after(self) @positionable_neighbours = [issue_to_move]
@positionable_neighbours = [before, after] pos_before = issue_to_move.relative_position
else
self.relative_position = position_between(pos_before, pos_after)
end end
self.relative_position = position_between(pos_before, pos_after)
end end
def move_before(after) def move_to_end
self.relative_position = position_between(after.prev_relative_position, after.relative_position) self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
end end
def move_after(before) # Indicates if there is an issue that should be shifted to free the place
self.relative_position = position_between(before.relative_position, before.next_relative_position) def shift_after?
next_pos = next_relative_position
next_pos && (next_pos - relative_position) == 1
end end
def move_to_end # Indicates if there is an issue that should be shifted to free the place
self.relative_position = position_between(max_relative_position, MAX_POSITION) def shift_before?
prev_pos = prev_relative_position
prev_pos && (relative_position - prev_pos) == 1
end end
private private
# This method takes two integer values (positions) and # This method takes two integer values (positions) and
# calculates some random position between them. The range is huge as # calculates the position between them. The range is huge as
# the maximum integer value is 2147483647. Ideally, the calculated value would be # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time
# exactly between those terminating values, but this will introduce possibility of a race condition # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number
# so two or more issues can get the same value, we want to avoid that and we also want to avoid
# using a lock here. If we have two issues with distance more than one thousand, we are OK.
# Given the huge range of possible values that integer can fit we shoud never face a problem.
def position_between(pos_before, pos_after) def position_between(pos_before, pos_after)
pos_before ||= MIN_POSITION pos_before ||= MIN_POSITION
pos_after ||= MAX_POSITION pos_after ||= MAX_POSITION
pos_before, pos_after = [pos_before, pos_after].sort pos_before, pos_after = [pos_before, pos_after].sort
rand(pos_before.next..pos_after.pred) halfway = (pos_after + pos_before) / 2
distance_to_halfway = pos_after - halfway
if distance_to_halfway < IDEAL_DISTANCE
halfway
else
if pos_before == MIN_POSITION
pos_after - IDEAL_DISTANCE
elsif pos_after == MAX_POSITION
pos_before + IDEAL_DISTANCE
else
halfway
end
end
end end
def save_positionable_neighbours def save_positionable_neighbours
......
...@@ -28,6 +28,28 @@ class GlobalMilestone ...@@ -28,6 +28,28 @@ class GlobalMilestone
new(title, child_milestones) new(title, child_milestones)
end end
def self.states_count(projects)
relation = MilestonesFinder.new.execute(projects, state: 'all')
milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count
opened = count_by_state(milestones_by_state_and_title, 'active')
closed = count_by_state(milestones_by_state_and_title, 'closed')
all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
{
opened: opened,
closed: closed,
all: all
}
end
def self.count_by_state(milestones_by_state_and_title, state)
milestones_by_state_and_title.count do |(milestone_state, _), _|
milestone_state == state
end
end
private_class_method :count_by_state
def initialize(title, milestones) def initialize(title, milestones)
@title = title @title = title
@name = title @name = title
......
class Guest class Guest
class << self class << self
def can?(action, subject) def can?(action, subject = :global)
Ability.allowed?(nil, action, subject) Ability.allowed?(nil, action, subject)
end end
end end
......
...@@ -106,6 +106,13 @@ class Issue < ActiveRecord::Base ...@@ -106,6 +106,13 @@ class Issue < ActiveRecord::Base
end end
end end
def self.order_by_position_and_priority
order_labels_priority.
reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'),
Gitlab::Database.nulls_last_order('highest_priority', 'ASC'),
"id DESC")
end
# `from` argument can be a Namespace or Project. # `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
......
...@@ -137,7 +137,6 @@ class User < ActiveRecord::Base ...@@ -137,7 +137,6 @@ class User < ActiveRecord::Base
validate :unique_email, if: ->(user) { user.email_changed? } validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? }
validate :ghost_users_must_be_blocked
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create before_validation :generate_password, on: :create
...@@ -375,12 +374,27 @@ class User < ActiveRecord::Base ...@@ -375,12 +374,27 @@ class User < ActiveRecord::Base
def ghost def ghost
unique_internal(where(ghost: true), 'ghost', 'ghost%s@example.com') do |u| unique_internal(where(ghost: true), 'ghost', 'ghost%s@example.com') do |u|
u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.' u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
u.state = :blocked
u.name = 'Ghost User' u.name = 'Ghost User'
end end
end end
end end
def self.internal_attributes
[:ghost]
end
def internal?
self.class.internal_attributes.any? { |a| self[a] }
end
def self.internal
where(Hash[internal_attributes.zip([true] * internal_attributes.size)])
end
def self.non_internal
where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
end
# #
# Instance methods # Instance methods
# #
...@@ -477,12 +491,6 @@ class User < ActiveRecord::Base ...@@ -477,12 +491,6 @@ class User < ActiveRecord::Base
errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email) errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end end
def ghost_users_must_be_blocked
if ghost? && !blocked?
errors.add(:ghost, 'cannot be enabled for a user who is not blocked.')
end
end
def update_emails_with_primary_email def update_emails_with_primary_email
primary_email_record = emails.find_by(email: email) primary_email_record = emails.find_by(email: email)
if primary_email_record if primary_email_record
...@@ -588,14 +596,14 @@ class User < ActiveRecord::Base ...@@ -588,14 +596,14 @@ class User < ActiveRecord::Base
end end
def can_create_group? def can_create_group?
can?(:create_group, nil) can?(:create_group)
end end
def can_select_namespace? def can_select_namespace?
several_namespaces? || admin several_namespaces? || admin
end end
def can?(action, subject) def can?(action, subject = :global)
Ability.allowed?(self, action, subject) Ability.allowed?(self, action, subject)
end end
...@@ -991,6 +999,14 @@ class User < ActiveRecord::Base ...@@ -991,6 +999,14 @@ class User < ActiveRecord::Base
self.auditor = (new_level == 'auditor') self.auditor = (new_level == 'auditor')
end end
protected
# override, from Devise::Validatable
def password_required?
return false if internal?
super
end
private private
def ci_projects_union def ci_projects_union
...@@ -1091,7 +1107,6 @@ class User < ActiveRecord::Base ...@@ -1091,7 +1107,6 @@ class User < ActiveRecord::Base
scope.create( scope.create(
username: username, username: username,
password: Devise.friendly_token,
email: email, email: email,
&creation_block &creation_block
) )
......
...@@ -12,6 +12,10 @@ class BasePolicy ...@@ -12,6 +12,10 @@ class BasePolicy
new(Set.new, Set.new) new(Set.new, Set.new)
end end
def self.none
empty.freeze
end
def can?(ability) def can?(ability)
@can_set.include?(ability) && !@cannot_set.include?(ability) @can_set.include?(ability) && !@cannot_set.include?(ability)
end end
...@@ -49,7 +53,8 @@ class BasePolicy ...@@ -49,7 +53,8 @@ class BasePolicy
end end
def self.class_for(subject) def self.class_for(subject)
return GlobalPolicy if subject.nil? return GlobalPolicy if subject == :global
raise ArgumentError, 'no policy for nil' if subject.nil?
if subject.class.try(:presenter?) if subject.class.try(:presenter?)
subject = subject.subject subject = subject.subject
...@@ -79,7 +84,7 @@ class BasePolicy ...@@ -79,7 +84,7 @@ class BasePolicy
end end
def abilities def abilities
return RuleSet.empty if @user && @user.blocked? return RuleSet.none if @user && @user.blocked?
return anonymous_abilities if @user.nil? return anonymous_abilities if @user.nil?
collect_rules { rules } collect_rules { rules }
end end
......
...@@ -4,5 +4,12 @@ class GlobalPolicy < BasePolicy ...@@ -4,5 +4,12 @@ class GlobalPolicy < BasePolicy
can! :create_group if @user.can_create_group can! :create_group if @user.can_create_group
can! :read_users_list can! :read_users_list
unless @user.blocked? || @user.internal?
can! :log_in unless @user.access_locked?
can! :access_api
can! :access_git
can! :receive_notifications
end
end end
end end
...@@ -5,7 +5,7 @@ module Boards ...@@ -5,7 +5,7 @@ module Boards
issues = IssuesFinder.new(current_user, filter_params).execute issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless movable_list? issues = without_board_labels(issues) unless movable_list?
issues = with_list_label(issues) if movable_list? issues = with_list_label(issues) if movable_list?
issues.reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC')) issues.order_by_position_and_priority
end end
private private
......
module Issues
module ResolveDiscussions
attr_reader :merge_request_to_resolve_discussions_of_iid, :discussion_to_resolve_id
def filter_resolve_discussion_params
@merge_request_to_resolve_discussions_of_iid ||= params.delete(:merge_request_to_resolve_discussions_of)
@discussion_to_resolve_id ||= params.delete(:discussion_to_resolve)
end
def merge_request_to_resolve_discussions_of
return @merge_request_to_resolve_discussions_of if defined?(@merge_request_to_resolve_discussions_of)
@merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id).
execute.
find_by(iid: merge_request_to_resolve_discussions_of_iid)
end
def discussions_to_resolve
return [] unless merge_request_to_resolve_discussions_of
@discussions_to_resolve ||=
if discussion_to_resolve_id
discussion_or_nil = merge_request_to_resolve_discussions_of
.find_diff_discussion(discussion_to_resolve_id)
Array(discussion_or_nil)
else
merge_request_to_resolve_discussions_of
.resolvable_discussions
end
end
end
end
module Issues module Issues
class BaseService < ::IssuableBaseService class BaseService < ::IssuableBaseService
attr_reader :merge_request_for_resolving_discussions
def initialize(*args)
super
@merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions)
end
def hook_data(issue, action) def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user) issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.build(issue) issue_url = Gitlab::UrlBuilder.build(issue)
......
module Issues module Issues
class BuildService < Issues::BaseService class BuildService < Issues::BaseService
include ResolveDiscussions
def execute def execute
filter_resolve_discussion_params
@issue = project.issues.new(issue_params) @issue = project.issues.new(issue_params)
end end
def issue_params_with_info_from_merge_request def issue_params_with_info_from_discussions
return {} unless merge_request_for_resolving_discussions return {} unless merge_request_to_resolve_discussions_of
{ title: title_from_merge_request, description: description_from_merge_request } { title: title_from_merge_request, description: description_for_discussions }
end end
def title_from_merge_request def title_from_merge_request
"Follow-up from \"#{merge_request_for_resolving_discussions.title}\"" "Follow-up from \"#{merge_request_to_resolve_discussions_of.title}\""
end end
def description_from_merge_request def description_for_discussions
if merge_request_for_resolving_discussions.resolvable_discussions.empty? if discussions_to_resolve.empty?
return "There are no unresolved discussions. "\ return "There are no unresolved discussions. "\
"Review the conversation in #{merge_request_for_resolving_discussions.to_reference}" "Review the conversation in #{merge_request_to_resolve_discussions_of.to_reference}"
end end
description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:" description = "The following #{'discussion'.pluralize(discussions_to_resolve.size)} "\
"from #{merge_request_to_resolve_discussions_of.to_reference} "\
"should be addressed:"
[description, *items_for_discussions].join("\n\n") [description, *items_for_discussions].join("\n\n")
end end
def items_for_discussions def items_for_discussions
merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) } discussions_to_resolve.map { |discussion| item_for_discussion(discussion) }
end end
def item_for_discussion(discussion) def item_for_discussion(discussion)
first_note = discussion.first_note_to_resolve first_note = discussion.first_note_to_resolve || discussion.first_note
other_note_count = discussion.notes.size - 1 other_note_count = discussion.notes.size - 1
creation_time = first_note.created_at.to_s(:medium)
note_url = Gitlab::UrlBuilder.build(first_note) note_url = Gitlab::UrlBuilder.build(first_note)
discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): " discussion_info = "- [ ] #{first_note.author.to_reference} commented on a [discussion](#{note_url}): "
discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0 discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0
note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call
quote = ">>>\n#{note_without_block_quotes}\n>>>" spaces = ' ' * 4
quote = note_without_block_quotes.lines.map { |line| "#{spaces}> #{line}" }.join
[discussion_info, quote].join("\n\n") [discussion_info, quote].join("\n\n")
end end
def issue_params def issue_params
@issue_params ||= issue_params_with_info_from_merge_request.merge(whitelisted_issue_params) @issue_params ||= issue_params_with_info_from_discussions.merge(whitelisted_issue_params)
end end
def whitelisted_issue_params def whitelisted_issue_params
......
module Issues module Issues
class CreateService < Issues::BaseService class CreateService < Issues::BaseService
include SpamCheckService include SpamCheckService
include ResolveDiscussions
def execute def execute
filter_spam_check_params @issue = BuildService.new(project, current_user, params).execute
issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) filter_spam_check_params
@issue = BuildService.new(project, current_user, issue_attributes).execute filter_resolve_discussion_params
create(@issue) create(@issue)
end end
...@@ -21,17 +22,16 @@ module Issues ...@@ -21,17 +22,16 @@ module Issues
notification_service.new_issue(issuable, current_user) notification_service.new_issue(issuable, current_user)
todo_service.new_issue(issuable, current_user) todo_service.new_issue(issuable, current_user)
user_agent_detail_service.create user_agent_detail_service.create
resolve_discussions_with_issue(issuable)
if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user)
resolve_discussions_in_merge_request(issuable)
end
end end
def resolve_discussions_in_merge_request(issue) def resolve_discussions_with_issue(issue)
return if discussions_to_resolve.empty?
Discussions::ResolveService.new(project, current_user, Discussions::ResolveService.new(project, current_user,
merge_request: merge_request_for_resolving_discussions, merge_request: merge_request_to_resolve_discussions_of,
follow_up_issue: issue). follow_up_issue: issue).
execute(merge_request_for_resolving_discussions.resolvable_discussions) execute(discussions_to_resolve)
end end
private private
......
...@@ -482,7 +482,7 @@ class NotificationService ...@@ -482,7 +482,7 @@ class NotificationService
end end
users = users.to_a.compact.uniq users = users.to_a.compact.uniq
users = users.reject(&:blocked?) users = users.select { |u| u.can?(:receive_notifications) }
users.reject do |user| users.reject do |user|
global_notification_setting = user.global_notification_setting global_notification_setting = user.global_notification_setting
......
...@@ -21,6 +21,8 @@ module Tags ...@@ -21,6 +21,8 @@ module Tags
else else
error('Failed to remove tag') error('Failed to remove tag')
end end
rescue GitHooksService::PreReceiveError => ex
error(ex.message)
end end
def error(message, return_code = 400) def error(message, return_code = 400)
......
...@@ -36,7 +36,8 @@ class NamespaceValidator < ActiveModel::EachValidator ...@@ -36,7 +36,8 @@ class NamespaceValidator < ActiveModel::EachValidator
].freeze ].freeze
WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file].freeze preview blob blame raw files create_dir find_file
artifacts graphs refs badges].freeze
STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze
......
...@@ -26,4 +26,4 @@ ...@@ -26,4 +26,4 @@
.form-actions .form-actions
= f.submit 'Submit', class: "btn btn-save wide" = f.submit 'Submit', class: "btn btn-save wide"
= link_to "Cancel", admin_applications_path, class: "btn btn-default" = link_to "Cancel", admin_applications_path, class: "btn btn-cancel"
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
= @user.name = @user.name
- if @user.blocked? - if @user.blocked?
%span.cred (Blocked) %span.cred (Blocked)
- if @user.internal?
%span.cred (Internal)
- if @user.admin - if @user.admin
%span.cred (Admin) %span.cred (Admin)
- if @user.auditor - if @user.auditor
%span.cred (Auditor) %span.cred (Auditor)
.pull-right .pull-right
- unless @user == current_user || @user.blocked? - if @user != current_user && @user.can?(:log_in)
= link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info" = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
= link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do = link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o %i.fa.fa-pencil-square-o
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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