Commit 996e8024 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into members-ui

parents a0eaff14 dcfda304
...@@ -207,9 +207,7 @@ rubocop: *exec ...@@ -207,9 +207,7 @@ rubocop: *exec
rake haml_lint: *exec rake haml_lint: *exec
rake scss_lint: *exec rake scss_lint: *exec
rake brakeman: *exec rake brakeman: *exec
rake flay: rake flay: *exec
<<: *exec
allow_failure: yes
license_finder: *exec license_finder: *exec
rake downtime_check: *exec rake downtime_check: *exec
......
...@@ -453,6 +453,10 @@ Style/VariableName: ...@@ -453,6 +453,10 @@ Style/VariableName:
EnforcedStyle: snake_case EnforcedStyle: snake_case
Enabled: true Enabled: true
# Use the configured style when numbering variables.
Style/VariableNumber:
Enabled: false
# Use when x then ... for one-line cases. # Use when x then ... for one-line cases.
Style/WhenThen: Style/WhenThen:
Enabled: true Enabled: true
......
This diff is collapsed.
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased) v 8.13.0 (unreleased)
- Update runner version only when updating contacted_at
- Add link from system note to compare with previous version - Add link from system note to compare with previous version
- Use gitlab-shell v3.6.2 (GIT TRACE logging) - Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Fix centering of custom header logos (Ashley Dumaine) - Fix centering of custom header logos (Ashley Dumaine)
...@@ -8,29 +9,44 @@ v 8.13.0 (unreleased) ...@@ -8,29 +9,44 @@ v 8.13.0 (unreleased)
- Replaced the check sign to arrow in the show build view. !6501 - Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Speed-up group milestones show page - Speed-up group milestones show page
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps) - Add more tests for calendar contribution (ClemMakesApps)
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
- API: Multi-file commit !6096 (mahcsig)
- Revert "Label list shows all issues (opened or closed) with that label"
- Expose expires_at field when sharing project on API - Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API - Allow the Koding integration to be configured through the API
- Added soft wrap button to repository file/blob editor - Added soft wrap button to repository file/blob editor
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps)
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix that manual jobs would no longer block jobs in the next stage. !6604
- Add configurable email subject suffix (Fu Xu) - Add configurable email subject suffix (Fu Xu)
- Use a ConnectionPool for Rails.cache on Sidekiq servers - Use a ConnectionPool for Rails.cache on Sidekiq servers
- Replace `alias_method_chain` with `Module#prepend` - Replace `alias_method_chain` with `Module#prepend`
- Enable GitLab Import/Export for non-admin users. - Enable GitLab Import/Export for non-admin users.
- Preserve label filters when sorting !6136 (Joseph Frazier) - Preserve label filters when sorting !6136 (Joseph Frazier)
- MergeRequest#new form load diff asynchronously
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496 - Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Prevent flash alert text from being obscured when container is fluid
- Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree - Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts? - Revert avoid touching file system on Build#artifacts?
- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
- Add broadcast messages and alerts below sub-nav - Add broadcast messages and alerts below sub-nav
- Better empty state for Groups view - Better empty state for Groups view
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
...@@ -40,17 +56,35 @@ v 8.13.0 (unreleased) ...@@ -40,17 +56,35 @@ v 8.13.0 (unreleased)
- Optimize GitHub importing for speed and memory - Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar) - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- Notify the Merger about merge after successful build (Dimitris Karakasilis) - Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Reduce queries needed to find users using their SSH keys when pushing commits
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- Fix broken repository 500 errors in project list - Fix broken repository 500 errors in project list
- Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone) - Close todos when accepting merge requests via the API !6486 (tonygambone)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer) - Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
v 8.12.4 (unreleased) - Grouped pipeline dropdown is a scrollable container
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. (lukehowell)
v 8.12.5 (unreleased)
v 8.12.4
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
- Fix padding in build sidebar. !6506
- Changed compare dropdowns to dropdowns with isolated search input. !6550
- Fix race condition on LFS Token. !6592
- Fix type mismatch bug when closing Jira issue. !6619
- Fix lint-doc error. !6623
- Skip wiki creation when GitHub project has wiki enabled. !6665
- Fix issues importing services via Import/Export. !6667
- Restrict failed login attempts for users with 2FA enabled. !6668
- Fix failed project deletion when feature visibility set to private. !6688
- Prevent claiming associated model IDs via import.
- Set GitLab project exported file permissions to owner only
v 8.12.3 v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves - Update Gitlab Shell to support low IO priority for storage moves
v 8.12.2 (unreleased) v 8.12.2
- Fix Import/Export not recognising correctly the imported services. - Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination - Fix snippets pagination
- Fix "Create project" button layout when visibility options are restricted - Fix "Create project" button layout when visibility options are restricted
...@@ -64,6 +98,7 @@ v 8.12.2 (unreleased) ...@@ -64,6 +98,7 @@ v 8.12.2 (unreleased)
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv) - Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
- Fix resolve discussion buttons endpoint path - Fix resolve discussion buttons endpoint path
- Refactor remnants of CoffeeScript destructured opts and super !6261
v 8.12.1 v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
...@@ -104,6 +139,7 @@ v 8.12.0 ...@@ -104,6 +139,7 @@ v 8.12.0
- Fix long comments in diffs messing with table width - Fix long comments in diffs messing with table width
- Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman) - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page - Fix pagination on user snippets page
- Honor "fixed layout" preference in more places !6422
- Run CI builds with the permissions of users !5735 - Run CI builds with the permissions of users !5735
- Fix sorting of issues in API - Fix sorting of issues in API
- Fix download artifacts button links !6407 - Fix download artifacts button links !6407
...@@ -144,6 +180,7 @@ v 8.12.0 ...@@ -144,6 +180,7 @@ v 8.12.0
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
- Add textarea autoresize after comment (ClemMakesApps) - Add textarea autoresize after comment (ClemMakesApps)
- Do not write SSH public key 'comments' to authorized_keys !6381 - Do not write SSH public key 'comments' to authorized_keys !6381
- Add due date to issue todos
- Refresh todos count cache when an Issue/MR is deleted - Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps) - Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions - Hides merge request button on branches page is user doesn't have permissions
......
...@@ -130,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0' ...@@ -130,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0'
gem 'after_commit_queue', '~> 1.3.0' gem 'after_commit_queue', '~> 1.3.0'
# Issue tags # Issue tags
gem 'acts-as-taggable-on', '~> 3.4' gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 4.2' gem 'sidekiq', '~> 4.2'
...@@ -231,7 +231,7 @@ gem 'net-ssh', '~> 3.0.1' ...@@ -231,7 +231,7 @@ gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0' gem 'base32', '~> 0.3.0'
# Sentry integration # Sentry integration
gem 'sentry-raven', '~> 1.1.0' gem 'sentry-raven', '~> 2.0.0'
gem 'premailer-rails', '~> 1.9.0' gem 'premailer-rails', '~> 1.9.0'
...@@ -295,7 +295,7 @@ group :development, :test do ...@@ -295,7 +295,7 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.42.0', require: false gem 'rubocop', '~> 0.43.0', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false gem 'haml_lint', '~> 0.18.2', require: false
......
...@@ -44,8 +44,8 @@ GEM ...@@ -44,8 +44,8 @@ GEM
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (3.5.0) acts-as-taggable-on (4.0.0)
activerecord (>= 3.2, < 5) activerecord (>= 4.0)
addressable (2.3.8) addressable (2.3.8)
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
...@@ -487,7 +487,7 @@ GEM ...@@ -487,7 +487,7 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
paranoia (2.1.4) paranoia (2.1.4)
activerecord (~> 4.0) activerecord (~> 4.0)
parser (2.3.1.2) parser (2.3.1.4)
ast (~> 2.2) ast (~> 2.2)
pg (0.18.4) pg (0.18.4)
pkg-config (1.1.7) pkg-config (1.1.7)
...@@ -620,7 +620,7 @@ GEM ...@@ -620,7 +620,7 @@ GEM
rspec-retry (0.4.5) rspec-retry (0.4.5)
rspec-core rspec-core
rspec-support (3.5.0) rspec-support (3.5.0)
rubocop (0.42.0) rubocop (0.43.0)
parser (>= 2.3.1.1, < 3.0) parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0) rainbow (>= 1.99.1, < 3.0)
...@@ -665,8 +665,8 @@ GEM ...@@ -665,8 +665,8 @@ GEM
activesupport (>= 3.1) activesupport (>= 3.1)
select2-rails (3.5.9.3) select2-rails (3.5.9.3)
thor (~> 0.14) thor (~> 0.14)
sentry-raven (1.1.0) sentry-raven (2.0.2)
faraday (>= 0.7.6) faraday (>= 0.7.6, < 0.10.x)
settingslogic (2.0.9) settingslogic (2.0.9)
sexp_processor (4.7.0) sexp_processor (4.7.0)
sham_rack (1.3.6) sham_rack (1.3.6)
...@@ -802,7 +802,7 @@ DEPENDENCIES ...@@ -802,7 +802,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2) RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0) ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0) activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4) acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8) addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0) after_commit_queue (~> 1.3.0)
akismet (~> 2.0) akismet (~> 2.0)
...@@ -938,7 +938,7 @@ DEPENDENCIES ...@@ -938,7 +938,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0) rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rubocop (~> 0.42.0) rubocop (~> 0.43.0)
rubocop-rspec (~> 1.5.0) rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
...@@ -948,7 +948,7 @@ DEPENDENCIES ...@@ -948,7 +948,7 @@ DEPENDENCIES
sdoc (~> 0.3.20) sdoc (~> 0.3.20)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
sentry-raven (~> 1.1.0) sentry-raven (~> 2.0.0)
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
...@@ -986,4 +986,4 @@ DEPENDENCIES ...@@ -986,4 +986,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.13.1 1.13.2
(function() { ((global) => {
this.LabelManager = (function() {
LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
function LabelManager(opts) { class LabelManager {
// Defaults constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
var ref, ref1, ref2; this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
if (opts == null) { this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
opts = {}; this.otherLabels = otherLabels || $('.js-other-labels');
} this.errorMessage = 'Unable to update label prioritization at this time';
this.togglePriorityButton = (ref = opts.togglePriorityButton) != null ? ref : $('.js-toggle-priority'), this.prioritizedLabels = (ref1 = opts.prioritizedLabels) != null ? ref1 : $('.js-prioritized-labels'), this.otherLabels = (ref2 = opts.otherLabels) != null ? ref2 : $('.js-other-labels');
this.prioritizedLabels.sortable({ this.prioritizedLabels.sortable({
items: 'li', items: 'li',
placeholder: 'list-placeholder', placeholder: 'list-placeholder',
...@@ -18,33 +15,30 @@ ...@@ -18,33 +15,30 @@
this.bindEvents(); this.bindEvents();
} }
LabelManager.prototype.bindEvents = function() { bindEvents() {
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
}; }
LabelManager.prototype.onTogglePriorityClick = function(e) { onTogglePriorityClick(e) {
var $btn, $label, $tooltip, _this, action;
e.preventDefault(); e.preventDefault();
_this = e.data; const _this = e.data;
$btn = $(e.currentTarget); const $btn = $(e.currentTarget);
$label = $("#" + ($btn.data('domId'))); const $label = $(`#${$btn.data('domId')}`);
action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
// Make sure tooltip will hide const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
$tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby')));
$tooltip.tooltip('destroy'); $tooltip.tooltip('destroy');
return _this.toggleLabelPriority($label, action); return _this.toggleLabelPriority($label, action);
}; }
LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) { toggleLabelPriority($label, action, persistState) {
var $from, $target, _this, url, xhr;
if (persistState == null) { if (persistState == null) {
persistState = true; persistState = true;
} }
_this = this; let xhr;
url = $label.find('.js-toggle-priority').data('url'); const _this = this;
$target = this.prioritizedLabels; const url = $label.find('.js-toggle-priority').data('url');
$from = this.otherLabels; let $target = this.prioritizedLabels;
// Optimistic update let $from = this.otherLabels;
if (action === 'remove') { if (action === 'remove') {
$target = this.otherLabels; $target = this.otherLabels;
$from = this.prioritizedLabels; $from = this.prioritizedLabels;
...@@ -62,7 +56,7 @@ ...@@ -62,7 +56,7 @@
} }
if (action === 'remove') { if (action === 'remove') {
xhr = $.ajax({ xhr = $.ajax({
url: url, url,
type: 'DELETE' type: 'DELETE'
}); });
// Restore empty message // Restore empty message
...@@ -73,43 +67,40 @@ ...@@ -73,43 +67,40 @@
xhr = this.savePrioritySort($label, action); xhr = this.savePrioritySort($label, action);
} }
return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
}; }
LabelManager.prototype.onPrioritySortUpdate = function() { onPrioritySortUpdate() {
var xhr; const xhr = this.savePrioritySort();
xhr = this.savePrioritySort();
return xhr.fail(function() { return xhr.fail(function() {
return new Flash(this.errorMessage, 'alert'); return new Flash(this.errorMessage, 'alert');
}); });
}; }
LabelManager.prototype.savePrioritySort = function() { savePrioritySort() {
return $.post({ return $.post({
url: this.prioritizedLabels.data('url'), url: this.prioritizedLabels.data('url'),
data: { data: {
label_ids: this.getSortedLabelsIds() label_ids: this.getSortedLabelsIds()
} }
}); });
}; }
LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) { rollbackLabelPosition($label, originalAction) {
var action; const action = originalAction === 'remove' ? 'add' : 'remove';
action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false); this.toggleLabelPriority($label, action, false);
return new Flash(this.errorMessage, 'alert'); return new Flash(this.errorMessage, 'alert');
}; }
LabelManager.prototype.getSortedLabelsIds = function() { getSortedLabelsIds() {
var sortedIds; const sortedIds = [];
sortedIds = [];
this.prioritizedLabels.find('li').each(function() { this.prioritizedLabels.find('li').each(function() {
return sortedIds.push($(this).data('id')); sortedIds.push($(this).data('id'));
}); });
return sortedIds; return sortedIds;
}; }
}
return LabelManager; gl.LabelManager = LabelManager;
})(); })(window.gl || (window.gl = {}));
}).call(this);
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
namespacesPath: "/api/:version/namespaces.json", namespacesPath: "/api/:version/namespaces.json",
groupProjectsPath: "/api/:version/groups/:id/projects.json", groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true", projectsPath: "/api/:version/projects.json?simple=true",
labelsPath: "/api/:version/projects/:id/labels", labelsPath: "/:namespace_path/:project_path/labels",
licensePath: "/api/:version/licenses/:key", licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key", gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
...@@ -23,12 +23,13 @@ ...@@ -23,12 +23,13 @@
}, },
// Return groups list. Filtered by query // Return groups list. Filtered by query
// Only active groups retrieved // Only active groups retrieved
groups: function(query, skip_ldap, callback) { groups: function(query, skip_ldap, skip_groups, callback) {
var url = Api.buildUrl(Api.groupsPath); var url = Api.buildUrl(Api.groupsPath);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
search: query, search: query,
skip_groups: skip_groups,
per_page: 20 per_page: 20
}, },
dataType: "json" dataType: "json"
...@@ -65,13 +66,14 @@ ...@@ -65,13 +66,14 @@
return callback(projects); return callback(projects);
}); });
}, },
newLabel: function(project_id, data, callback) { newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath) var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id); .replace(':namespace_path', namespace_path)
.replace(':project_path', project_path);
return $.ajax({ return $.ajax({
url: url, url: url,
type: "POST", type: "POST",
data: data, data: {'label': data},
dataType: "json" dataType: "json"
}).done(function(label) { }).done(function(label) {
return callback(label); return callback(label);
......
/*= require blob/template_selector */
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
this.BlobCiYamlSelector = (function(superClass) {
extend(BlobCiYamlSelector, superClass);
function BlobCiYamlSelector() {
return BlobCiYamlSelector.__super__.constructor.apply(this, arguments);
}
BlobCiYamlSelector.prototype.requestFile = function(query) {
return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
};
return BlobCiYamlSelector;
})(TemplateSelector);
this.BlobCiYamlSelectors = (function() {
function BlobCiYamlSelectors(opts) {
var ref;
this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitlab-ci-yml-selector'), this.editor = opts.editor;
this.$dropdowns.each((function(_this) {
return function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return new BlobCiYamlSelector({
pattern: /(.gitlab-ci.yml)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
dropdown: $dropdown,
editor: _this.editor
});
};
})(this));
}
return BlobCiYamlSelectors;
})();
}).call(this);
/*= require blob/template_selector */
((global) => {
class BlobCiYamlSelector extends gl.TemplateSelector {
requestFile(query) {
return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
}
requestFileSuccess(file) {
return super.requestFileSuccess(file);
}
}
global.BlobCiYamlSelector = BlobCiYamlSelector;
class BlobCiYamlSelectors {
constructor({ editor, $dropdowns } = {}) {
this.editor = editor;
this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
this.initSelectors();
}
initSelectors() {
const editor = this.editor;
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobCiYamlSelector({
editor,
pattern: /(.gitlab-ci.yml)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
dropdown: $dropdown
});
});
}
}
global.BlobCiYamlSelectors = BlobCiYamlSelectors;
})(window.gl || (window.gl = {}));
...@@ -18,6 +18,6 @@ ...@@ -18,6 +18,6 @@
return BlobGitignoreSelector; return BlobGitignoreSelector;
})(TemplateSelector); })(gl.TemplateSelector);
}).call(this); }).call(this);
...@@ -23,6 +23,6 @@ ...@@ -23,6 +23,6 @@
return BlobLicenseSelector; return BlobLicenseSelector;
})(TemplateSelector); })(gl.TemplateSelector);
}).call(this); }).call(this);
(function() {
this.BlobLicenseSelectors = (function() {
function BlobLicenseSelectors(opts) {
var ref;
this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-license-selector'), this.editor = opts.editor;
this.$dropdowns.each((function(_this) {
return function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return new BlobLicenseSelector({
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
editor: _this.editor
});
};
})(this));
}
return BlobLicenseSelectors;
})();
}).call(this);
((global) => {
class BlobLicenseSelectors {
constructor({ $dropdowns, editor }) {
this.$dropdowns = $('.js-license-selector');
this.editor = editor;
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobLicenseSelector({
editor,
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
global.BlobLicenseSelectors = BlobLicenseSelectors;
})(window.gl || (window.gl = {}));
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.TemplateSelector = (function() {
function TemplateSelector(opts) {
var ref;
if (opts == null) {
opts = {};
}
this.onClick = bind(this.onClick, this);
this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
this.autosizeUpdateEvent = document.createEvent('Event');
this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
}
TemplateSelector.prototype.buildDropdown = function() {
return this.dropdown.glDropdown({
data: this.data,
filterable: true,
selectable: true,
toggleLabel: this.toggleLabel,
search: {
fields: ['name']
},
clicked: this.onClick,
text: function(item) {
return item.name;
}
});
};
TemplateSelector.prototype.bindEvents = function() {
return this.$input.on('keyup blur', (function(_this) {
return function(e) {
return _this.onFilenameUpdate();
};
})(this));
};
TemplateSelector.prototype.toggleLabel = function(item) {
return item.name;
};
TemplateSelector.prototype.onFilenameUpdate = function() {
var filenameMatches;
if (!this.$input.length) {
return;
}
filenameMatches = this.pattern.test(this.$input.val().trim());
if (!filenameMatches) {
this.wrapper.addClass('hidden');
return;
}
return this.wrapper.removeClass('hidden');
};
TemplateSelector.prototype.onClick = function(item, el, e) {
e.preventDefault();
return this.requestFile(item);
};
TemplateSelector.prototype.requestFile = function(item) {
// This `requestFile` method is an abstract method that should
// be added by all subclasses.
};
// To be implemented on the extending class
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1);
if (!skipFocus) this.editor.focus();
if (this.editor instanceof jQuery) {
this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
}
};
TemplateSelector.prototype.startLoadingSpinner = function() {
this.dropdownIcon
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
};
TemplateSelector.prototype.stopLoadingSpinner = function() {
this.dropdownIcon
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
};
return TemplateSelector;
})();
}).call(this);
((global) => {
class TemplateSelector {
constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
this.onClick = this.onClick.bind(this);
this.dropdown = dropdown;
this.data = data;
this.pattern = pattern;
this.wrapper = wrapper;
this.editor = editor;
this.fileEndpoint = fileEndpoint;
this.$input = $input || $('#file_name');
this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
this.autosizeUpdateEvent = document.createEvent('Event');
this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
}
buildDropdown() {
return this.dropdown.glDropdown({
data: this.data,
filterable: true,
selectable: true,
toggleLabel: this.toggleLabel,
search: {
fields: ['name']
},
clicked: this.onClick,
text: function(item) {
return item.name;
}
});
}
bindEvents() {
return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
}
toggleLabel(item) {
return item.name;
}
onFilenameUpdate() {
var filenameMatches;
if (!this.$input.length) {
return;
}
filenameMatches = this.pattern.test(this.$input.val().trim());
if (!filenameMatches) {
this.wrapper.addClass('hidden');
return;
}
return this.wrapper.removeClass('hidden');
}
onClick(item, el, e) {
e.preventDefault();
return this.requestFile(item);
}
requestFile(item) {
// This `requestFile` method is an abstract method that should
// be added by all subclasses.
}
// To be implemented on the extending class
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess(file, { skipFocus, append } = {}) {
const oldValue = this.editor.getValue();
let newValue = file.content;
if (append && oldValue.length && oldValue !== newValue) {
newValue = oldValue + '\n\n' + newValue;
}
this.editor.setValue(newValue, 1);
if (!skipFocus) this.editor.focus();
if (this.editor instanceof jQuery) {
this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
}
}
startLoadingSpinner() {
this.dropdownIcon
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
}
stopLoadingSpinner() {
this.dropdownIcon
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
}
}
global.TemplateSelector = TemplateSelector;
})(window.gl || ( window.gl = {}));
...@@ -23,13 +23,13 @@ ...@@ -23,13 +23,13 @@
})(this)); })(this));
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap(); this.initSoftWrap();
new BlobLicenseSelectors({ new gl.BlobLicenseSelectors({
editor: this.editor editor: this.editor
}); });
new BlobGitignoreSelectors({ new BlobGitignoreSelectors({
editor: this.editor editor: this.editor
}); });
new BlobCiYamlSelectors({ new gl.BlobCiYamlSelectors({
editor: this.editor editor: this.editor
}); });
} }
......
...@@ -3,8 +3,7 @@ $(() => { ...@@ -3,8 +3,7 @@ $(() => {
$('.js-new-board-list').each(function () { $('.js-new-board-list').each(function () {
const $this = $(this); const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
$this.glDropdown({ $this.glDropdown({
data(term, callback) { data(term, callback) {
......
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
$date = $('.js-artifacts-remove'); $date = $('.js-artifacts-remove');
if ($date.length) { if ($date.length) {
date = $date.text(); date = $date.text();
return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' ')); return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
} }
}; };
......
(function (w) { (function (w) {
class CreateLabelDropdown { class CreateLabelDropdown {
constructor ($el, projectId) { constructor ($el, namespacePath, projectPath) {
this.$el = $el; this.$el = $el;
this.projectId = projectId; this.namespacePath = namespacePath;
this.projectPath = projectPath;
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown')); this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
this.$cancelButton = $('.js-cancel-label-btn', this.$el); this.$cancelButton = $('.js-cancel-label-btn', this.$el);
this.$newLabelField = $('#new_label_name', this.$el); this.$newLabelField = $('#new_label_name', this.$el);
...@@ -91,8 +92,8 @@ ...@@ -91,8 +92,8 @@
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
Api.newLabel(this.projectId, { Api.newLabel(this.namespacePath, this.projectPath, {
name: this.$newLabelField.val(), title: this.$newLabelField.val(),
color: this.$newColorField.val() color: this.$newColorField.val()
}, (label) => { }, (label) => {
this.$newLabelCreateButton.enable(); this.$newLabelCreateButton.enable();
......
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
function Diff() { function Diff() {
$('.files .diff-file').singleFileDiff(); $('.files .diff-file').singleFileDiff();
this.filesCommentButton = $('.files .diff-file').filesCommentButton(); this.filesCommentButton = $('.files .diff-file').filesCommentButton();
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$(document).off('click', '.js-unfold'); $(document).off('click', '.js-unfold');
$(document).on('click', '.js-unfold', (function(_this) { $(document).on('click', '.js-unfold', (function(_this) {
return function(event) { return function(event) {
...@@ -52,6 +55,10 @@ ...@@ -52,6 +55,10 @@
})(this)); })(this));
} }
Diff.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
Diff.prototype.lineNumbers = function(line) { Diff.prototype.lineNumbers = function(line) {
if (!line.children().length) { if (!line.children().length) {
return [0, 0]; return [0, 0];
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
Issuable.init(); Issuable.init();
new IssuableBulkActions(); new gl.IssuableBulkActions();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
break; break;
case 'projects:issues:show': case 'projects:issues:show':
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
new Milestone(); new Milestone();
break; break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new Todos(); new gl.Todos();
break; break;
case 'projects:milestones:new': case 'projects:milestones:new':
case 'projects:milestones:edit': case 'projects:milestones:edit':
...@@ -59,7 +59,9 @@ ...@@ -59,7 +59,9 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.issue-form')); new GLForm($('.issue-form'));
new IssuableForm($('.issue-form')); new IssuableForm($('.issue-form'));
new IssuableTemplateSelectors(); new LabelsSelect();
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
break; break;
case 'projects:merge_requests:new': case 'projects:merge_requests:new':
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
...@@ -67,7 +69,9 @@ ...@@ -67,7 +69,9 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form')); new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form')); new IssuableForm($('.merge-request-form'));
new IssuableTemplateSelectors(); new LabelsSelect();
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
break; break;
case 'projects:tags:new': case 'projects:tags:new':
new ZenMode(); new ZenMode();
...@@ -165,7 +169,7 @@ ...@@ -165,7 +169,7 @@
break; break;
case 'projects:labels:index': case 'projects:labels:index':
if ($('.prioritized-labels').length) { if ($('.prioritized-labels').length) {
new LabelManager(); new gl.LabelManager();
} }
break; break;
case 'projects:network:show': case 'projects:network:show':
...@@ -279,7 +283,7 @@ ...@@ -279,7 +283,7 @@
Dispatcher.prototype.initSearch = function() { Dispatcher.prototype.initSearch = function() {
// Only when search form is present // Only when search form is present
if ($('.search').length) { if ($('.search').length) {
return new SearchAutocomplete(); return new gl.SearchAutocomplete();
} }
}; };
......
...@@ -443,6 +443,7 @@ ...@@ -443,6 +443,7 @@
var contentHtml; var contentHtml;
this.resetRows(); this.resetRows();
this.addArrowKeyEvent(); this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) { if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this); this.options.setIndeterminateIds.call(this);
} }
...@@ -460,9 +461,21 @@ ...@@ -460,9 +461,21 @@
if (this.options.filterable) { if (this.options.filterable) {
this.filterInput.focus(); this.filterInput.focus();
} }
if (this.options.showMenuAbove) {
this.positionMenuAbove();
}
return this.dropdown.trigger('shown.gl.dropdown'); 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) { GitLabDropdown.prototype.hidden = function(e) {
var $input; var $input;
this.resetRows(); this.resetRows();
......
...@@ -5,14 +5,15 @@ ...@@ -5,14 +5,15 @@
function GroupsSelect() { function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) { $('.ajax-groups-select').each((function(_this) {
return function(i, select) { return function(i, select) {
var skip_ldap; var skip_ldap, skip_groups;
skip_ldap = $(select).hasClass('skip_ldap'); skip_ldap = $(select).hasClass('skip_ldap');
skip_groups = $(select).data('skip-groups') || [];
return $(select).select2({ return $(select).select2({
placeholder: "Search for a group", placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'), multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0, minimumInputLength: 0,
query: function(query) { query: function(query) {
return Api.groups(query.term, skip_ldap, function(groups) { return Api.groups(query.term, skip_ldap, skip_groups, function(groups) {
var data; var data;
data = { data = {
results: groups results: groups
......
...@@ -51,7 +51,6 @@ ...@@ -51,7 +51,6 @@
}).remove(); }).remove();
// Submit the form to get new data // Submit the form to get new data
Issuable.filterResults($('.filter-form')); Issuable.filterResults($('.filter-form'));
return $('.js-label-select').trigger('update.label');
}); });
}, },
filterResults: (function(_this) { filterResults: (function(_this) {
......
(function() { ((global) => {
this.IssuableBulkActions = (function() {
function IssuableBulkActions(opts) { class IssuableBulkActions {
// Set defaults constructor({ container, form, issues } = {}) {
var ref, ref1, ref2; this.container = container || $('.content'),
if (opts == null) { this.form = form || this.getElement('.bulk-update');
opts = {}; this.issues = issues || this.getElement('.issues-list .issue');
}
this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li');
// Save instance
this.form.data('bulkActions', this); this.form.data('bulkActions', this);
this.willUpdateLabels = false; this.willUpdateLabels = false;
this.bindEvents(); this.bindEvents();
...@@ -15,53 +12,46 @@ ...@@ -15,53 +12,46 @@
Issuable.initChecks(); Issuable.initChecks();
} }
IssuableBulkActions.prototype.getElement = function(selector) { getElement(selector) {
return this.container.find(selector); return this.container.find(selector);
}; }
IssuableBulkActions.prototype.bindEvents = function() { bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}; }
IssuableBulkActions.prototype.onFormSubmit = function(e) { onFormSubmit(e) {
e.preventDefault(); e.preventDefault();
return this.submit(); return this.submit();
}; }
IssuableBulkActions.prototype.submit = function() { submit() {
var _this, xhr; const _this = this;
_this = this; const xhr = $.ajax({
xhr = $.ajax({
url: this.form.attr('action'), url: this.form.attr('action'),
method: this.form.attr('method'), method: this.form.attr('method'),
dataType: 'JSON', dataType: 'JSON',
data: this.getFormDataAsObject() data: this.getFormDataAsObject()
}); });
xhr.done(function(response, status, xhr) { xhr.done(() => window.location.reload());
return location.reload(); xhr.fail(() => new Flash("Issue update failed"));
});
xhr.fail(function() {
return new Flash("Issue update failed");
});
return xhr.always(this.onFormSubmitAlways.bind(this)); return xhr.always(this.onFormSubmitAlways.bind(this));
}; }
IssuableBulkActions.prototype.onFormSubmitAlways = function() { onFormSubmitAlways() {
return this.form.find('[type="submit"]').enable(); return this.form.find('[type="submit"]').enable();
}; }
IssuableBulkActions.prototype.getSelectedIssues = function() { getSelectedIssues() {
return this.issues.has('.selected_issue:checked'); return this.issues.has('.selected_issue:checked');
}; }
IssuableBulkActions.prototype.getLabelsFromSelection = function() { getLabelsFromSelection() {
var labels; const labels = [];
labels = [];
this.getSelectedIssues().map(function() { this.getSelectedIssues().map(function() {
var _labels; const labelsData = $(this).data('labels');
_labels = $(this).data('labels'); if (labelsData) {
if (_labels) { return labelsData.map(function(labelId) {
return _labels.map(function(labelId) {
if (labels.indexOf(labelId) === -1) { if (labels.indexOf(labelId) === -1) {
return labels.push(labelId); return labels.push(labelId);
} }
...@@ -69,7 +59,7 @@ ...@@ -69,7 +59,7 @@
} }
}); });
return labels; return labels;
}; }
/** /**
...@@ -77,25 +67,21 @@ ...@@ -77,25 +67,21 @@
* @return {Array} Label IDs * @return {Array} Label IDs
*/ */
IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() { getUnmarkedIndeterminedLabels() {
var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result; const result = [];
result = []; const labelsToKeep = [];
labelsToKeep = [];
ref = this.getElement('.labels-filter .is-indeterminate'); this.getElement('.labels-filter .is-indeterminate')
for (i = 0, len = ref.length; i < len; i++) { .each((i, el) => labelsToKeep.push($(el).data('labelId')));
el = ref[i];
labelsToKeep.push($(el).data('labelId')); this.getLabelsFromSelection().forEach((id) => {
}
ref1 = this.getLabelsFromSelection();
for (j = 0, len1 = ref1.length; j < len1; j++) {
id = ref1[j];
// Only the ones that we are not going to keep
if (labelsToKeep.indexOf(id) === -1) { if (labelsToKeep.indexOf(id) === -1) {
result.push(id); result.push(id);
} }
} });
return result; return result;
}; }
/** /**
...@@ -103,9 +89,8 @@ ...@@ -103,9 +89,8 @@
* Returns key/value pairs from form data * Returns key/value pairs from form data
*/ */
IssuableBulkActions.prototype.getFormDataAsObject = function() { getFormDataAsObject() {
var formData; const formData = {
formData = {
update: { update: {
state_event: this.form.find('input[name="update[state_event]"]').val(), state_event: this.form.find('input[name="update[state_event]"]').val(),
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
...@@ -125,19 +110,18 @@ ...@@ -125,19 +110,18 @@
}); });
} }
return formData; return formData;
}; }
IssuableBulkActions.prototype.getLabelsToApply = function() { getLabelsToApply() {
var $labels, labelIds; const labelIds = [];
labelIds = []; const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels.each(function(k, label) { $labels.each(function(k, label) {
if (label) { if (label) {
return labelIds.push(parseInt($(label).val())); return labelIds.push(parseInt($(label).val()));
} }
}); });
return labelIds; return labelIds;
}; }
/** /**
...@@ -145,11 +129,10 @@ ...@@ -145,11 +129,10 @@
* @return {Array} Array of labels IDs * @return {Array} Array of labels IDs
*/ */
IssuableBulkActions.prototype.getLabelsToRemove = function() { getLabelsToRemove() {
var indeterminatedLabels, labelsToApply, result; const result = [];
result = []; const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); const labelsToApply = this.getLabelsToApply();
labelsToApply = this.getLabelsToApply();
indeterminatedLabels.map(function(id) { indeterminatedLabels.map(function(id) {
// We need to exclude label IDs that will be applied // We need to exclude label IDs that will be applied
// By not doing this will cause issues from selection to not add labels at all // By not doing this will cause issues from selection to not add labels at all
...@@ -158,10 +141,9 @@ ...@@ -158,10 +141,9 @@
} }
}); });
return result; return result;
}; }
}
return IssuableBulkActions;
})(); global.IssuableBulkActions = IssuableBulkActions;
}).call(this); })(window.gl || (window.gl = {}));
This diff is collapsed.
...@@ -38,6 +38,11 @@ ...@@ -38,6 +38,11 @@
gl.utils.getPagePath = function() { gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0]; return $('body').data('page').split(':')[0];
}; };
gl.utils.parseUrl = function (url) {
var parser = document.createElement('a');
parser.href = url;
return parser;
};
return jQuery.timefor = function(time, suffix, expiredLabel) { return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor; var suffixFromNow, timefor;
if (!time) { if (!time) {
......
...@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs'; ...@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs';
class MergeConflictDataProvider { class MergeConflictDataProvider {
getInitialData() { getInitialData() {
// TODO: remove reliance on jQuery and DOM state introspection
const diffViewType = $.cookie('diff_view'); const diffViewType = $.cookie('diff_view');
const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
return { return {
isLoading : true, isLoading : true,
hasError : false, hasError : false,
isParallel : diffViewType === 'parallel', isParallel : diffViewType === 'parallel',
diffViewType : diffViewType, diffViewType : diffViewType,
fixedLayout : fixedLayout,
isSubmitting : false, isSubmitting : false,
conflictsData : {}, conflictsData : {},
resolutionData : {} resolutionData : {}
...@@ -192,14 +195,17 @@ class MergeConflictDataProvider { ...@@ -192,14 +195,17 @@ class MergeConflictDataProvider {
updateViewType(newType) { updateViewType(newType) {
const vi = this.vueInstance; const vi = this.vueInstance;
if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) { if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
return; return;
} }
vi.diffView = newType; vi.diffViewType = newType;
vi.isParallel = newType === 'parallel'; vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType); // TODO: Make sure that cookie path added. $.cookie('diff_view', newType, {
$('.content-wrapper .container-fluid').toggleClass('container-limited'); path: (gon && gon.relative_url_root) || '/'
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
} }
......
...@@ -60,9 +60,8 @@ class MergeConflictResolver { ...@@ -60,9 +60,8 @@ class MergeConflictResolver {
$('#conflicts .js-syntax-highlight').syntaxHighlight(); $('#conflicts .js-syntax-highlight').syntaxHighlight();
}); });
if (this.vue.diffViewType === 'parallel') { $('.content-wrapper .container-fluid')
$('.content-wrapper .container-fluid').removeClass('container-limited'); .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
}
}) })
} }
......
...@@ -36,13 +36,10 @@ ...@@ -36,13 +36,10 @@
}; };
MergeRequest.prototype.initTabs = function() { MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') { if (window.mrTabs) {
// `MergeRequests#new` has no tab-persisting or lazy-loading behavior window.mrTabs.unbindEvents();
window.mrTabs = new MergeRequestTabs(this.opts);
} else {
// Show the first tab (Commits)
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
} }
window.mrTabs = new MergeRequestTabs(this.opts);
}; };
MergeRequest.prototype.showAllCommits = function() { MergeRequest.prototype.showAllCommits = function() {
......
...@@ -56,9 +56,14 @@ ...@@ -56,9 +56,14 @@
MergeRequestTabs.prototype.commitsLoaded = false; MergeRequestTabs.prototype.commitsLoaded = false;
MergeRequestTabs.prototype.fixedLayoutPref = null;
function MergeRequestTabs(opts) { function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {}; this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
this.buildsLoaded = this.opts.buildsLoaded || false;
this.setCurrentAction = bind(this.setCurrentAction, this); this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this); this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this); this.showTab = bind(this.showTab, this);
...@@ -70,7 +75,12 @@ ...@@ -70,7 +75,12 @@
MergeRequestTabs.prototype.bindEvents = function() { MergeRequestTabs.prototype.bindEvents = function() {
$(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
return $(document).on('click', '.js-show-tab', this.showTab); $(document).on('click', '.js-show-tab', this.showTab);
};
MergeRequestTabs.prototype.unbindEvents = function() {
$(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
$(document).off('click', '.js-show-tab', this.showTab);
}; };
MergeRequestTabs.prototype.showTab = function(event) { MergeRequestTabs.prototype.showTab = function(event) {
...@@ -85,11 +95,15 @@ ...@@ -85,11 +95,15 @@
if (action === 'commits') { if (action === 'commits') {
this.loadCommits($target.attr('href')); this.loadCommits($target.attr('href'));
this.expandView(); this.expandView();
} else if (action === 'diffs') { this.resetViewContainer();
} else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView(); this.shrinkView();
} }
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
navBarHeight = $('.navbar-gitlab').outerHeight(); navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo(".merge-request-details .merge-request-tabs", { $.scrollTo(".merge-request-details .merge-request-tabs", {
offset: -navBarHeight offset: -navBarHeight
...@@ -97,11 +111,14 @@ ...@@ -97,11 +111,14 @@
} else if (action === 'builds') { } else if (action === 'builds') {
this.loadBuilds($target.attr('href')); this.loadBuilds($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
this.loadPipelines($target.attr('href')); this.loadPipelines($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer();
} }
if (this.opts.setUrl) { if (this.opts.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
...@@ -126,7 +143,7 @@ ...@@ -126,7 +143,7 @@
if (action === 'show') { if (action === 'show') {
action = 'notes'; action = 'notes';
} }
return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
}; };
// Replaces the current Merge Request-specific action in the URL with a new one // Replaces the current Merge Request-specific action in the URL with a new one
...@@ -156,8 +173,9 @@ ...@@ -156,8 +173,9 @@
action = 'notes'; action = 'notes';
} }
this.currentAction = action; this.currentAction = action;
// Remove a trailing '/commits' or '/diffs' // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, ''); new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes' // Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') { if (action !== 'notes') {
new_state += "/" + action; new_state += "/" + action;
...@@ -196,8 +214,13 @@ ...@@ -196,8 +214,13 @@
if (this.diffsLoaded) { if (this.diffsLoaded) {
return; return;
} }
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
var url = gl.utils.parseUrl(source);
return this._get({ return this._get({
url: (source + ".json") + this._location.search, url: (url.pathname + ".json") + this._location.search,
success: (function(_this) { success: (function(_this) {
return function(data) { return function(data) {
$('#diffs').html(data.html); $('#diffs').html(data.html);
...@@ -209,7 +232,7 @@ ...@@ -209,7 +232,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff(); $('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel') { if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer(); _this.expandViewContainer();
} }
_this.diffsLoaded = true; _this.diffsLoaded = true;
...@@ -308,11 +331,25 @@ ...@@ -308,11 +331,25 @@
MergeRequestTabs.prototype.diffViewType = function() { MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
// Returns diff view type };
MergeRequestTabs.prototype.isDiffAction = function(action) {
return action === 'diffs' || action === 'new/diffs'
}; };
MergeRequestTabs.prototype.expandViewContainer = function() { MergeRequestTabs.prototype.expandViewContainer = function() {
return $('.container-fluid').removeClass('container-limited'); var $wrapper = $('.content-wrapper .container-fluid');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
$wrapper.removeClass('container-limited');
};
MergeRequestTabs.prototype.resetViewContainer = function() {
if (this.fixedLayoutPref !== null) {
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', this.fixedLayoutPref);
}
}; };
MergeRequestTabs.prototype.shrinkView = function() { MergeRequestTabs.prototype.shrinkView = function() {
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
this.currentProject = JSON.parse(currentProject); this.currentProject = JSON.parse(currentProject);
} }
$('.js-milestone-select').each(function(i, dropdown) { $('.js-milestone-select').each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId; var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
projectId = $dropdown.data('project-id'); projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones'); milestonesUrl = $dropdown.data('milestones');
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
selectedMilestone = $dropdown.data('selected'); selectedMilestone = $dropdown.data('selected');
showNo = $dropdown.data('show-no'); showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any'); showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming'); showUpcoming = $dropdown.data('show-upcoming');
useId = $dropdown.data('use-id'); useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label'); defaultLabel = $dropdown.data('default-label');
...@@ -31,12 +32,12 @@ ...@@ -31,12 +32,12 @@
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
} }
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
return $.ajax({ return $.ajax({
url: milestonesUrl url: milestonesUrl
}).done(function(data) { }).done(function(data) {
var extraOptions; var extraOptions = [];
extraOptions = [];
if (showAny) { if (showAny) {
extraOptions.push({ extraOptions.push({
id: 0, id: 0,
...@@ -58,10 +59,14 @@ ...@@ -58,10 +59,14 @@
title: 'Upcoming' title: 'Upcoming'
}); });
} }
if (extraOptions.length > 2) { if (extraOptions.length) {
extraOptions.push('divider'); extraOptions.push('divider');
} }
return callback(extraOptions.concat(data));
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
}); });
}, },
filterable: true, filterable: true,
...@@ -69,19 +74,20 @@ ...@@ -69,19 +74,20 @@
fields: ['title'] fields: ['title']
}, },
selectable: true, selectable: true,
toggleLabel: function(selected) { toggleLabel: function(selected, el, e) {
if (selected && 'id' in selected) { if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title; return selected.title;
} else { } else {
return defaultLabel; return defaultLabel;
} }
}, },
defaultLabel: defaultLabel,
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
text: function(milestone) { text: function(milestone) {
return _.escape(milestone.title); return _.escape(milestone.title);
}, },
id: function(milestone) { id: function(milestone) {
if (!useId) { if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name; return milestone.name;
} else { } else {
return milestone.id; return milestone.id;
...@@ -100,7 +106,8 @@ ...@@ -100,7 +106,8 @@
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
if ($dropdown.hasClass('js-filter-bulk-update')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return; return;
} }
if (page === 'projects:boards:show') { if (page === 'projects:boards:show') {
......
(function() { ((global) => {
var GitLabCrop,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
GitLabCrop = (function() { // Matches everything but the file name
var FILENAMEREGEX; const FILENAMEREGEX = /^.*[\\\/]/;
// Matches everything but the file name class GitLabCrop {
FILENAMEREGEX = /^.*[\\\/]/; constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
function GitLabCrop(input, opts) { this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
var ref, ref1, ref2, ref3, ref4; this.onModalHide = this.onModalHide.bind(this);
if (opts == null) { this.onModalShow = this.onModalShow.bind(this);
opts = {}; this.onPickImageClick = this.onPickImageClick.bind(this);
}
this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this);
this.onModalHide = bind(this.onModalHide, this);
this.onModalShow = bind(this.onModalShow, this);
this.onPickImageClick = bind(this.onPickImageClick, this);
this.fileInput = $(input); this.fileInput = $(input);
// We should rename to avoid spec to fail
// Form will submit the proper input filed with a file using FormData
this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger");
// Set defaults
this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg;
// Required params
// Ensure needed elements are jquery objects
// If selector is provided we will convert them to a jQuery Object
this.filename = this.getElement(this.filename);
this.previewImage = this.getElement(this.previewImage);
this.pickImageEl = this.getElement(this.pickImageEl);
// Modal elements usually are outside the @form element
this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop;
this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn;
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`);
this.exportWidth = exportWidth;
this.exportHeight = exportHeight;
this.cropBoxWidth = cropBoxWidth;
this.cropBoxHeight = cropBoxHeight;
this.form = this.fileInput.parents('form');
this.filename = filename;
this.previewImage = previewImage;
this.modalCrop = modalCrop;
this.pickImageEl = pickImageEl;
this.uploadImageBtn = uploadImageBtn;
this.modalCropImg = modalCropImg;
this.filename = this.getElement(filename);
this.previewImage = this.getElement(previewImage);
this.pickImageEl = this.getElement(pickImageEl);
this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.cropActionsBtn = this.modalCrop.find('[data-method]'); this.cropActionsBtn = this.modalCrop.find('[data-method]');
this.bindEvents(); this.bindEvents();
} }
GitLabCrop.prototype.getElement = function(selector) { getElement(selector) {
return $(selector, this.form); return $(selector, this.form);
}; }
GitLabCrop.prototype.bindEvents = function() { bindEvents() {
var _this; var _this;
_this = this; _this = this;
this.fileInput.on('change', function(e) { this.fileInput.on('change', function(e) {
...@@ -57,13 +55,13 @@ ...@@ -57,13 +55,13 @@
return _this.onActionBtnClick(btn); return _this.onActionBtnClick(btn);
}); });
return this.croppedImageBlob = null; return this.croppedImageBlob = null;
}; }
GitLabCrop.prototype.onPickImageClick = function() { onPickImageClick() {
return this.fileInput.trigger('click'); return this.fileInput.trigger('click');
}; }
GitLabCrop.prototype.onModalShow = function() { onModalShow() {
var _this; var _this;
_this = this; _this = this;
return this.modalCropImg.cropper({ return this.modalCropImg.cropper({
...@@ -95,44 +93,44 @@ ...@@ -95,44 +93,44 @@
}); });
} }
}); });
}; }
GitLabCrop.prototype.onModalHide = function() { onModalHide() {
return this.modalCropImg.attr('src', '').cropper('destroy'); return this.modalCropImg.attr('src', '').cropper('destroy');
}; }
GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image onUploadImageBtnClick(e) {
e.preventDefault(); // Destroy cropper instance e.preventDefault();
this.setBlob(); this.setBlob();
this.setPreview(); this.setPreview();
this.modalCrop.modal('hide'); this.modalCrop.modal('hide');
return this.fileInput.val(''); return this.fileInput.val('');
}; }
GitLabCrop.prototype.onActionBtnClick = function(btn) { onActionBtnClick(btn) {
var data, result; var data, result;
data = $(btn).data(); data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) { if (this.modalCropImg.data('cropper') && data.method) {
return result = this.modalCropImg.cropper(data.method, data.option); return result = this.modalCropImg.cropper(data.method, data.option);
} }
}; }
GitLabCrop.prototype.onFileInputChange = function(e, input) { onFileInputChange(e, input) {
return this.readFile(input); return this.readFile(input);
}; }
GitLabCrop.prototype.readFile = function(input) { readFile(input) {
var _this, reader; var _this, reader;
_this = this; _this = this;
reader = new FileReader; reader = new FileReader;
reader.onload = function() { reader.onload = () => {
_this.modalCropImg.attr('src', reader.result); _this.modalCropImg.attr('src', reader.result);
return _this.modalCrop.modal('show'); return _this.modalCrop.modal('show');
}; };
return reader.readAsDataURL(input.files[0]); return reader.readAsDataURL(input.files[0]);
}; }
GitLabCrop.prototype.dataURLtoBlob = function(dataURL) { dataURLtoBlob(dataURL) {
var array, binary, i, k, len, v; var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]); binary = atob(dataURL.split(',')[1]);
array = []; array = [];
...@@ -143,35 +141,32 @@ ...@@ -143,35 +141,32 @@
return new Blob([new Uint8Array(array)], { return new Blob([new Uint8Array(array)], {
type: 'image/png' type: 'image/png'
}); });
}; }
GitLabCrop.prototype.setPreview = function() { setPreview() {
var filename; var filename;
this.previewImage.attr('src', this.dataURL); this.previewImage.attr('src', this.dataURL);
filename = this.fileInput.val().replace(FILENAMEREGEX, ''); filename = this.fileInput.val().replace(FILENAMEREGEX, '');
return this.filename.text(filename); return this.filename.text(filename);
}; }
GitLabCrop.prototype.setBlob = function() { setBlob() {
this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
width: 200, width: 200,
height: 200 height: 200
}).toDataURL('image/png'); }).toDataURL('image/png');
return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
}; }
GitLabCrop.prototype.getBlob = function() { getBlob() {
return this.croppedImageBlob; return this.croppedImageBlob;
}; }
}
return GitLabCrop;
})();
$.fn.glCrop = function(opts) { $.fn.glCrop = function(opts) {
return this.each(function() { return this.each(function() {
return $(this).data('glcrop', new GitLabCrop(this, opts)); return $(this).data('glcrop', new GitLabCrop(this, opts));
}); });
}; }
}).call(this); })(window.gl || (window.gl = {}));
(function() { ((global) => {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.Profile = (function() { class Profile {
function Profile(opts) { constructor({ form } = {}) {
var cropOpts, ref; this.onSubmitForm = this.onSubmitForm.bind(this);
if (opts == null) { this.form = form || $('.edit-user');
opts = {};
}
this.onSubmitForm = bind(this.onSubmitForm, this);
this.form = (ref = opts.form) != null ? ref : $('.edit-user');
$('.js-preferences-form').on('change.preference', 'input[type=radio]', function() {
return $(this).parents('form').submit();
// Automatically submit the Preferences form when any of its radio buttons change
});
$('#user_notification_email').on('change', function() {
return $(this).parents('form').submit();
// Automatically submit email form when it changes
});
$('.update-username').on('ajax:before', function() {
$('.loading-username').show();
$(this).find('.update-success').hide();
return $(this).find('.update-failed').hide();
});
$('.update-username').on('ajax:complete', function() {
$('.loading-username').hide();
$(this).find('.btn-save').enable();
return $(this).find('.loading-gif').hide();
});
$('.update-notifications').on('ajax:success', function(e, data) {
if (data.saved) {
return new Flash("Notification settings saved", "notice");
} else {
return new Flash("Failed to save new settings", "alert");
}
});
this.bindEvents(); this.bindEvents();
cropOpts = { this.initAvatarGlCrop();
}
initAvatarGlCrop() {
const cropOpts = {
filename: '.js-avatar-filename', filename: '.js-avatar-filename',
previewImage: '.avatar-image .avatar', previewImage: '.avatar-image .avatar',
modalCrop: '.modal-profile-crop', modalCrop: '.modal-profile-crop',
...@@ -46,23 +20,51 @@ ...@@ -46,23 +20,51 @@
this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
} }
Profile.prototype.bindEvents = function() { bindEvents() {
return this.form.on('submit', this.onSubmitForm); $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
}; $('#user_notification_email').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
this.form.on('submit', this.onSubmitForm);
}
submitForm() {
return $(this).parents('form').submit();
}
Profile.prototype.onSubmitForm = function(e) { onSubmitForm(e) {
e.preventDefault(); e.preventDefault();
return this.saveForm(); return this.saveForm();
}; }
beforeUpdateUsername() {
$('.loading-username').show();
$(this).find('.update-success').hide();
return $(this).find('.update-failed').hide();
}
afterUpdateUsername() {
$('.loading-username').hide();
$(this).find('.btn-save').enable();
return $(this).find('.loading-gif').hide();
}
onUpdateNotifs(e, data) {
return data.saved ?
new Flash("Notification settings saved", "notice") :
new Flash("Failed to save new settings", "alert");
}
saveForm() {
const self = this;
const formData = new FormData(this.form[0]);
const avatarBlob = this.avatarGlCrop.getBlob();
Profile.prototype.saveForm = function() {
var avatarBlob, formData, self;
self = this;
formData = new FormData(this.form[0]);
avatarBlob = this.avatarGlCrop.getBlob();
if (avatarBlob != null) { if (avatarBlob != null) {
formData.append('user[avatar]', avatarBlob, 'avatar.png'); formData.append('user[avatar]', avatarBlob, 'avatar.png');
} }
return $.ajax({ return $.ajax({
url: this.form.attr('action'), url: this.form.attr('action'),
type: this.form.attr('method'), type: this.form.attr('method'),
...@@ -70,37 +72,29 @@ ...@@ -70,37 +72,29 @@
dataType: "json", dataType: "json",
processData: false, processData: false,
contentType: false, contentType: false,
success: function(response) { success: response => new Flash(response.message, 'notice'),
return new Flash(response.message, 'notice'); error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
}, complete: () => {
error: function(jqXHR) {
return new Flash(jqXHR.responseJSON.message, 'alert');
},
complete: function() {
window.scrollTo(0, 0); window.scrollTo(0, 0);
// Enable submit button after requests ends // Enable submit button after requests ends
return self.form.find(':input[disabled]').enable(); return self.form.find(':input[disabled]').enable();
} }
}); });
}; }
}
return Profile;
})();
$(function() { $(function() {
$(document).on('focusout.ssh_key', '#key_key', function() { $(document).on('focusout.ssh_key', '#key_key', function() {
var $title, comment; const $title = $('#key_title');
$title = $('#key_title'); const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
if (comment && comment.length > 1 && $title.val() === '') { if (comment && comment.length > 1 && $title.val() === '') {
return $title.val(comment[1]).change(); return $title.val(comment[1]).change();
} }
// Extract the SSH Key title from its comment // Extract the SSH Key title from its comment
}); });
if (gl.utils.getPagePath() === 'profiles') { if (global.utils.getPagePath() === 'profiles') {
return new Profile(); return new Profile();
} }
}); });
}).call(this); })(window.gl || (window.gl = {}));
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
data = groups.concat(projects); data = groups.concat(projects);
return finalCallback(data); return finalCallback(data);
}; };
return Api.groups(term, false, groupsCallback); return Api.groups(term, false, false, groupsCallback);
}; };
} else { } else {
projectsCallback = finalCallback; projectsCallback = finalCallback;
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
data = groups.concat(projects); data = groups.concat(projects);
return finalCallback(data); return finalCallback(data);
}; };
return Api.groups(query.term, false, groupsCallback); return Api.groups(query.term, false, false, groupsCallback);
}; };
} else { } else {
projectsCallback = finalCallback; projectsCallback = finalCallback;
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
filterable: true, filterable: true,
fieldName: 'group_id', fieldName: 'group_id',
data: function(term, callback) { data: function(term, callback) {
return Api.groups(term, null, function(data) { return Api.groups(term, false, false, function(data) {
data.unshift({ data.unshift({
name: 'Any' name: 'Any'
}); });
......
/*= require ../blob/template_selector */ /*= require ../blob/template_selector */
((global) => { ((global) => {
class IssuableTemplateSelector extends TemplateSelector { class IssuableTemplateSelector extends gl.TemplateSelector {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.projectPath = this.dropdown.data('project-path'); this.projectPath = this.dropdown.data('project-path');
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
if (initialQuery.name) this.requestFile(initialQuery); if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => { $('.reset-template', this.dropdown.parent()).on('click', () => {
if (this.currentTemplate) this.setInputValueToTemplateContent(); if (this.currentTemplate) this.setInputValueToTemplateContent(false);
}); });
} }
...@@ -26,26 +26,28 @@ ...@@ -26,26 +26,28 @@
this.currentTemplate = currentTemplate; this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner(); this.stopLoadingSpinner();
this.setInputValueToTemplateContent(); this.setInputValueToTemplateContent(true);
}); });
return; return;
} }
setInputValueToTemplateContent() { setInputValueToTemplateContent(append) {
// `this.requestFileSuccess` sets the value of the description input field // `this.requestFileSuccess` sets the value of the description input field
// to the content of the template selected. // to the content of the template selected. If `append` is true, the
// template content will be appended to the previous value of the field,
// separated by a blank line if the previous value is non-empty.
if (this.titleInput.val() === '') { if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and // If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the 2nd // skip focusing the description input by setting `true` as the
// argument to `requestFileSuccess`. // `skipFocus` option to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, true); this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
this.titleInput.focus(); this.titleInput.focus();
} else { } else {
this.requestFileSuccess(this.currentTemplate); this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
} }
return; return;
} }
} }
global.IssuableTemplateSelector = IssuableTemplateSelector; global.IssuableTemplateSelector = IssuableTemplateSelector;
})(window); })(window.gl || (window.gl = {}));
((global) => { ((global) => {
class IssuableTemplateSelectors { class IssuableTemplateSelectors {
constructor(opts = {}) { constructor({ $dropdowns, editor } = {}) {
this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector'); this.$dropdowns = $dropdowns || $('.js-issuable-selector');
this.editor = opts.editor || this.initEditor(); this.editor = editor || this.initEditor();
this.$dropdowns.each((i, dropdown) => { this.$dropdowns.each((i, dropdown) => {
let $dropdown = $(dropdown); const $dropdown = $(dropdown);
new IssuableTemplateSelector({ new gl.IssuableTemplateSelector({
pattern: /(\.md)/, pattern: /(\.md)/,
data: $dropdown.data('data'), data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-issuable-selector-wrap'), wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
...@@ -26,4 +26,4 @@ ...@@ -26,4 +26,4 @@
} }
global.IssuableTemplateSelectors = IssuableTemplateSelectors; global.IssuableTemplateSelectors = IssuableTemplateSelectors;
})(window); })(window.gl || (window.gl = {}));
(function() { ((global) => {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
class Todos {
this.Todos = (function() { constructor({ el } = {}) {
function Todos(opts) { this.allDoneClicked = this.allDoneClicked.bind(this);
var ref; this.doneClicked = this.doneClicked.bind(this);
if (opts == null) { this.el = el || $('.js-todos-options');
opts = {};
}
this.allDoneClicked = bind(this.allDoneClicked, this);
this.doneClicked = bind(this.doneClicked, this);
this.el = (ref = opts.el) != null ? ref : $('.js-todos-options');
this.perPage = this.el.data('perPage'); this.perPage = this.el.data('perPage');
this.clearListeners(); this.clearListeners();
this.initBtnListeners(); this.initBtnListeners();
this.initFilters(); this.initFilters();
} }
Todos.prototype.clearListeners = function() { clearListeners() {
$('.done-todo').off('click'); $('.done-todo').off('click');
$('.js-todos-mark-all').off('click'); $('.js-todos-mark-all').off('click');
return $('.todo').off('click'); return $('.todo').off('click');
}; }
Todos.prototype.initBtnListeners = function() { initBtnListeners() {
$('.done-todo').on('click', this.doneClicked); $('.done-todo').on('click', this.doneClicked);
$('.js-todos-mark-all').on('click', this.allDoneClicked); $('.js-todos-mark-all').on('click', this.allDoneClicked);
return $('.todo').on('click', this.goToTodoUrl); return $('.todo').on('click', this.goToTodoUrl);
}; }
Todos.prototype.initFilters = function() { initFilters() {
new UsersSelect(); 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');
...@@ -38,125 +33,117 @@ ...@@ -38,125 +33,117 @@
event.preventDefault(); event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize()); Turbolinks.visit(this.action + '&' + $(this).serialize());
}); });
}; }
Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) { initFilterDropdown($dropdown, fieldName, searchFields) {
$dropdown.glDropdown({ $dropdown.glDropdown({
fieldName,
selectable: true, selectable: true,
filterable: searchFields ? true : false, filterable: searchFields ? true : false,
fieldName: fieldName,
search: { fields: searchFields }, search: { fields: searchFields },
data: $dropdown.data('data'), data: $dropdown.data('data'),
clicked: function() { clicked: function() {
return $dropdown.closest('form.filter-form').submit(); return $dropdown.closest('form.filter-form').submit();
} }
}) })
}; }
Todos.prototype.doneClicked = function(e) { doneClicked(e) {
var $this;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
$this = $(e.currentTarget); const $target = $(e.currentTarget);
$this.disable(); $target.disable();
return $.ajax({ return $.ajax({
type: 'POST', type: 'POST',
url: $this.attr('href'), url: $target.attr('href'),
dataType: 'json', dataType: 'json',
data: { data: {
'_method': 'delete' '_method': 'delete'
}, },
success: (function(_this) { success: (data) => {
return function(data) { this.redirectIfNeeded(data.count);
_this.redirectIfNeeded(data.count); this.clearDone($target.closest('li'));
_this.clearDone($this.closest('li')); return this.updateBadges(data);
return _this.updateBadges(data); }
};
})(this)
}); });
}; }
Todos.prototype.allDoneClicked = function(e) { allDoneClicked(e) {
var $this;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
$this = $(e.currentTarget); $target = $(e.currentTarget);
$this.disable(); $target.disable();
return $.ajax({ return $.ajax({
type: 'POST', type: 'POST',
url: $this.attr('href'), url: $target.attr('href'),
dataType: 'json', dataType: 'json',
data: { data: {
'_method': 'delete' '_method': 'delete'
}, },
success: (function(_this) { success: (data) => {
return function(data) { $target.remove();
$this.remove(); $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
$('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); return this.updateBadges(data);
return _this.updateBadges(data); }
};
})(this)
}); });
}; }
Todos.prototype.clearDone = function($row) { clearDone($row) {
var $ul; const $ul = $row.closest('ul');
$ul = $row.closest('ul');
$row.remove(); $row.remove();
if (!$ul.find('li').length) { if (!$ul.find('li').length) {
return $ul.parents('.panel').remove(); return $ul.parents('.panel').remove();
} }
}; }
Todos.prototype.updateBadges = function(data) { updateBadges(data) {
$('.todos-pending .badge, .todos-pending-count').text(data.count); $('.todos-pending .badge, .todos-pending-count').text(data.count);
return $('.todos-done .badge').text(data.done_count); return $('.todos-done .badge').text(data.done_count);
}; }
Todos.prototype.getTotalPages = function() { getTotalPages() {
return this.el.data('totalPages'); return this.el.data('totalPages');
}; }
Todos.prototype.getCurrentPage = function() { getCurrentPage() {
return this.el.data('currentPage'); return this.el.data('currentPage');
}; }
Todos.prototype.getTodosPerPage = function() { getTodosPerPage() {
return this.el.data('perPage'); return this.el.data('perPage');
}; }
redirectIfNeeded(total) {
const currPages = this.getTotalPages();
const currPage = this.getCurrentPage();
Todos.prototype.redirectIfNeeded = function(total) {
var currPage, currPages, newPages, pageParams, url;
currPages = this.getTotalPages();
currPage = this.getCurrentPage();
// Refresh if no remaining Todos // Refresh if no remaining Todos
if (!total) { if (!total) {
location.reload(); window.location.reload();
return; return;
} }
// Do nothing if no pagination // Do nothing if no pagination
if (!currPages) { if (!currPages) {
return; return;
} }
newPages = Math.ceil(total / this.getTodosPerPage());
// Includes query strings const newPages = Math.ceil(total / this.getTodosPerPage());
url = location.href; let url = location.href;
// If new total of pages is different than we have now
if (newPages !== currPages) { if (newPages !== currPages) {
// Redirect to previous page if there's one available // Redirect to previous page if there's one available
if (currPages > 1 && currPage === currPages) { if (currPages > 1 && currPage === currPages) {
pageParams = { const pageParams = {
page: currPages - 1 page: currPages - 1
}; };
url = gl.utils.mergeUrlParams(pageParams, url); url = gl.utils.mergeUrlParams(pageParams, url);
} }
return Turbolinks.visit(url); return Turbolinks.visit(url);
} }
}; }
Todos.prototype.goToTodoUrl = function(e) { goToTodoUrl(e) {
var todoLink; const todoLink = $(this).data('url');
todoLink = $(this).data('url');
if (!todoLink) { if (!todoLink) {
return; return;
} }
...@@ -167,10 +154,8 @@ ...@@ -167,10 +154,8 @@
} else { } else {
return Turbolinks.visit(todoLink); return Turbolinks.visit(todoLink);
} }
}; }
}
return Todos;
})();
}).call(this); global.Todos = Todos;
})(window.gl || (window.gl = {}));
(global => { ((global) => {
global.User = class { global.User = class {
constructor(opts) { constructor({ action }) {
this.opts = opts; this.action = action;
this.placeProfileAvatarsToTop(); this.placeProfileAvatarsToTop();
this.initTabs(); this.initTabs();
this.hideProjectLimitMessage(); this.hideProjectLimitMessage();
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
} }
initTabs() { initTabs() {
return new UserTabs({ return new global.UserTabs({
parentEl: '.user-profile', parentEl: '.user-profile',
action: this.opts.action action: this.action
}); });
} }
......
// UserTabs
//
// Handles persisting and restoring the current tab selection and lazily-loading
// content on the Users#show page.
//
// ### Example Markup
//
// <ul class="nav-links">
// <li class="activity-tab active">
// <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
// Activity
// </a>
// </li>
// <li class="groups-tab">
// <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
// Groups
// </a>
// </li>
// <li class="contributed-tab">
// <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
// Contributed projects
// </a>
// </li>
// <li class="projects-tab">
// <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
// Personal projects
// </a>
// </li>
// <li class="snippets-tab">
// <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
// </a>
// </li>
// </ul>
//
// <div class="tab-content">
// <div class="tab-pane" id="activity">
// Activity Content
// </div>
// <div class="tab-pane" id="groups">
// Groups Content
// </div>
// <div class="tab-pane" id="contributed">
// Contributed projects content
// </div>
// <div class="tab-pane" id="projects">
// Projects content
// </div>
// <div class="tab-pane" id="snippets">
// Snippets content
// </div>
// </div>
//
// <div class="loading-status">
// <div class="loading">
// Loading Animation
// </div>
// </div>
//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.UserTabs = (function() {
function UserTabs(opts) {
this.tabShown = bind(this.tabShown, this);
var i, item, len, ref, ref1, ref2, ref3;
this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document);
// Make jQuery object if selector is provided
if (typeof this.parentEl === 'string') {
this.parentEl = $(this.parentEl);
}
// Store the `location` object, allowing for easier stubbing in tests
this._location = location;
// Set tab states
this.loaded = {};
ref3 = this.parentEl.find('.nav-links a');
for (i = 0, len = ref3.length; i < len; i++) {
item = ref3[i];
this.loaded[$(item).attr('data-action')] = false;
}
// Actions
this.actions = Object.keys(this.loaded);
this.bindEvents();
// Set active tab
if (this.action === 'show') {
this.action = this.defaultAction;
}
this.activateTab(this.action);
}
UserTabs.prototype.bindEvents = function() {
// Toggle event listeners
return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown);
};
UserTabs.prototype.tabShown = function(event) {
var $target, action, source;
$target = $(event.target);
action = $target.data('action');
source = $target.attr('href');
this.setTab(source, action);
return this.setCurrentAction(action);
};
UserTabs.prototype.activateTab = function(action) {
return this.parentEl.find(".nav-links .js-" + action + "-tab a").tab('show');
};
UserTabs.prototype.setTab = function(source, action) {
if (this.loaded[action] === true) {
return;
}
if (action === 'activity') {
this.loadActivities(source);
}
if (action === 'groups' || action === 'contributed' || action === 'projects' || action === 'snippets') {
return this.loadTab(source, action);
}
};
UserTabs.prototype.loadTab = function(source, action) {
return $.ajax({
beforeSend: (function(_this) {
return function() {
return _this.toggleLoading(true);
};
})(this),
complete: (function(_this) {
return function() {
return _this.toggleLoading(false);
};
})(this),
dataType: 'json',
type: 'GET',
url: source + ".json",
success: (function(_this) {
return function(data) {
var tabSelector;
tabSelector = 'div#' + action;
_this.parentEl.find(tabSelector).html(data.html);
_this.loaded[action] = true;
// Fix tooltips
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
};
})(this)
});
};
UserTabs.prototype.loadActivities = function(source) {
var $calendarWrap;
if (this.loaded['activity'] === true) {
return;
}
$calendarWrap = this.parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
new Activities();
return this.loaded['activity'] = true;
};
UserTabs.prototype.toggleLoading = function(status) {
return this.parentEl.find('.loading-status .loading').toggle(status);
};
UserTabs.prototype.setCurrentAction = function(action) {
var new_state, regExp;
// Remove possible actions from URL
regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$');
new_state = this._location.pathname;
// remove trailing slashes
new_state = new_state.replace(/\/+$/, "");
new_state = new_state.replace(regExp, '');
// Append the new action if we're on a tab other than 'activity'
if (action !== this.defaultAction) {
new_state += "/" + action;
}
// Ensure parameters and hash come along for the ride
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
url: new_state
}, document.title, new_state);
return new_state;
};
return UserTabs;
})();
}).call(this);
/*
UserTabs
Handles persisting and restoring the current tab selection and lazily-loading
content on the Users#show page.
### Example Markup
<ul class="nav-links">
<li class="activity-tab active">
<a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
Activity
</a>
</li>
<li class="groups-tab">
<a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
Groups
</a>
</li>
<li class="contributed-tab">
<a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
Contributed projects
</a>
</li>
<li class="projects-tab">
<a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
Personal projects
</a>
</li>
<li class="snippets-tab">
<a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="activity">
Activity Content
</div>
<div class="tab-pane" id="groups">
Groups Content
</div>
<div class="tab-pane" id="contributed">
Contributed projects content
</div>
<div class="tab-pane" id="projects">
Projects content
</div>
<div class="tab-pane" id="snippets">
Snippets content
</div>
</div>
<div class="loading-status">
<div class="loading">
Loading Animation
</div>
</div>
*/
((global) => {
class UserTabs {
constructor ({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this._location = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
});
this.actions = Object.keys(this.loaded);
this.bindEvents();
if (this.action === 'show') {
this.action = this.defaultAction;
}
this.activateTab(this.action);
}
bindEvents() {
return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
}
tabShown(event) {
const $target = $(event.target);
const action = $target.data('action');
const source = $target.attr('href');
this.setTab(source, action);
return this.setCurrentAction(action);
}
activateTab(action) {
return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
.tab('show');
}
setTab(source, action) {
if (this.loaded[action]) {
return;
}
if (action === 'activity') {
this.loadActivities(source);
}
const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ];
if (loadableActions.indexOf(action) > -1) {
return this.loadTab(source, action);
}
}
loadTab(source, action) {
return $.ajax({
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
url: `${source}.json`,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
}
});
}
loadActivities(source) {
if (this.loaded['activity']) {
return;
}
const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
new Activities();
return this.loaded['activity'] = true;
}
toggleLoading(status) {
return this.$parentEl.find('.loading-status .loading')
.toggle(status);
}
setCurrentAction(action) {
const regExp = new RegExp(`\/(${this.actions.join('|')})(\.html)?\/?$`);
let new_state = this._location.pathname;
new_state = new_state.replace(/\/+$/, '');
new_state = new_state.replace(regExp, '');
if (action !== this.defaultAction) {
new_state += `/${action}`;
}
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
url: new_state
}, document.title, new_state);
return new_state;
}
}
global.UserTabs = UserTabs;
})(window.gl || (window.gl = {}));
...@@ -14,11 +14,12 @@ ...@@ -14,11 +14,12 @@
$('.js-user-search').each((function(_this) { $('.js-user-search').each((function(_this) {
return function(i, dropdown) { return function(i, dropdown) {
var options = {}; var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id'); options.projectId = $dropdown.data('project-id');
options.showCurrentUser = $dropdown.data('current-user'); options.showCurrentUser = $dropdown.data('current-user');
showNullUser = $dropdown.data('null-user'); showNullUser = $dropdown.data('null-user');
showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user'); showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user'); firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id'); options.authorId = $dropdown.data('author-id');
...@@ -73,6 +74,7 @@ ...@@ -73,6 +74,7 @@
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
var isAuthorFilter; var isAuthorFilter;
isAuthorFilter = $('.js-author-search'); isAuthorFilter = $('.js-author-search');
...@@ -116,8 +118,11 @@ ...@@ -116,8 +118,11 @@
if (showDivider) { if (showDivider) {
users.splice(showDivider, 0, "divider"); users.splice(showDivider, 0, "divider");
} }
// Send the data back
return callback(users); callback(users);
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
}); });
}, },
filterable: true, filterable: true,
...@@ -127,8 +132,8 @@ ...@@ -127,8 +132,8 @@
}, },
selectable: true, selectable: true,
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
toggleLabel: function(selected) { toggleLabel: function(selected, el) {
if (selected && 'id' in selected) { if (selected && 'id' in selected && $(el).hasClass('is-active')) {
if (selected.text) { if (selected.text) {
return selected.text; return selected.text;
} else { } else {
...@@ -138,6 +143,7 @@ ...@@ -138,6 +143,7 @@
return defaultLabel; return defaultLabel;
} }
}, },
defaultLabel: defaultLabel,
inputId: 'issue_assignee_id', inputId: 'issue_assignee_id',
hidden: function(e) { hidden: function(e) {
$selectbox.hide(); $selectbox.hide();
...@@ -149,7 +155,9 @@ ...@@ -149,7 +155,9 @@
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
if ($dropdown.hasClass('js-filter-bulk-update')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
selectedId = user.id;
return; return;
} }
if (page === 'projects:boards:show') { if (page === 'projects:boards:show') {
...@@ -167,6 +175,9 @@ ...@@ -167,6 +175,9 @@
return assignTo(selected); return assignTo(selected);
} }
}, },
id: function (user) {
return user.id;
},
renderRow: function(user) { renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
username = user.username ? "@" + user.username : ""; username = user.username ? "@" + user.username : "";
......
...@@ -604,3 +604,9 @@ ...@@ -604,3 +604,9 @@
display: block; display: block;
color: $gl-placeholder-color; color: $gl-placeholder-color;
} }
.dropdown-toggle-text {
&.is-default {
color: $gl-placeholder-color;
}
}
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
.flash-notice, .flash-alert { .flash-notice, .flash-alert {
border-radius: $border-radius-default; border-radius: $border-radius-default;
.container-fluid.container-limited.flash-text { .container-fluid,
.container-fluid.container-limited {
background: transparent; background: transparent;
} }
} }
...@@ -35,12 +36,6 @@ ...@@ -35,12 +36,6 @@
} }
} }
.content-wrapper {
.flash-notice .container-fluid {
background-color: transparent;
}
}
@media (max-width: $screen-md-min) { @media (max-width: $screen-md-min) {
ul.notes { ul.notes {
.flash-container.timeline-content { .flash-container.timeline-content {
......
...@@ -350,6 +350,10 @@ ...@@ -350,6 +350,10 @@
.issuable-form-select-holder { .issuable-form-select-holder {
display: inline-block; display: inline-block;
width: 250px; width: 250px;
.dropdown-menu-toggle {
width: 100%;
}
} }
.table-holder { .table-holder {
......
...@@ -22,6 +22,11 @@ ...@@ -22,6 +22,11 @@
.table.builds { .table.builds {
min-width: 1200px; min-width: 1200px;
.branch-commit {
width: 33%;
}
} }
} }
...@@ -385,6 +390,8 @@ ...@@ -385,6 +390,8 @@
left: auto; left: auto;
right: -214px; right: -214px;
top: -9px; top: -9px;
max-height: 245px;
overflow-y: scroll;
a:hover { a:hover {
.ci-status-text { .ci-status-text {
......
...@@ -146,7 +146,8 @@ ...@@ -146,7 +146,8 @@
} }
.project-repo-btn-group, .project-repo-btn-group,
.notification-dropdown { .notification-dropdown,
.project-dropdown {
margin-left: 10px; margin-left: 10px;
} }
......
...@@ -51,6 +51,7 @@ ...@@ -51,6 +51,7 @@
-webkit-flex-direction: column; -webkit-flex-direction: column;
flex-direction: column; flex-direction: column;
margin-left: 10px; margin-left: 10px;
min-width: 55px;
} }
.todo-item { .todo-item {
...@@ -120,6 +121,14 @@ ...@@ -120,6 +121,14 @@
} }
} }
@media (max-width: $screen-sm-max) {
.todos-filters {
.dropdown-menu-toggle {
width: 135px;
}
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.todo { .todo {
.avatar { .avatar {
...@@ -141,4 +150,14 @@ ...@@ -141,4 +150,14 @@
padding-left: 10px; padding-left: 10px;
} }
} }
.todos-filters {
.row-content-block {
padding-bottom: 50px;
}
.dropdown-menu-toggle {
width: 100%;
}
}
} }
...@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor ...@@ -23,15 +23,24 @@ 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?
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
setup_u2f_authentication(user) setup_u2f_authentication(user)
render 'devise/sessions/two_factor' render 'devise/sessions/two_factor'
end end
def locked_user_redirect(user)
flash.now[:alert] = 'Invalid Login or password'
render 'devise/sessions/new'
end
def authenticate_with_two_factor def authenticate_with_two_factor
user = self.resource = find_user user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id] if user.access_locked?
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)
...@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor ...@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.' flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor prompt_for_two_factor(user)
end end
end end
...@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor ...@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.' flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end
......
...@@ -15,18 +15,17 @@ module MembershipActions ...@@ -15,18 +15,17 @@ module MembershipActions
end end
def leave def leave
@member = membershipable.members.find_by(user_id: current_user) || member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
membershipable.requesters.find_by(user_id: current_user) execute(:all)
Members::DestroyService.new(@member, current_user).execute
source_type = @member.real_source_type.humanize(capitalize: false) source_type = membershipable.class.to_s.humanize(capitalize: false)
notice = notice =
if @member.request? if member.request?
"Your access request to the #{source_type} has been withdrawn." "Your access request to the #{source_type} has been withdrawn."
else else
"You left the \"#{@member.source.human_name}\" #{source_type}." "You left the \"#{membershipable.human_name}\" #{source_type}."
end end
redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice redirect_to redirect_path, notice: notice
end end
......
...@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def trending def trending
@projects = TrendingProjectsFinder.new.execute(current_user) @projects = TrendingProjectsFinder.new.execute
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.page(params[:page]) @projects = @projects.page(params[:page])
......
...@@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
def destroy def destroy
@group_member = @group.members.find_by(id: params[:id]) || Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
@group.requesters.find_by(id: params[:id])
Members::DestroyService.new(@group_member, current_user).execute
respond_to do |format| respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
......
class Projects::BoardsController < Projects::ApplicationController class Projects::BoardsController < Projects::ApplicationController
include IssuableCollections
respond_to :html respond_to :html
before_action :authorize_read_board!, only: [:show] before_action :authorize_read_board!, only: [:show]
......
...@@ -5,17 +5,25 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -5,17 +5,25 @@ class Projects::GroupLinksController < Projects::ApplicationController
def index def index
@group_links = project.project_group_links.all @group_links = project.project_group_links.all
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << project.group.try(:id)
end end
def create def create
group = Group.find(params[:link_group_id]) group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
return render_404 unless can?(current_user, :read_group, group)
if group
project.project_group_links.create( return render_404 unless can?(current_user, :read_group, group)
group: group,
group_access: params[:link_group_access], project.project_group_links.create(
expires_at: params[:expires_at] group: group,
) group_access: params[:link_group_access],
expires_at: params[:expires_at]
)
else
flash[:alert] = 'Please select a group.'
end
redirect_to namespace_project_group_links_path(project.namespace, project) redirect_to namespace_project_group_links_path(project.namespace, project)
end end
......
...@@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController
@label = @project.labels.create(label_params) @label = @project.labels.create(label_params)
if @label.valid? if @label.valid?
redirect_to namespace_project_labels_path(@project.namespace, @project) respond_to do |format|
format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) }
format.json { render json: @label }
end
else else
render 'new' respond_to do |format|
format.html { render 'new' }
format.json { render json: { message: @label.errors.messages }, status: 400 }
end
end end
end end
......
...@@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_diff_comment_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :apply_diff_view_cookie!, only: [:new_diffs]
before_action :build_merge_request, only: [:new, :new_diffs]
# Allow read any merge_request # Allow read any merge_request
before_action :authorize_read_merge_request! before_action :authorize_read_merge_request!
...@@ -210,29 +212,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -210,29 +212,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def new def new
apply_diff_view_cookie! define_new_vars
end
build_merge_request
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@diffs = @merge_request.diffs(diff_options) if @merge_request.compare
@diff_notes_disabled = true
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)). def new_diffs
group(:commit_id).count respond_to do |format|
format.html do
define_new_vars
render "new"
end
format.json do
@diffs = if @merge_request.can_be_created
@merge_request.diffs(diff_options)
else
[]
end
@diff_notes_disabled = true
render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
end
end
end end
def create def create
...@@ -490,6 +489,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -490,6 +489,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
) )
end end
def define_new_vars
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
end
def invalid_mr def invalid_mr
# Render special view for MR with removed target branch # Render special view for MR with removed target branch
render 'invalid' render 'invalid'
...@@ -521,7 +541,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -521,7 +541,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def build_merge_request def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end end
def compared_diff_version def compared_diff_version
......
...@@ -59,10 +59,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -59,10 +59,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
def destroy def destroy
@project_member = @project.members.find_by(id: params[:id]) || Members::DestroyService.new(@project, current_user, params).
@project.requesters.find_by(id: params[:id]) execute(:all)
Members::DestroyService.new(@project_member, current_user).execute
respond_to do |format| respond_to do |format|
format.html do format.html do
......
# Finder for retrieving public trending projects in a given time range.
class TrendingProjectsFinder class TrendingProjectsFinder
def execute(current_user, start_date = 1.month.ago) # current_user - The currently logged in User, if any.
projects_for(current_user).trending(start_date) # last_months - The number of months to limit the trending data to.
def execute(months_limit = 1)
Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do
Project.public_only.trending(months_limit.months.ago)
end
end end
private private
def projects_for(current_user) def cache_key_for(months)
ProjectsFinder.new.execute(current_user) "trending_projects/#{months}"
end end
end end
...@@ -40,8 +40,9 @@ module DropdownsHelper ...@@ -40,8 +40,9 @@ module DropdownsHelper
end end
def dropdown_toggle(toggle_text, data_attr, options = {}) def dropdown_toggle(toggle_text, data_attr, options = {})
default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down') output << icon('chevron-down')
output.html_safe output.html_safe
end end
......
...@@ -8,18 +8,12 @@ module IssuablesHelper ...@@ -8,18 +8,12 @@ module IssuablesHelper
end end
def multi_label_name(current_labels, default_label) def multi_label_name(current_labels, default_label)
# current_labels may be a string from before if current_labels && current_labels.any?
if current_labels.is_a?(Array) title = current_labels.first.try(:title)
if current_labels.count > 1 if current_labels.size > 1
"#{current_labels[0]} +#{current_labels.count - 1} more" "#{title} +#{current_labels.size - 1} more"
else else
current_labels[0] title
end
elsif current_labels.is_a?(String)
if current_labels.nil? || current_labels.empty?
default_label
else
current_labels
end end
else else
default_label default_label
......
...@@ -115,8 +115,9 @@ module LabelsHelper ...@@ -115,8 +115,9 @@ module LabelsHelper
end end
def labels_filter_path def labels_filter_path
if @project project = @target_project || @project
namespace_project_labels_path(@project.namespace, @project, :json) if project
namespace_project_labels_path(project.namespace, project, :json)
else else
dashboard_labels_path(:json) dashboard_labels_path(:json)
end end
......
...@@ -71,8 +71,9 @@ module MilestonesHelper ...@@ -71,8 +71,9 @@ module MilestonesHelper
end end
def milestones_filter_dropdown_path def milestones_filter_dropdown_path
if @project project = @target_project || @project
namespace_project_milestones_path(@project.namespace, @project, :json) if project
namespace_project_milestones_path(project.namespace, project, :json)
else else
dashboard_milestones_path(:json) dashboard_milestones_path(:json)
end end
......
...@@ -92,12 +92,8 @@ module PageLayoutHelper ...@@ -92,12 +92,8 @@ module PageLayoutHelper
end end
end end
def fluid_layout(enabled = false) def fluid_layout
if @fluid_layout.nil? current_user && current_user.layout == "fluid"
@fluid_layout = (current_user && current_user.layout == "fluid") || enabled
else
@fluid_layout
end
end end
def blank_container(enabled = false) def blank_container(enabled = false)
......
...@@ -49,12 +49,10 @@ module SelectsHelper ...@@ -49,12 +49,10 @@ module SelectsHelper
end end
def select2_tag(id, opts = {}) def select2_tag(id, opts = {})
css_class = '' opts[:class] << ' multiselect' if opts[:multiple]
css_class << 'multiselect ' if opts[:multiple]
css_class << (opts[:class] || '')
value = opts[:selected] || '' value = opts[:selected] || ''
hidden_field_tag(id, value, class: css_class) hidden_field_tag(id, value, opts)
end end
private private
......
...@@ -114,6 +114,26 @@ module TodosHelper ...@@ -114,6 +114,26 @@ module TodosHelper
selected_type ? selected_type[:text] : default_type selected_type ? selected_type[:text] : default_type
end end
def todo_due_date(todo)
return unless todo.target.try(:due_date)
is_due_today = todo.target.due_date.today?
is_overdue = todo.target.overdue?
css_class =
if is_due_today
'text-warning'
elsif is_overdue
'text-danger'
else
''
end
html = "&middot; ".html_safe
html << content_tag(:span, class: css_class) do
"Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}"
end
end
private private
def show_todo_state?(todo) def show_todo_state?(todo)
......
...@@ -196,7 +196,7 @@ module Ci ...@@ -196,7 +196,7 @@ module Ci
end end
def has_warnings? def has_warnings?
builds.latest.ignored.any? builds.latest.failed_but_allowed.any?
end end
def config_processor def config_processor
...@@ -251,9 +251,8 @@ module Ci ...@@ -251,9 +251,8 @@ module Ci
Ci::ProcessPipelineService.new(project, user).execute(self) Ci::ProcessPipelineService.new(project, user).execute(self)
end end
def build_updated def update_status
with_lock do with_lock do
reload
case latest_builds_status case latest_builds_status
when 'pending' then enqueue when 'pending' then enqueue
when 'running' then run when 'running' then run
......
...@@ -2,7 +2,7 @@ module Ci ...@@ -2,7 +2,7 @@ module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Ci::Model extend Ci::Model
LAST_CONTACT_TIME = 2.hours.ago LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online] AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked] FORM_EDITABLE = %i[description tag_list active run_untagged locked]
......
...@@ -24,7 +24,22 @@ class CommitStatus < ActiveRecord::Base ...@@ -24,7 +24,22 @@ class CommitStatus < ActiveRecord::Base
scope :retried, -> { where.not(id: latest) } scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) } scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled])
end
scope :exclude_ignored, -> do
quoted_when = connection.quote_column_name('when')
# We want to ignore failed_but_allowed jobs
where("allow_failure = ? OR status IN (?)",
false, all_state_names - [:failed, :canceled]).
# We want to ignore skipped manual jobs
where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped').
# We want to ignore skipped on_failure
where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
end
scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) } scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) } scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
...@@ -69,13 +84,18 @@ class CommitStatus < ActiveRecord::Base ...@@ -69,13 +84,18 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now commit_status.update_attributes finished_at: Time.now
end end
after_transition any => [:success, :failed, :canceled] do |commit_status|
commit_status.pipeline.try(:process!)
true
end
after_transition do |commit_status, transition| after_transition do |commit_status, transition|
commit_status.pipeline.try(:build_updated) unless transition.loopback? commit_status.pipeline.try do |pipeline|
break if transition.loopback?
if commit_status.complete?
ProcessPipelineWorker.perform_async(pipeline.id)
end
UpdatePipelineWorker.perform_async(pipeline.id)
end
true
end end
after_transition [:created, :pending, :running] => :success do |commit_status| after_transition [:created, :pending, :running] => :success do |commit_status|
...@@ -111,7 +131,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -111,7 +131,7 @@ class CommitStatus < ActiveRecord::Base
end end
end end
def ignored? def failed_but_allowed?
allow_failure? && (failed? || canceled?) allow_failure? && (failed? || canceled?)
end end
......
...@@ -8,32 +8,32 @@ module HasStatus ...@@ -8,32 +8,32 @@ module HasStatus
class_methods do class_methods do
def status_sql def status_sql
scope = all scope = if respond_to?(:exclude_ignored)
exclude_ignored
else
all
end
builds = scope.select('count(*)').to_sql builds = scope.select('count(*)').to_sql
created = scope.created.select('count(*)').to_sql created = scope.created.select('count(*)').to_sql
success = scope.success.select('count(*)').to_sql success = scope.success.select('count(*)').to_sql
ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
ignored ||= '0'
pending = scope.pending.select('count(*)').to_sql pending = scope.pending.select('count(*)').to_sql
running = scope.running.select('count(*)').to_sql running = scope.running.select('count(*)').to_sql
canceled = scope.canceled.select('count(*)').to_sql
skipped = scope.skipped.select('count(*)').to_sql skipped = scope.skipped.select('count(*)').to_sql
canceled = scope.canceled.select('count(*)').to_sql
deduce_status = "(CASE "(CASE
WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created' WHEN (#{builds})=(#{created}) THEN 'created'
WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
ELSE 'failed' ELSE 'failed'
END)" END)"
deduce_status
end end
def status def status
all.pluck(self.status_sql).first all.pluck(status_sql).first
end end
def started_at def started_at
...@@ -43,6 +43,10 @@ module HasStatus ...@@ -43,6 +43,10 @@ module HasStatus
def finished_at def finished_at
all.maximum(:finished_at) all.maximum(:finished_at)
end end
def all_state_names
state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) }
end
end end
included do included do
......
...@@ -43,19 +43,15 @@ module Mentionable ...@@ -43,19 +43,15 @@ module Mentionable
self self
end end
def all_references(current_user = nil, text = nil, extractor: nil) def all_references(current_user = nil, extractor: nil)
extractor ||= Gitlab::ReferenceExtractor. extractor ||= Gitlab::ReferenceExtractor.
new(project, current_user) new(project, current_user)
if text self.class.mentionable_attrs.each do |attr, options|
extractor.analyze(text, author: author) text = __send__(attr)
else options = options.merge(cache_key: [self, attr], author: author)
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
options = options.merge(cache_key: [self, attr], author: author)
extractor.analyze(text, options) extractor.analyze(text, options)
end
end end
extractor extractor
...@@ -66,8 +62,8 @@ module Mentionable ...@@ -66,8 +62,8 @@ module Mentionable
end end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author, text = nil) def referenced_mentionables(current_user = self.author)
refs = all_references(current_user, text) refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits) refs = (refs.issues + refs.merge_requests + refs.commits)
# We're using this method instead of Array diffing because that requires # We're using this method instead of Array diffing because that requires
...@@ -77,8 +73,8 @@ module Mentionable ...@@ -77,8 +73,8 @@ module Mentionable
end end
# Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+. # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
def create_cross_references!(author = self.author, without = [], text = nil) def create_cross_references!(author = self.author, without = [])
refs = referenced_mentionables(author, text) refs = referenced_mentionables(author)
# We're using this method instead of Array diffing because that requires # We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the # both of the object's `hash` values to be the same, which may not be the
...@@ -97,10 +93,7 @@ module Mentionable ...@@ -97,10 +93,7 @@ module Mentionable
return if changes.empty? return if changes.empty?
original_text = changes.collect { |_, vals| vals.first }.join(' ') create_cross_references!(author)
preexisting = referenced_mentionables(author, original_text)
create_cross_references!(author, preexisting)
end end
private private
......
...@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base ...@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true delegate :name, to: :environment, prefix: true
after_save :keep_around_commit after_save :create_ref
def commit def commit
project.commit(sha) project.commit(sha)
...@@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base ...@@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base
self == environment.last_deployment self == environment.last_deployment
end end
def keep_around_commit def create_ref
project.repository.keep_around(self.sha) project.repository.create_ref(ref, ref_path)
end end
def manual_actions def manual_actions
...@@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base ...@@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base
where.not(id: self.id). where.not(id: self.id).
take take
end end
private
def ref_path
File.join(environment.ref_path, 'deployments', id.to_s)
end
end end
...@@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base ...@@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base
def update_merge_request_metrics? def update_merge_request_metrics?
self.name == "production" self.name == "production"
end end
def ref_path
"refs/environments/#{Shellwords.shellescape(name)}"
end
end end
...@@ -328,13 +328,15 @@ class Event < ActiveRecord::Base ...@@ -328,13 +328,15 @@ class Event < ActiveRecord::Base
def reset_project_activity def reset_project_activity
return unless project return unless project
# Don't even bother obtaining a lock if the last update happened less than # Don't bother updating if we know the project was updated recently.
# 60 minutes ago.
return if recent_update? return if recent_update?
return unless try_obtain_lease # At this point it's possible for multiple threads/processes to try to
# update the project. Only one query should actually perform the update,
project.update_column(:last_activity_at, created_at) # hence we add the extra WHERE clause for last_activity_at.
Project.unscoped.where(id: project_id).
where('last_activity_at > ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
update_all(last_activity_at: created_at)
end end
private private
...@@ -342,11 +344,4 @@ class Event < ActiveRecord::Base ...@@ -342,11 +344,4 @@ class Event < ActiveRecord::Base
def recent_update? def recent_update?
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
end end
def try_obtain_lease
Gitlab::ExclusiveLease.
new("project:update_last_activity_at:#{project.id}",
timeout: RESET_PROJECT_ACTIVITY_INTERVAL.to_i).
try_obtain
end
end end
...@@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars # Temporary fields to store compare vars
# when creating new merge request # when creating new merge request
attr_accessor :can_be_created, :compare_commits, :compare attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
state_machine :state, initial: :opened do state_machine :state, initial: :opened do
event :close do event :close do
...@@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base
end end
def diff_size def diff_size
merge_request_diff.size diffs(diff_options).size
end end
def diff_base_commit def diff_base_commit
...@@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base
# `MergeRequestsClosingIssues` model. This is a performance optimization. # `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires # Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately. # running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user = self.author) def cache_merge_request_closes_issues!(current_user = self.author)
return if project.has_external_issue_tracker?
transaction do transaction do
self.merge_requests_closing_issues.delete_all self.merge_requests_closing_issues.delete_all
closes_issues(current_user).each do |issue| closes_issues(current_user).each do |issue|
self.merge_requests_closing_issues.create!(issue: issue) self.merge_requests_closing_issues.create!(issue: issue)
end end
......
...@@ -380,6 +380,7 @@ class Project < ActiveRecord::Base ...@@ -380,6 +380,7 @@ class Project < ActiveRecord::Base
SELECT project_id, COUNT(*) AS amount SELECT project_id, COUNT(*) AS amount
FROM notes FROM notes
WHERE created_at >= #{sanitize(since)} WHERE created_at >= #{sanitize(since)}
AND system IS FALSE
GROUP BY project_id GROUP BY project_id
) join_note_counts ON projects.id = join_note_counts.project_id" ) join_note_counts ON projects.id = join_note_counts.project_id"
......
...@@ -20,7 +20,10 @@ class ProjectFeature < ActiveRecord::Base ...@@ -20,7 +20,10 @@ class ProjectFeature < ActiveRecord::Base
FEATURES = %i(issues merge_requests wiki snippets builds) FEATURES = %i(issues merge_requests wiki snippets builds)
belongs_to :project # Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) }
default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_access_level, value: ENABLED, allows_nil: false default_value_for :issues_access_level, value: ENABLED, allows_nil: false
......
...@@ -838,6 +838,52 @@ class Repository ...@@ -838,6 +838,52 @@ class Repository
end end
end end
def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref|
index = rugged.index
parents = []
branch = find_branch(ref)
if branch
last_commit = branch.target
index.read_tree(last_commit.raw_commit.tree)
parents = [last_commit.sha]
end
actions.each do |action|
case action[:action]
when :create, :update, :move
mode =
case action[:action]
when :update
index.get(action[:file_path])[:mode]
when :move
index.get(action[:previous_path])[:mode]
end
mode ||= 0o100644
index.remove(action[:previous_path]) if action[:action] == :move
content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
oid = rugged.write(content, :blob)
index.add(path: action[:file_path], oid: oid, mode: mode)
when :delete
index.remove(action[:file_path])
end
end
options = {
tree: index.write_tree(rugged),
message: message,
parents: parents
}
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
Rugged::Commit.create(rugged, options)
end
end
def get_committer_and_author(user, email: nil, name: nil) def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user) committer = user_to_committer(user)
author = Gitlab::Git::committer_hash(email: email, name: name) || committer author = Gitlab::Git::committer_hash(email: email, name: name) || committer
...@@ -997,6 +1043,10 @@ class Repository ...@@ -997,6 +1043,10 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo) Gitlab::Popen.popen(args, path_to_repo)
end end
def create_ref(ref, ref_path)
fetch_ref(path_to_repo, ref, ref_path)
end
def update_branch_with_hooks(current_user, branch) def update_branch_with_hooks(current_user, branch)
update_autocrlf_option update_autocrlf_option
......
...@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base ...@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base
end end
def #{arg}=(value) def #{arg}=(value)
self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties['#{arg}'] = value self.properties['#{arg}'] = value
end end
......
...@@ -279,6 +279,11 @@ class User < ActiveRecord::Base ...@@ -279,6 +279,11 @@ class User < ActiveRecord::Base
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end end
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
end
def build_user(attrs = {}) def build_user(attrs = {})
User.new(attrs) User.new(attrs)
end end
...@@ -827,6 +832,22 @@ class User < ActiveRecord::Base ...@@ -827,6 +832,22 @@ class User < ActiveRecord::Base
todos_pending_count(force: true) todos_pending_count(force: true)
end end
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
# flow means we don't call that automatically (and can't conveniently do so).
#
# See:
# <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
#
def increment_failed_attempts!
self.failed_attempts ||= 0
self.failed_attempts += 1
if attempts_exceeded?
lock_access! unless access_locked?
else
save(validate: false)
end
end
private private
def projects_union(min_access_level = nil) def projects_union(min_access_level = nil)
......
...@@ -56,9 +56,8 @@ class BaseService ...@@ -56,9 +56,8 @@ class BaseService
result result
end end
def success def success(pass_back = {})
{ pass_back[:status] = :success
status: :success pass_back
}
end end
end end
...@@ -36,12 +36,7 @@ module Boards ...@@ -36,12 +36,7 @@ module Boards
end end
def set_state def set_state
params[:state] = params[:state] = list.done? ? 'closed' : 'opened'
case list.list_type.to_sym
when :backlog then 'opened'
when :done then 'closed'
else 'all'
end
end end
def board_label_ids def board_label_ids
......
...@@ -27,8 +27,9 @@ module Files ...@@ -27,8 +27,9 @@ module Files
create_target_branch create_target_branch
end end
if commit result = commit
success if result
success(result: result)
else else
error('Something went wrong. Your changes were not committed') error('Something went wrong. Your changes were not committed')
end end
...@@ -42,6 +43,12 @@ module Files ...@@ -42,6 +43,12 @@ module Files
@source_branch != @target_branch || @source_project != @project @source_branch != @target_branch || @source_project != @project
end end
def file_has_changed?
return false unless @last_commit_sha && last_commit
@last_commit_sha != last_commit.sha
end
def raise_error(message) def raise_error(message)
raise ValidationError.new(message) raise ValidationError.new(message)
end end
......
require_relative "base_service"
module Files
class MultiService < Files::BaseService
class FileChangedError < StandardError; end
def commit
repository.multi_action(
user: current_user,
branch: @target_branch,
message: @commit_message,
actions: params[:actions],
author_email: @author_email,
author_name: @author_name
)
end
private
def validate
super
params[:actions].each_with_index do |action, index|
unless action[:file_path].present?
raise_error("You must specify a file_path.")
end
regex_check(action[:file_path])
regex_check(action[:previous_path]) if action[:previous_path]
if project.empty_repo? && action[:action] != :create
raise_error("No files to #{action[:action]}.")
end
validate_file_exists(action)
case action[:action]
when :create
validate_create(action)
when :update
validate_update(action)
when :delete
validate_delete(action)
when :move
validate_move(action, index)
else
raise_error("Unknown action type `#{action[:action]}`.")
end
end
end
def validate_file_exists(action)
return if action[:action] == :create
file_path = action[:file_path]
file_path = action[:previous_path] if action[:action] == :move
blob = repository.blob_at_branch(params[:branch_name], file_path)
unless blob
raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
end
end
def last_commit
Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path)
end
def regex_check(file)
if file =~ Gitlab::Regex.directory_traversal_regex
raise_error(
'Your changes could not be committed, because the file name, `' +
file +
'` ' +
Gitlab::Regex.directory_traversal_regex_message
)
end
unless file =~ Gitlab::Regex.file_path_regex
raise_error(
'Your changes could not be committed, because the file name, `' +
file +
'` ' +
Gitlab::Regex.file_path_regex_message
)
end
end
def validate_create(action)
return if project.empty_repo?
if repository.blob_at_branch(params[:branch_name], action[:file_path])
raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
end
end
def validate_delete(action)
end
def validate_move(action, index)
if action[:previous_path].nil?
raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
end
blob = repository.blob_at_branch(params[:branch_name], action[:file_path])
if blob
raise_error("Move destination `#{action[:file_path]}` already exists.")
end
if action[:content].nil?
blob = repository.blob_at_branch(params[:branch_name], action[:previous_path])
blob.load_all_data!(repository) if blob.truncated?
params[:actions][index][:content] = blob.data
end
end
def validate_update(action)
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
end
end
end
end
...@@ -23,12 +23,6 @@ module Files ...@@ -23,12 +23,6 @@ module Files
end end
end end
def file_has_changed?
return false unless @last_commit_sha && last_commit
@last_commit_sha != last_commit.sha
end
def last_commit def last_commit
@last_commit ||= Gitlab::Git::Commit. @last_commit ||= Gitlab::Git::Commit.
last_for_path(@source_project.repository, @source_branch, @file_path) last_for_path(@source_project.repository, @source_branch, @file_path)
......
...@@ -14,6 +14,8 @@ module Members ...@@ -14,6 +14,8 @@ module Members
if member.request? && member.user != user if member.request? && member.user != user
notification_service.decline_access_request(member) notification_service.decline_access_request(member)
end end
member
end end
end end
end end
module Members module Members
class DestroyService < BaseService class DestroyService < BaseService
attr_accessor :member, :current_user include MembersHelper
def initialize(member, current_user) attr_accessor :source
@member = member
ALLOWED_SCOPES = %i[members requesters all]
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user @current_user = current_user
@params = params
end end
def execute def execute(scope = :members)
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
raise Gitlab::Access::AccessDeniedError
end member = find_member!(scope)
raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
AuthorizedDestroyService.new(member, current_user).execute AuthorizedDestroyService.new(member, current_user).execute
end end
private
def find_member!(scope)
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
case scope
when :all
source.members.find_by(condition) ||
source.requesters.find_by!(condition)
else
source.public_send(scope).find_by!(condition)
end
end
def can_destroy_member?(member)
member && can?(current_user, action_member_permission(:destroy, member), member)
end
end end
end end
...@@ -7,6 +7,8 @@ module Projects ...@@ -7,6 +7,8 @@ module Projects
def execute def execute
forked_from_project_id = params.delete(:forked_from_project_id) forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data) import_data = params.delete(:import_data)
@skip_wiki = params.delete(:skip_wiki)
@project = Project.new(params) @project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level # Make sure that the user is allowed to use the specified visibility level
...@@ -15,6 +17,11 @@ module Projects ...@@ -15,6 +17,11 @@ module Projects
return @project return @project
end end
unless allowed_fork?(forked_from_project_id)
@project.errors.add(:forked_from_project_id, 'is forbidden')
return @project
end
# Set project name from path # Set project name from path
if @project.name.present? && @project.path.present? if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok # if both name and path set - everything is ok
...@@ -71,6 +78,13 @@ module Projects ...@@ -71,6 +78,13 @@ module Projects
@project.errors.add(:namespace, "is not valid") @project.errors.add(:namespace, "is not valid")
end end
def allowed_fork?(source_project_id)
return true if source_project_id.nil?
source_project = Project.find_by(id: source_project_id)
current_user.can?(:fork_project, source_project)
end
def allowed_namespace?(user, namespace_id) def allowed_namespace?(user, namespace_id)
namespace = Namespace.find_by(id: namespace_id) namespace = Namespace.find_by(id: namespace_id)
current_user.can?(:create_projects, namespace) current_user.can?(:create_projects, namespace)
...@@ -80,7 +94,7 @@ module Projects ...@@ -80,7 +94,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
@project.create_wiki if @project.feature_available?(:wiki, current_user) @project.create_wiki unless skip_wiki?
@project.build_missing_services @project.build_missing_services
@project.create_labels @project.create_labels
...@@ -94,6 +108,10 @@ module Projects ...@@ -94,6 +108,10 @@ module Projects
end end
end end
def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
def save_project_and_import_data(import_data) def save_project_and_import_data(import_data)
Project.transaction do Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
......
...@@ -16,6 +16,8 @@ module Projects ...@@ -16,6 +16,8 @@ module Projects
end end
new_project = CreateService.new(current_user, new_params).execute new_project = CreateService.new(current_user, new_params).execute
return new_project unless new_project.persisted?
builds_access_level = @project.project_feature.builds_access_level builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level) new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
......
...@@ -347,7 +347,7 @@ module SystemNoteService ...@@ -347,7 +347,7 @@ module SystemNoteService
notes = notes.where(noteable_id: noteable.id) notes = notes.where(noteable_id: noteable.id)
end end
notes_for_mentioner(mentioner, noteable, notes).count > 0 notes_for_mentioner(mentioner, noteable, notes).exists?
end end
# Build an Array of lines detailing each commit added in a merge request # Build an Array of lines detailing each commit added in a merge request
......
...@@ -63,6 +63,11 @@ ...@@ -63,6 +63,11 @@
Reply by email Reply by email
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled? = boolean_to_icon Gitlab::IncomingEmail.enabled?
%p
Container Registry
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
.col-md-4 .col-md-4
%h4 %h4
Components Components
......
...@@ -16,8 +16,7 @@ ...@@ -16,8 +16,7 @@
%tr %tr
%td #{stage.capitalize} Job - #{build[:name]} %td #{stage.capitalize} Job - #{build[:name]}
%td %td
%pre %pre= build[:commands]
= simple_format build[:commands]
%br %br
%b Tag list: %b Tag list:
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
(removed) (removed)
&middot; #{time_ago_with_tooltip(todo.created_at)} &middot; #{time_ago_with_tooltip(todo.created_at)}
= todo_due_date(todo)
.todo-body .todo-body
.todo-note .todo-note
......
...@@ -33,8 +33,8 @@ ...@@ -33,8 +33,8 @@
.form-group .form-group
= f.label :projects, "Projects", class: "control-label" = f.label :projects, "Projects", class: "control-label"
.col-sm-10 .col-sm-10
= f.collection_select :project_ids, @group.projects, :id, :name, = f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
{ selected: @group.projects.map(&:id) }, multiple: true, class: 'select2' { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
.col-md-6 .col-md-6
.form-group .form-group
......
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
%div{ class: fluid_layout ? "container-fluid" : "container-fluid" } %div{ class: "container-fluid" }
.header-content .header-content
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
......
...@@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>') ...@@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class // Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') { if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper').find('.container-fluid').removeClass('container-limited') $('.content-wrapper .container-fluid').removeClass('container-limited')
} else { } else {
$('.content-wrapper').find('.container-fluid').addClass('container-limited') $('.content-wrapper .container-fluid').addClass('container-limited')
} }
// Re-enable the "Save" button // Re-enable the "Save" button
......
- admin = local_assigns.fetch(:admin, false) - admin = local_assigns.fetch(:admin, false)
- if builds.blank? - if builds.blank?
%li %div
.nothing-here-block No builds to show .nothing-here-block No builds to show
- else - else
.table-holder .table-holder
......
...@@ -19,5 +19,5 @@ ...@@ -19,5 +19,5 @@
= link_to ci_lint_path, class: 'btn btn-default' do = link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint %span CI Lint
%ul.content-list.builds-content-list %div.content-list.builds-content-list
= render "table", builds: @builds, project: @project = render "table", builds: @builds, project: @project
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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