Commit ce884b34 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee' into 'master'

CE->EE merge before 8.13.0-rc1

Conflicts resolved in the first merge:

```
UU app/models/group.rb
UU app/models/member.rb
UU app/models/project_team.rb
UU app/services/merge_requests/update_service.rb
UU app/services/notification_service.rb
UU app/views/shared/_sort_dropdown.html.haml
UU doc/api/oauth2.md
UU lib/api/members.rb
UU spec/models/merge_request_spec.rb
AA spec/views/ci/lints/show.html.haml_spec.rb
UU app/views/projects/commits/_head.html.haml
```

Conflicts resolved in the second merge:

```
UU app/assets/javascripts/groups_select.js
UU app/assets/javascripts/labels_select.js
UU app/controllers/concerns/membership_actions.rb
UU app/controllers/groups/group_members_controller.rb
UU app/helpers/selects_helper.rb
UU app/views/admin/dashboard/index.html.haml
UU app/views/shared/issuable/_form.html.haml
UU app/views/shared/issuable/_label_dropdown.html.haml
UU app/views/shared/issuable/_sidebar.html.haml
UU config/routes.rb
UU spec/services/merge_requests/merge_service_spec.rb
UU spec/services/system_note_service_spec.rb
```

See merge request !772
parents 3171ace1 99232ab0
......@@ -6,7 +6,7 @@
"always-semicolon": true,
"color-case": "lower",
"block-indent": " ",
"color-shorthand": true,
"color-shorthand": false,
"element-case": "lower",
"space-before-colon": "",
"space-after-colon": " ",
......
image: "ruby:2.3.1"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3-git-2.7-phantomjs-2.1"
cache:
key: "ruby-231"
paths:
- vendor/apt
- vendor/ruby
variables:
......@@ -143,14 +142,13 @@ spinach 9 10: *spinach-knapsack
# Execute all testing suites against Ruby 2.1
.ruby-21: &ruby-21
image: "ruby:2.1"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1"
<<: *use-db
only:
- master
cache:
key: "ruby21"
paths:
- vendor/apt
- vendor/ruby
.rspec-knapsack-ruby21: &rspec-knapsack-ruby21
......@@ -211,9 +209,7 @@ rubocop: *exec
rake haml_lint: *exec
rake scss_lint: *exec
rake brakeman: *exec
rake flay:
<<: *exec
allow_failure: yes
rake flay: *exec
license_finder: *exec
rake downtime_check: *exec
......
......@@ -453,6 +453,10 @@ Style/VariableName:
EnforcedStyle: snake_case
Enabled: true
# Use the configured style when numbering variables.
Style/VariableNumber:
Enabled: false
# Use when x then ... for one-line cases.
Style/WhenThen:
Enabled: true
......@@ -639,6 +643,10 @@ Lint/RescueException:
Lint/ShadowedException:
Enabled: false
# Checks for Object#to_s usage in string interpolation.
Lint/StringConversionInInterpolation:
Enabled: true
# Do not use prefix `_` for a variable that is used.
Lint/UnderscorePrefixedVariableName:
Enabled: true
......
This diff is collapsed.
......@@ -79,7 +79,7 @@ linters:
# HEX colors should use three-character values where possible.
HexLength:
enabled: true
enabled: false
# HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased)
- Update runner version only when updating contacted_at
- Add link from system note to compare with previous version
- Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Fix centering of custom header logos (Ashley Dumaine)
- AbstractReferenceFilter caches project_refs on RequestStore when active
- 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)
- Speed-up group milestones show page
- Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps)
- 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
- 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)
- Allow the Koding integration to be configured through the API
- Added soft wrap button to repository file/blob editor
- 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)
- 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)
- Use a ConnectionPool for Rails.cache on Sidekiq servers
- Replace `alias_method_chain` with `Module#prepend`
- Enable GitLab Import/Export for non-admin users.
- Preserve label filters when sorting !6136 (Joseph Frazier)
- Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496
- 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.
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree
- 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
- Better empty state for Groups view
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile
- Fix resolved discussion display in side-by-side diff view !6575
- Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- 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 Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone)
v 8.12.4 (unreleased)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Grouped pipeline dropdown is a scrollable container
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
- Update Gitlab Shell to support low IO priority for storage moves
......@@ -30,6 +81,7 @@ v 8.12.3
v 8.12.2
- Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination
- Fix "Create project" button layout when visibility options are restricted
- Fix List-Unsubscribe header in emails
- Fix IssuesController#show degradation including project on loaded notes
- Fix an issue with the "Commits" section of the cycle analytics summary. !6513
......@@ -41,6 +93,7 @@ v 8.12.2
- 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 resolve discussion buttons endpoint path
- Refactor remnants of CoffeeScript destructured opts and super !6261
v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
......@@ -81,6 +134,7 @@ v 8.12.0
- Fix long comments in diffs messing with table width
- Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page
- Honor "fixed layout" preference in more places !6422
- Run CI builds with the permissions of users !5735
- Fix sorting of issues in API
- Fix download artifacts button links !6407
......@@ -97,6 +151,7 @@ v 8.12.0
- Reduce contributions calendar data payload (ClemMakesApps)
- Show all pipelines for merge requests even from discarded commits !6414
- Replace contributions calendar timezone payload with dates (ClemMakesApps)
- Changed MR widget build status to pipeline status !6335
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278
- Move parsing of sidekiq ps into helper !6245 (pascalbetz)
......@@ -120,6 +175,7 @@ v 8.12.0
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
- Add textarea autoresize after comment (ClemMakesApps)
- 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
- Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions
......
......@@ -109,17 +109,17 @@ gem 'elasticsearch-rails'
gem 'gitlab-elasticsearch-git', '~> 1.0.1', require: "elasticsearch/git"
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'task_list', '~> 1.0.2', require: 'task_list/railtie'
gem 'github-markup', '~> 1.4'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~>3.6'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 2.0'
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie'
gem 'github-markup', '~> 1.4'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~>3.6'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 2.0'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
......@@ -140,7 +140,7 @@ gem 'state_machines-activerecord', '~> 0.4.0'
gem 'after_commit_queue', '~> 1.3.0'
# Issue tags
gem 'acts-as-taggable-on', '~> 3.4'
gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
gem 'sidekiq', '~> 4.2'
......@@ -242,7 +242,7 @@ gem 'base32', '~> 0.3.0'
gem "gitlab-license", "~> 1.0"
# Sentry integration
gem 'sentry-raven', '~> 1.1.0'
gem 'sentry-raven', '~> 2.0.0'
gem 'premailer-rails', '~> 1.9.0'
......@@ -306,7 +306,7 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0'
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 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false
......
......@@ -44,8 +44,8 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
acts-as-taggable-on (3.5.0)
activerecord (>= 3.2, < 5)
acts-as-taggable-on (4.0.0)
activerecord (>= 4.0)
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
......@@ -157,6 +157,10 @@ GEM
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
deckar01-task_list (1.0.5)
activesupport (~> 4.0)
html-pipeline
rack (~> 1.0)
default_value_for (3.0.2)
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
......@@ -507,7 +511,7 @@ GEM
orm_adapter (0.5.0)
paranoia (2.1.4)
activerecord (~> 4.0)
parser (2.3.1.2)
parser (2.3.1.4)
ast (~> 2.2)
pg (0.18.4)
pkg-config (1.1.7)
......@@ -640,7 +644,7 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.5.0)
rubocop (0.42.0)
rubocop (0.43.0)
parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
......@@ -685,8 +689,8 @@ GEM
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
sentry-raven (1.1.0)
faraday (>= 0.7.6)
sentry-raven (2.0.2)
faraday (>= 0.7.6, < 0.10.x)
settingslogic (2.0.9)
sexp_processor (4.7.0)
sham_rack (1.3.6)
......@@ -749,8 +753,6 @@ GEM
ffi
sysexits (1.2.0)
systemu (2.6.5)
task_list (1.0.2)
html-pipeline
teaspoon (1.1.5)
railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
......@@ -827,7 +829,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4)
acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0)
akismet (~> 2.0)
......@@ -858,6 +860,7 @@ DEPENDENCIES
creole (~> 0.5.0)
d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 1.0.5)
default_value_for (~> 3.0.0)
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
......@@ -968,7 +971,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
rubocop (~> 0.42.0)
rubocop (~> 0.43.0)
rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
......@@ -978,7 +981,7 @@ DEPENDENCIES
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
sentry-raven (~> 1.1.0)
sentry-raven (~> 2.0.0)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
......@@ -996,7 +999,6 @@ DEPENDENCIES
sprockets-es6 (~> 0.9.2)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
task_list (~> 1.0.2)
teaspoon (~> 1.1.0)
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
......@@ -1018,4 +1020,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.13.1
1.13.2
(function() {
this.LabelManager = (function() {
LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
((global) => {
function LabelManager(opts) {
// Defaults
var ref, ref1, ref2;
if (opts == null) {
opts = {};
}
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');
class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
this.otherLabels = otherLabels || $('.js-other-labels');
this.errorMessage = 'Unable to update label prioritization at this time';
this.prioritizedLabels.sortable({
items: 'li',
placeholder: 'list-placeholder',
......@@ -18,33 +15,30 @@
this.bindEvents();
}
LabelManager.prototype.bindEvents = function() {
bindEvents() {
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
};
}
LabelManager.prototype.onTogglePriorityClick = function(e) {
var $btn, $label, $tooltip, _this, action;
onTogglePriorityClick(e) {
e.preventDefault();
_this = e.data;
$btn = $(e.currentTarget);
$label = $("#" + ($btn.data('domId')));
action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
// Make sure tooltip will hide
$tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby')));
const _this = e.data;
const $btn = $(e.currentTarget);
const $label = $(`#${$btn.data('domId')}`);
const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
$tooltip.tooltip('destroy');
return _this.toggleLabelPriority($label, action);
};
}
LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) {
var $from, $target, _this, url, xhr;
toggleLabelPriority($label, action, persistState) {
if (persistState == null) {
persistState = true;
}
_this = this;
url = $label.find('.js-toggle-priority').data('url');
$target = this.prioritizedLabels;
$from = this.otherLabels;
// Optimistic update
let xhr;
const _this = this;
const url = $label.find('.js-toggle-priority').data('url');
let $target = this.prioritizedLabels;
let $from = this.otherLabels;
if (action === 'remove') {
$target = this.otherLabels;
$from = this.prioritizedLabels;
......@@ -62,7 +56,7 @@
}
if (action === 'remove') {
xhr = $.ajax({
url: url,
url,
type: 'DELETE'
});
// Restore empty message
......@@ -73,43 +67,40 @@
xhr = this.savePrioritySort($label, action);
}
return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
};
}
LabelManager.prototype.onPrioritySortUpdate = function() {
var xhr;
xhr = this.savePrioritySort();
onPrioritySortUpdate() {
const xhr = this.savePrioritySort();
return xhr.fail(function() {
return new Flash(this.errorMessage, 'alert');
});
};
}
LabelManager.prototype.savePrioritySort = function() {
savePrioritySort() {
return $.post({
url: this.prioritizedLabels.data('url'),
data: {
label_ids: this.getSortedLabelsIds()
}
});
};
}
LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) {
var action;
action = originalAction === 'remove' ? 'add' : 'remove';
rollbackLabelPosition($label, originalAction) {
const action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false);
return new Flash(this.errorMessage, 'alert');
};
}
LabelManager.prototype.getSortedLabelsIds = function() {
var sortedIds;
sortedIds = [];
getSortedLabelsIds() {
const sortedIds = [];
this.prioritizedLabels.find('li').each(function() {
return sortedIds.push($(this).data('id'));
sortedIds.push($(this).data('id'));
});
return sortedIds;
};
}
}
return LabelManager;
gl.LabelManager = LabelManager;
})();
})(window.gl || (window.gl = {}));
}).call(this);
......@@ -24,12 +24,13 @@
},
// Return groups list. Filtered by query
// Only active groups retrieved
groups: function(query, skip_ldap, callback) {
groups: function(query, skip_ldap, skip_groups, callback) {
var url = Api.buildUrl(Api.groupsPath);
return $.ajax({
url: url,
data: {
search: query,
skip_groups: skip_groups,
per_page: 20
},
dataType: "json"
......
/*= 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 @@
return BlobGitignoreSelector;
})(TemplateSelector);
})(gl.TemplateSelector);
}).call(this);
......@@ -23,6 +23,6 @@
return BlobLicenseSelector;
})(TemplateSelector);
})(gl.TemplateSelector);
}).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 = {}));
......@@ -22,13 +22,14 @@
// submitted textarea
})(this));
this.initModePanesAndLinks();
new BlobLicenseSelectors({
this.initSoftWrap();
new gl.BlobLicenseSelectors({
editor: this.editor
});
new BlobGitignoreSelectors({
editor: this.editor
});
new BlobCiYamlSelectors({
new gl.BlobCiYamlSelectors({
editor: this.editor
});
}
......@@ -50,6 +51,7 @@
this.$editModePanes.hide();
currentPane.fadeIn(200);
if (paneId === "#preview") {
this.$toggleButton.hide();
return $.post(currentLink.data("preview-url"), {
content: this.editor.getValue()
}, function(response) {
......@@ -57,10 +59,23 @@
return currentPane.syntaxHighlight();
});
} else {
this.$toggleButton.show();
return this.editor.focus();
}
};
EditBlob.prototype.initSoftWrap = function() {
this.isSoftWrapped = false;
this.$toggleButton = $('.soft-wrap-toggle');
this.$toggleButton.on('click', this.toggleSoftWrap.bind(this));
};
EditBlob.prototype.toggleSoftWrap = function(e) {
this.isSoftWrapped = !this.isSoftWrapped;
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
};
return EditBlob;
})();
......
......@@ -146,7 +146,7 @@
$date = $('.js-artifacts-remove');
if ($date.length) {
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')), ' '));
}
};
......
......@@ -23,8 +23,9 @@
selectable: true,
filterable: true,
filterByText: true,
fieldName: $dropdown.attr('name'),
filterInput: 'input[type="text"]',
toggleLabel: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
var link;
if (ref.header != null) {
......
......@@ -26,15 +26,15 @@
};
showTooltip = function(target, title) {
return $(target).tooltip({
container: 'body',
html: 'true',
placement: 'auto bottom',
title: title,
trigger: 'manual'
}).tooltip('show').one('mouseleave', function() {
return $(this).tooltip('hide');
});
var $target = $(target);
var originalTitle = $target.data('original-title');
$target
.attr('title', 'Copied!')
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
.tooltip('fixTitle');
};
$(function() {
......
......@@ -7,6 +7,9 @@
function Diff() {
$('.files .diff-file').singleFileDiff();
this.filesCommentButton = $('.files .diff-file').filesCommentButton();
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$(document).off('click', '.js-unfold');
$(document).on('click', '.js-unfold', (function(_this) {
return function(event) {
......@@ -52,6 +55,10 @@
})(this));
}
Diff.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
Diff.prototype.lineNumbers = function(line) {
if (!line.children().length) {
return [0, 0];
......
......@@ -26,7 +26,7 @@
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
new IssuableBulkActions();
new gl.IssuableBulkActions();
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:issues:show':
......@@ -40,7 +40,7 @@
new Milestone();
break;
case 'dashboard:todos:index':
new Todos();
new gl.Todos();
break;
case 'projects:milestones:new':
case 'projects:milestones:edit':
......@@ -59,7 +59,9 @@
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
new IssuableTemplateSelectors();
new LabelsSelect();
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
break;
case 'projects:merge_requests:new':
case 'projects:merge_requests:edit':
......@@ -67,7 +69,9 @@
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
new IssuableTemplateSelectors();
new LabelsSelect();
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
break;
case 'projects:tags:new':
new ZenMode();
......@@ -165,7 +169,7 @@
break;
case 'projects:labels:index':
if ($('.prioritized-labels').length) {
new LabelManager();
new gl.LabelManager();
}
break;
case 'projects:network:show':
......@@ -289,7 +293,7 @@
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
return new gl.SearchAutocomplete();
}
};
......
......@@ -443,6 +443,7 @@
var contentHtml;
this.resetRows();
this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this);
}
......@@ -460,9 +461,21 @@
if (this.options.filterable) {
this.filterInput.focus();
}
if (this.options.showMenuAbove) {
this.positionMenuAbove();
}
return this.dropdown.trigger('shown.gl.dropdown');
};
GitLabDropdown.prototype.positionMenuAbove = function() {
var $button = $(this.el);
var $menu = this.dropdown.find('.dropdown-menu');
$menu.css('top', ($button.height() + $menu.height()) * -1);
};
GitLabDropdown.prototype.hidden = function(e) {
var $input;
this.resetRows();
......
......@@ -5,33 +5,19 @@
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
return function(i, select) {
var skip_group, url;
skip_group = $(select).data("skip-group");
url = $(select).data("url");
var skip_ldap, skip_groups;
skip_ldap = $(select).hasClass('skip_ldap');
skip_groups = $(select).data('skip-groups') || [];
return $(select).select2({
placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
return $.ajax({
url: url,
data: {
search: query.term,
per_page: 20
},
dataType: "json"
}).done(function(groups) {
var data, group, j, len;
return Api.groups(query.term, skip_ldap, skip_groups, function(groups) {
var data;
data = {
results: []
results: groups
};
for (j = 0, len = groups.length; j < len; j++) {
group = groups[j];
if (skip_group && group.path === skip_group) {
continue;
}
data.results.push(group);
}
return query.callback(data);
});
},
......
......@@ -51,7 +51,6 @@
}).remove();
// Submit the form to get new data
Issuable.filterResults($('.filter-form'));
return $('.js-label-select').trigger('update.label');
});
},
filterResults: (function(_this) {
......
(function() {
this.IssuableBulkActions = (function() {
function IssuableBulkActions(opts) {
// Set defaults
var ref, ref1, ref2;
if (opts == null) {
opts = {};
}
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
((global) => {
class IssuableBulkActions {
constructor({ container, form, issues } = {}) {
this.container = container || $('.content'),
this.form = form || this.getElement('.bulk-update');
this.issues = issues || this.getElement('.issues-list .issue');
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
this.bindEvents();
......@@ -15,53 +12,46 @@
Issuable.initChecks();
}
IssuableBulkActions.prototype.getElement = function(selector) {
getElement(selector) {
return this.container.find(selector);
};
}
IssuableBulkActions.prototype.bindEvents = function() {
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
};
}
IssuableBulkActions.prototype.onFormSubmit = function(e) {
onFormSubmit(e) {
e.preventDefault();
return this.submit();
};
}
IssuableBulkActions.prototype.submit = function() {
var _this, xhr;
_this = this;
xhr = $.ajax({
submit() {
const _this = this;
const xhr = $.ajax({
url: this.form.attr('action'),
method: this.form.attr('method'),
dataType: 'JSON',
data: this.getFormDataAsObject()
});
xhr.done(function(response, status, xhr) {
return location.reload();
});
xhr.fail(function() {
return new Flash("Issue update failed");
});
xhr.done(() => window.location.reload());
xhr.fail(() => new Flash("Issue update failed"));
return xhr.always(this.onFormSubmitAlways.bind(this));
};
}
IssuableBulkActions.prototype.onFormSubmitAlways = function() {
onFormSubmitAlways() {
return this.form.find('[type="submit"]').enable();
};
}
IssuableBulkActions.prototype.getSelectedIssues = function() {
getSelectedIssues() {
return this.issues.has('.selected_issue:checked');
};
}
IssuableBulkActions.prototype.getLabelsFromSelection = function() {
var labels;
labels = [];
getLabelsFromSelection() {
const labels = [];
this.getSelectedIssues().map(function() {
var _labels;
_labels = $(this).data('labels');
if (_labels) {
return _labels.map(function(labelId) {
const labelsData = $(this).data('labels');
if (labelsData) {
return labelsData.map(function(labelId) {
if (labels.indexOf(labelId) === -1) {
return labels.push(labelId);
}
......@@ -69,7 +59,7 @@
}
});
return labels;
};
}
/**
......@@ -77,25 +67,21 @@
* @return {Array} Label IDs
*/
IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() {
var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result;
result = [];
labelsToKeep = [];
ref = this.getElement('.labels-filter .is-indeterminate');
for (i = 0, len = ref.length; i < len; i++) {
el = ref[i];
labelsToKeep.push($(el).data('labelId'));
}
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
getUnmarkedIndeterminedLabels() {
const result = [];
const labelsToKeep = [];
this.getElement('.labels-filter .is-indeterminate')
.each((i, el) => labelsToKeep.push($(el).data('labelId')));
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
}
});
return result;
};
}
/**
......@@ -103,9 +89,8 @@
* Returns key/value pairs from form data
*/
IssuableBulkActions.prototype.getFormDataAsObject = function() {
var formData;
formData = {
getFormDataAsObject() {
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
......@@ -125,19 +110,18 @@
});
}
return formData;
};
}
IssuableBulkActions.prototype.getLabelsToApply = function() {
var $labels, labelIds;
labelIds = [];
$labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
getLabelsToApply() {
const labelIds = [];
const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels.each(function(k, label) {
if (label) {
return labelIds.push(parseInt($(label).val()));
}
});
return labelIds;
};
}
/**
......@@ -145,11 +129,10 @@
* @return {Array} Array of labels IDs
*/
IssuableBulkActions.prototype.getLabelsToRemove = function() {
var indeterminatedLabels, labelsToApply, result;
result = [];
indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
labelsToApply = this.getLabelsToApply();
getLabelsToRemove() {
const result = [];
const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
const labelsToApply = this.getLabelsToApply();
indeterminatedLabels.map(function(id) {
// 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
......@@ -158,10 +141,9 @@
}
});
return result;
};
return IssuableBulkActions;
}
}
})();
global.IssuableBulkActions = IssuableBulkActions;
}).call(this);
})(window.gl || (window.gl = {}));
This diff is collapsed.
......@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs';
class MergeConflictDataProvider {
getInitialData() {
// TODO: remove reliance on jQuery and DOM state introspection
const diffViewType = $.cookie('diff_view');
const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
return {
isLoading : true,
hasError : false,
isParallel : diffViewType === 'parallel',
diffViewType : diffViewType,
fixedLayout : fixedLayout,
isSubmitting : false,
conflictsData : {},
resolutionData : {}
......@@ -192,14 +195,17 @@ class MergeConflictDataProvider {
updateViewType(newType) {
const vi = this.vueInstance;
if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
return;
}
vi.diffView = newType;
vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
$('.content-wrapper .container-fluid').toggleClass('container-limited');
vi.diffViewType = newType;
vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType, {
path: (gon && gon.relative_url_root) || '/'
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
}
......
......@@ -60,9 +60,8 @@ class MergeConflictResolver {
$('#conflicts .js-syntax-highlight').syntaxHighlight();
});
if (this.vue.diffViewType === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
})
}
......
......@@ -36,13 +36,10 @@
};
MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') {
// `MergeRequests#new` has no tab-persisting or lazy-loading behavior
window.mrTabs = new MergeRequestTabs(this.opts);
} else {
// Show the first tab (Commits)
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
if (window.mrTabs) {
window.mrTabs.unbindEvents();
}
window.mrTabs = new MergeRequestTabs(this.opts);
};
MergeRequest.prototype.showAllCommits = function() {
......
......@@ -56,6 +56,8 @@
MergeRequestTabs.prototype.commitsLoaded = false;
MergeRequestTabs.prototype.fixedLayoutPref = null;
function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
......@@ -70,7 +72,12 @@
MergeRequestTabs.prototype.bindEvents = function() {
$(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) {
......@@ -85,11 +92,15 @@
if (action === 'commits') {
this.loadCommits($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else if (action === 'diffs') {
this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo(".merge-request-details .merge-request-tabs", {
offset: -navBarHeight
......@@ -97,11 +108,14 @@
} else if (action === 'builds') {
this.loadBuilds($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else if (action === 'pipelines') {
this.loadPipelines($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else {
this.expandView();
this.resetViewContainer();
}
if (this.opts.setUrl) {
this.setCurrentAction(action);
......@@ -126,7 +140,7 @@
if (action === 'show') {
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
......@@ -209,7 +223,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel') {
if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') {
_this.expandViewContainer();
}
_this.diffsLoaded = true;
......@@ -308,11 +322,21 @@
MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
// Returns diff view type
};
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() {
......
......@@ -7,7 +7,7 @@
this.currentProject = JSON.parse(currentProject);
}
$('.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);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
......@@ -15,6 +15,7 @@
selectedMilestone = $dropdown.data('selected');
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
......@@ -31,12 +32,12 @@
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
url: milestonesUrl
}).done(function(data) {
var extraOptions;
extraOptions = [];
var extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
......@@ -58,10 +59,14 @@
title: 'Upcoming'
});
}
if (extraOptions.length > 2) {
if (extraOptions.length) {
extraOptions.push('divider');
}
return callback(extraOptions.concat(data));
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
});
},
filterable: true,
......@@ -69,19 +74,20 @@
fields: ['title']
},
selectable: true,
toggleLabel: function(selected) {
if (selected && 'id' in selected) {
toggleLabel: function(selected, el, e) {
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title;
} else {
return defaultLabel;
}
},
defaultLabel: defaultLabel,
fieldName: $dropdown.data('field-name'),
text: function(milestone) {
return _.escape(milestone.title);
},
id: function(milestone) {
if (!useId) {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
return milestone.id;
......@@ -100,7 +106,8 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues: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;
}
if (page === 'projects:boards:show') {
......
(function() {
var GitLabCrop,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
((global) => {
GitLabCrop = (function() {
var FILENAMEREGEX;
// Matches everything but the file name
const FILENAMEREGEX = /^.*[\\\/]/;
// Matches everything but the file name
FILENAMEREGEX = /^.*[\\\/]/;
class GitLabCrop {
constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
function GitLabCrop(input, opts) {
var ref, ref1, ref2, ref3, ref4;
if (opts == null) {
opts = {};
}
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.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
this.onModalHide = this.onModalHide.bind(this);
this.onModalShow = this.onModalShow.bind(this);
this.onPickImageClick = this.onPickImageClick.bind(this);
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.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.bindEvents();
}
GitLabCrop.prototype.getElement = function(selector) {
getElement(selector) {
return $(selector, this.form);
};
}
GitLabCrop.prototype.bindEvents = function() {
bindEvents() {
var _this;
_this = this;
this.fileInput.on('change', function(e) {
......@@ -57,13 +55,13 @@
return _this.onActionBtnClick(btn);
});
return this.croppedImageBlob = null;
};
}
GitLabCrop.prototype.onPickImageClick = function() {
onPickImageClick() {
return this.fileInput.trigger('click');
};
}
GitLabCrop.prototype.onModalShow = function() {
onModalShow() {
var _this;
_this = this;
return this.modalCropImg.cropper({
......@@ -95,44 +93,44 @@
});
}
});
};
}
GitLabCrop.prototype.onModalHide = function() {
onModalHide() {
return this.modalCropImg.attr('src', '').cropper('destroy');
};
}
GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image
e.preventDefault(); // Destroy cropper instance
onUploadImageBtnClick(e) {
e.preventDefault();
this.setBlob();
this.setPreview();
this.modalCrop.modal('hide');
return this.fileInput.val('');
};
}
GitLabCrop.prototype.onActionBtnClick = function(btn) {
onActionBtnClick(btn) {
var data, result;
data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) {
return result = this.modalCropImg.cropper(data.method, data.option);
}
};
}
GitLabCrop.prototype.onFileInputChange = function(e, input) {
onFileInputChange(e, input) {
return this.readFile(input);
};
}
GitLabCrop.prototype.readFile = function(input) {
readFile(input) {
var _this, reader;
_this = this;
reader = new FileReader;
reader.onload = function() {
reader.onload = () => {
_this.modalCropImg.attr('src', reader.result);
return _this.modalCrop.modal('show');
};
return reader.readAsDataURL(input.files[0]);
};
}
GitLabCrop.prototype.dataURLtoBlob = function(dataURL) {
dataURLtoBlob(dataURL) {
var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]);
array = [];
......@@ -143,35 +141,32 @@
return new Blob([new Uint8Array(array)], {
type: 'image/png'
});
};
}
GitLabCrop.prototype.setPreview = function() {
setPreview() {
var filename;
this.previewImage.attr('src', this.dataURL);
filename = this.fileInput.val().replace(FILENAMEREGEX, '');
return this.filename.text(filename);
};
}
GitLabCrop.prototype.setBlob = function() {
setBlob() {
this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
width: 200,
height: 200
}).toDataURL('image/png');
return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
};
}
GitLabCrop.prototype.getBlob = function() {
getBlob() {
return this.croppedImageBlob;
};
return GitLabCrop;
})();
}
}
$.fn.glCrop = function(opts) {
return this.each(function() {
return $(this).data('glcrop', new GitLabCrop(this, opts));
});
};
}
}).call(this);
})(window.gl || (window.gl = {}));
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
((global) => {
this.Profile = (function() {
function Profile(opts) {
var cropOpts, ref;
if (opts == null) {
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");
}
});
class Profile {
constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this);
this.form = form || $('.edit-user');
this.bindEvents();
cropOpts = {
this.initAvatarGlCrop();
}
initAvatarGlCrop() {
const cropOpts = {
filename: '.js-avatar-filename',
previewImage: '.avatar-image .avatar',
modalCrop: '.modal-profile-crop',
......@@ -46,23 +20,51 @@
this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
}
Profile.prototype.bindEvents = function() {
return this.form.on('submit', this.onSubmitForm);
};
bindEvents() {
$('.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();
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) {
formData.append('user[avatar]', avatarBlob, 'avatar.png');
}
return $.ajax({
url: this.form.attr('action'),
type: this.form.attr('method'),
......@@ -70,37 +72,29 @@
dataType: "json",
processData: false,
contentType: false,
success: function(response) {
return new Flash(response.message, 'notice');
},
error: function(jqXHR) {
return new Flash(jqXHR.responseJSON.message, 'alert');
},
complete: function() {
success: response => new Flash(response.message, 'notice'),
error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
complete: () => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
return self.form.find(':input[disabled]').enable();
}
});
};
return Profile;
})();
}
}
$(function() {
$(document).on('focusout.ssh_key', '#key_key', function() {
var $title, comment;
$title = $('#key_title');
comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
const $title = $('#key_title');
const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
if (comment && comment.length > 1 && $title.val() === '') {
return $title.val(comment[1]).change();
}
// Extract the SSH Key title from its comment
});
if (gl.utils.getPagePath() === 'profiles') {
if (global.utils.getPagePath() === 'profiles') {
return new Profile();
}
});
}).call(this);
})(window.gl || (window.gl = {}));
......@@ -23,7 +23,7 @@
data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(term, false, groupsCallback);
return Api.groups(term, false, false, groupsCallback);
};
} else {
projectsCallback = finalCallback;
......@@ -72,7 +72,7 @@
data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(query.term, false, groupsCallback);
return Api.groups(query.term, false, false, groupsCallback);
};
} else {
projectsCallback = finalCallback;
......
......@@ -10,7 +10,7 @@
filterable: true,
fieldName: 'group_id',
data: function(term, callback) {
return Api.groups(term, null, function(data) {
return Api.groups(term, false, false, function(data) {
data.unshift({
name: 'Any'
});
......
......@@ -10,12 +10,13 @@
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>';
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file) {
this.file = file;
this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file);
this.$toggleIcon = $('.diff-toggle-caret', this.file);
this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path');
this.isOpen = !this.diffForPath;
if (this.diffForPath) {
......@@ -23,18 +24,22 @@
this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide();
this.content = null;
this.collapsedContent.after(this.loadingContent);
this.$toggleIcon.addClass('fa-caret-right');
} else {
this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide();
this.content.after(this.collapsedContent);
this.$toggleIcon.addClass('fa-caret-down');
}
this.collapsedContent.on('click', this.toggleDiff);
$('.file-title > a', this.file).on('click', this.toggleDiff);
$('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
}
SingleFileDiff.prototype.toggleDiff = function(e) {
var $target = $(e.target);
if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
......@@ -42,10 +47,12 @@
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML();
}
};
......
/*= require ../blob/template_selector */
((global) => {
class IssuableTemplateSelector extends TemplateSelector {
class IssuableTemplateSelector extends gl.TemplateSelector {
constructor(...args) {
super(...args);
this.projectPath = this.dropdown.data('project-path');
......@@ -16,7 +16,7 @@
if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => {
if (this.currentTemplate) this.setInputValueToTemplateContent();
if (this.currentTemplate) this.setInputValueToTemplateContent(false);
});
}
......@@ -26,26 +26,28 @@
this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner();
this.setInputValueToTemplateContent();
this.setInputValueToTemplateContent(true);
});
return;
}
setInputValueToTemplateContent() {
setInputValueToTemplateContent(append) {
// `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 the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the 2nd
// argument to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, true);
// skip focusing the description input by setting `true` as the
// `skipFocus` option to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
this.titleInput.focus();
} else {
this.requestFileSuccess(this.currentTemplate);
this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
}
return;
}
}
global.IssuableTemplateSelector = IssuableTemplateSelector;
})(window);
})(window.gl || (window.gl = {}));
((global) => {
class IssuableTemplateSelectors {
constructor(opts = {}) {
this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
this.editor = opts.editor || this.initEditor();
constructor({ $dropdowns, editor } = {}) {
this.$dropdowns = $dropdowns || $('.js-issuable-selector');
this.editor = editor || this.initEditor();
this.$dropdowns.each((i, dropdown) => {
let $dropdown = $(dropdown);
new IssuableTemplateSelector({
const $dropdown = $(dropdown);
new gl.IssuableTemplateSelector({
pattern: /(\.md)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
......@@ -26,4 +26,4 @@
}
global.IssuableTemplateSelectors = IssuableTemplateSelectors;
})(window);
})(window.gl || (window.gl = {}));
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.Todos = (function() {
function Todos(opts) {
var ref;
if (opts == null) {
opts = {};
}
this.allDoneClicked = bind(this.allDoneClicked, this);
this.doneClicked = bind(this.doneClicked, this);
this.el = (ref = opts.el) != null ? ref : $('.js-todos-options');
((global) => {
class Todos {
constructor({ el } = {}) {
this.allDoneClicked = this.allDoneClicked.bind(this);
this.doneClicked = this.doneClicked.bind(this);
this.el = el || $('.js-todos-options');
this.perPage = this.el.data('perPage');
this.clearListeners();
this.initBtnListeners();
this.initFilters();
}
Todos.prototype.clearListeners = function() {
clearListeners() {
$('.done-todo').off('click');
$('.js-todos-mark-all').off('click');
return $('.todo').off('click');
};
}
Todos.prototype.initBtnListeners = function() {
initBtnListeners() {
$('.done-todo').on('click', this.doneClicked);
$('.js-todos-mark-all').on('click', this.allDoneClicked);
return $('.todo').on('click', this.goToTodoUrl);
};
}
Todos.prototype.initFilters = function() {
initFilters() {
new UsersSelect();
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type');
......@@ -38,125 +33,117 @@
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
});
};
}
Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) {
initFilterDropdown($dropdown, fieldName, searchFields) {
$dropdown.glDropdown({
fieldName,
selectable: true,
filterable: searchFields ? true : false,
fieldName: fieldName,
search: { fields: searchFields },
data: $dropdown.data('data'),
clicked: function() {
return $dropdown.closest('form.filter-form').submit();
}
})
};
}
Todos.prototype.doneClicked = function(e) {
var $this;
doneClicked(e) {
e.preventDefault();
e.stopImmediatePropagation();
$this = $(e.currentTarget);
$this.disable();
const $target = $(e.currentTarget);
$target.disable();
return $.ajax({
type: 'POST',
url: $this.attr('href'),
url: $target.attr('href'),
dataType: 'json',
data: {
'_method': 'delete'
},
success: (function(_this) {
return function(data) {
_this.redirectIfNeeded(data.count);
_this.clearDone($this.closest('li'));
return _this.updateBadges(data);
};
})(this)
success: (data) => {
this.redirectIfNeeded(data.count);
this.clearDone($target.closest('li'));
return this.updateBadges(data);
}
});
};
}
Todos.prototype.allDoneClicked = function(e) {
var $this;
allDoneClicked(e) {
e.preventDefault();
e.stopImmediatePropagation();
$this = $(e.currentTarget);
$this.disable();
$target = $(e.currentTarget);
$target.disable();
return $.ajax({
type: 'POST',
url: $this.attr('href'),
url: $target.attr('href'),
dataType: 'json',
data: {
'_method': 'delete'
},
success: (function(_this) {
return function(data) {
$this.remove();
$('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
return _this.updateBadges(data);
};
})(this)
success: (data) => {
$target.remove();
$('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
return this.updateBadges(data);
}
});
};
}
Todos.prototype.clearDone = function($row) {
var $ul;
$ul = $row.closest('ul');
clearDone($row) {
const $ul = $row.closest('ul');
$row.remove();
if (!$ul.find('li').length) {
return $ul.parents('.panel').remove();
}
};
}
Todos.prototype.updateBadges = function(data) {
updateBadges(data) {
$('.todos-pending .badge, .todos-pending-count').text(data.count);
return $('.todos-done .badge').text(data.done_count);
};
}
Todos.prototype.getTotalPages = function() {
getTotalPages() {
return this.el.data('totalPages');
};
}
Todos.prototype.getCurrentPage = function() {
getCurrentPage() {
return this.el.data('currentPage');
};
}
Todos.prototype.getTodosPerPage = function() {
getTodosPerPage() {
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
if (!total) {
location.reload();
window.location.reload();
return;
}
// Do nothing if no pagination
if (!currPages) {
return;
}
newPages = Math.ceil(total / this.getTodosPerPage());
// Includes query strings
url = location.href;
// If new total of pages is different than we have now
const newPages = Math.ceil(total / this.getTodosPerPage());
let url = location.href;
if (newPages !== currPages) {
// Redirect to previous page if there's one available
if (currPages > 1 && currPage === currPages) {
pageParams = {
const pageParams = {
page: currPages - 1
};
url = gl.utils.mergeUrlParams(pageParams, url);
}
return Turbolinks.visit(url);
}
};
}
Todos.prototype.goToTodoUrl = function(e) {
var todoLink;
todoLink = $(this).data('url');
goToTodoUrl(e) {
const todoLink = $(this).data('url');
if (!todoLink) {
return;
}
......@@ -167,10 +154,8 @@
} else {
return Turbolinks.visit(todoLink);
}
};
return Todos;
})();
}
}
}).call(this);
global.Todos = Todos;
})(window.gl || (window.gl = {}));
(global => {
((global) => {
global.User = class {
constructor(opts) {
this.opts = opts;
constructor({ action }) {
this.action = action;
this.placeProfileAvatarsToTop();
this.initTabs();
this.hideProjectLimitMessage();
......@@ -14,9 +14,9 @@
}
initTabs() {
return new UserTabs({
return new global.UserTabs({
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 @@
$('.js-user-search').each((function(_this) {
return function(i, dropdown) {
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);
options.projectId = $dropdown.data('project-id');
options.showCurrentUser = $dropdown.data('current-user');
showNullUser = $dropdown.data('null-user');
showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id');
......@@ -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> <% } %>');
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({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
var isAuthorFilter;
isAuthorFilter = $('.js-author-search');
......@@ -116,8 +118,11 @@
if (showDivider) {
users.splice(showDivider, 0, "divider");
}
// Send the data back
return callback(users);
callback(users);
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
});
},
filterable: true,
......@@ -127,8 +132,8 @@
},
selectable: true,
fieldName: $dropdown.data('field-name'),
toggleLabel: function(selected) {
if (selected && 'id' in selected) {
toggleLabel: function(selected, el) {
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
if (selected.text) {
return selected.text;
} else {
......@@ -138,6 +143,7 @@
return defaultLabel;
}
},
defaultLabel: defaultLabel,
inputId: 'issue_assignee_id',
hidden: function(e) {
$selectbox.hide();
......@@ -149,7 +155,9 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues: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;
}
if (page === 'projects:boards:show') {
......@@ -167,6 +175,9 @@
return assignTo(selected);
}
},
id: function (user) {
return user.id;
},
renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
username = user.username ? "@" + user.username : "";
......
......@@ -19,10 +19,8 @@
&.diff-collapsed {
padding: 5px;
cursor: pointer;
&:hover {
background-color: $row-hover;
.click-to-expand {
cursor: pointer;
}
}
}
......
......@@ -604,3 +604,9 @@
display: block;
color: $gl-placeholder-color;
}
.dropdown-toggle-text {
&.is-default {
color: $gl-placeholder-color;
}
}
......@@ -26,6 +26,15 @@
padding: 10px $gl-padding;
word-wrap: break-word;
border-radius: 3px 3px 0 0;
cursor: pointer;
&:hover {
background-color: $dark-background-color;
}
.diff-toggle-caret {
padding-right: 6px;
}
&.file-title-clear {
padding-left: 0;
......
......@@ -3,6 +3,8 @@
margin: 0;
margin-bottom: $gl-padding;
font-size: 14px;
position: relative;
z-index: 1;
.flash-notice {
@extend .alert;
......@@ -19,7 +21,8 @@
.flash-notice, .flash-alert {
border-radius: $border-radius-default;
.container-fluid.container-limited.flash-text {
.container-fluid,
.container-fluid.container-limited {
background: transparent;
}
}
......
......@@ -112,11 +112,15 @@ header {
.header-logo {
position: absolute;
left: 50%;
margin-left: -18px;
top: 7px;
transition-duration: .3s;
z-index: 999;
#logo {
position: relative;
left: -50%;
}
svg, img {
height: 36px;
}
......@@ -126,8 +130,12 @@ header {
}
@media (max-width: $screen-xs-max) {
right: 25px;
right: 20px;
left: auto;
#logo {
left: auto;
}
}
}
......
......@@ -142,6 +142,7 @@
transition-duration: .3s;
position: absolute;
top: 0;
cursor: pointer;
&:hover,
&:focus {
......
......@@ -197,6 +197,7 @@ lex
a {
color: inherit;
word-wrap: break-word;
}
}
......
......@@ -107,10 +107,14 @@
.block {
width: 100%;
}
.block-first {
padding: 5px 16px 11px;
&.coverage {
padding: 0 16px 11px;
}
.btn-group-justified {
margin-top: 5px;
}
}
.js-build-variable {
......@@ -214,6 +218,9 @@
.build-detail-row {
margin-bottom: 5px;
&:last-of-type {
margin-bottom: 0;
}
}
.build-light-text {
......
......@@ -59,6 +59,7 @@
}
.encoding-selector,
.soft-wrap-toggle,
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector {
......@@ -67,6 +68,24 @@
font-family: $regular_font;
}
.soft-wrap-toggle {
margin: 0 $btn-side-margin;
.soft-wrap {
display: block;
}
.no-wrap {
display: none;
}
&.soft-wrap-active {
.soft-wrap {
display: none;
}
.no-wrap {
display: block;
}
}
}
.gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
.dropdown {
line-height: 21px;
......
......@@ -68,7 +68,6 @@
}
.groups-header {
@media (min-width: $screen-sm-min) {
.nav-links {
width: 35%;
......@@ -79,3 +78,38 @@
}
}
}
.groups-empty-state {
padding: 50px 100px;
overflow: hidden;
@media (max-width: $screen-md-min) {
padding: 50px 0;
}
svg {
float: right;
@media (max-width: $screen-md-min) {
float: none;
display: block;
width: 250px;
position: relative;
left: 50%;
margin-left: -125px;
}
}
.text-content {
float: left;
width: 460px;
margin-top: 120px;
@media (max-width: $screen-md-min) {
float: none;
margin-top: 60px;
width: auto;
text-align: center;
}
}
}
......@@ -70,7 +70,8 @@
&.ci-success {
color: $gl-success;
a.environment {
a.environment,
a.pipeline {
color: inherit;
}
}
......@@ -349,6 +350,10 @@
.issuable-form-select-holder {
display: inline-block;
width: 250px;
.dropdown-menu-toggle {
width: 100%;
}
}
.table-holder {
......
......@@ -33,6 +33,7 @@
// Issue title
span a {
color: $gl-text-color;
word-wrap: break-word;
}
}
}
......
......@@ -22,6 +22,11 @@
.table.builds {
min-width: 1200px;
.branch-commit {
width: 33%;
}
}
}
......@@ -385,6 +390,8 @@
left: auto;
right: -214px;
top: -9px;
max-height: 245px;
overflow-y: scroll;
a:hover {
.ci-status-text {
......
......@@ -146,7 +146,8 @@
}
.project-repo-btn-group,
.notification-dropdown {
.notification-dropdown,
.project-dropdown {
margin-left: 10px;
}
......@@ -772,6 +773,62 @@ a.allowed-to-merge, a.allowed-to-push {
.dropdown-menu {
width: 300px;
}
&.from .compare-dropdown-toggle {
width: 237px;
}
&.to .compare-dropdown-toggle {
width: 254px;
}
.dropdown-toggle-text {
display: block;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
.compare-ellipsis {
display: inline;
}
@media (max-width: $screen-xs-max) {
.compare-form-group {
.input-group {
width: 100%;
& > .compare-dropdown-toggle {
width: 100%;
}
}
.dropdown-menu {
width: 100%;
}
}
.compare-switch-container {
text-align: center;
padding: 0 0 $gl-padding;
.commits-compare-switch {
float: none;
}
}
.compare-ellipsis {
display: block;
text-align: center;
padding: 0 0 $gl-padding;
}
.commits-compare-btn {
width: 100%;
}
}
.clearable-input {
......@@ -808,4 +865,4 @@ a.allowed-to-merge, a.allowed-to-push {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
\ No newline at end of file
}
......@@ -51,6 +51,7 @@
-webkit-flex-direction: column;
flex-direction: column;
margin-left: 10px;
min-width: 55px;
}
.todo-item {
......@@ -120,6 +121,14 @@
}
}
@media (max-width: $screen-sm-max) {
.todos-filters {
.dropdown-menu-toggle {
width: 135px;
}
}
}
@media (max-width: $screen-xs-max) {
.todo {
.avatar {
......@@ -141,4 +150,14 @@
padding-left: 10px;
}
}
.todos-filters {
.row-content-block {
padding-bottom: 50px;
}
.dropdown-menu-toggle {
width: 100%;
}
}
}
......@@ -14,6 +14,7 @@ module Ci
@config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@jobs = @config_processor.jobs
end
rescue
@error = 'Undefined error'
......
......@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
#
# Returns nil
def prompt_for_two_factor(user)
return locked_user_redirect(user) if user.access_locked?
session[:otp_user_id] = user.id
setup_u2f_authentication(user)
render 'devise/sessions/two_factor'
end
def locked_user_redirect(user)
flash.now[:alert] = 'Invalid Login or password'
render 'devise/sessions/new'
end
def authenticate_with_two_factor
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)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
......@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor
prompt_for_two_factor(user)
end
end
......@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user)
end
......
......@@ -17,21 +17,20 @@ module MembershipActions
end
def leave
@member = membershipable.members.find_by(user_id: current_user) ||
membershipable.requesters.find_by(user_id: current_user)
Members::DestroyService.new(@member, current_user).execute
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
execute(:all)
source_type = @member.real_source_type.humanize(capitalize: false)
source_type = membershipable.class.to_s.humanize(capitalize: false)
notice =
if @member.request?
if member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{@member.source.human_name}\" #{source_type}."
"You left the \"#{membershipable.human_name}\" #{source_type}."
end
log_audit_event(@member, action: :destroy) unless @member.request?
log_audit_event(member, action: :destroy) unless member.request?
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
end
......
......@@ -7,7 +7,7 @@ module SpammableActions
def mark_as_spam
if SpamService.new(spammable).mark_as_spam!
redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully."
else
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
end
......
......@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
@projects = TrendingProjectsFinder.new.execute(current_user)
@projects = TrendingProjectsFinder.new.execute
@projects = filter_projects(@projects)
@projects = @projects.page(params[:page])
......
......@@ -50,11 +50,9 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def destroy
@group_member = @group.members.find_by(id: params[:id]) ||
@group.requesters.find_by(id: params[:id])
member = Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
Members::DestroyService.new(@group_member, current_user).execute
log_audit_event(@group_member, action: :destroy)
log_audit_event(member, action: :destroy)
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
......
class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
before_action :authenticate_admin!
def new
@namespace_id = project_params[:namespace_id]
......@@ -48,8 +47,4 @@ class Import::GitlabProjectsController < Import::BaseController
:path, :namespace_id, :file
)
end
def authenticate_admin!
render_404 unless current_user.is_admin?
end
end
......@@ -33,7 +33,7 @@ module Projects
def issue
@issue ||=
IssuesFinder.new(current_user, project_id: project.id, state: 'all')
IssuesFinder.new(current_user, project_id: project.id)
.execute
.where(iid: params[:id])
.first!
......
class Projects::BoardsController < Projects::ApplicationController
include IssuableCollections
respond_to :html
before_action :authorize_read_board!, only: [:show]
......
......@@ -4,17 +4,25 @@ class Projects::GroupLinksController < Projects::ApplicationController
def index
@group_links = project.project_group_links.all
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << project.group.try(:id)
end
def create
group = Group.find(params[:link_group_id])
return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create(
group: group,
group_access: params[:link_group_access],
expires_at: params[:expires_at]
)
group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
if group
return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create(
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)
end
......
......@@ -19,6 +19,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_commit_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 :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
# Allow read any merge_request
before_action :authorize_read_merge_request!
......@@ -288,7 +289,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def remove_wip
MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request)
MergeRequests::UpdateService.new(project, current_user, wip_event: 'unwip').execute(@merge_request)
redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
notice: "The merge request can now be merged."
......@@ -456,10 +457,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def validates_merge_request
# If source project was removed and merge request for some reason
# wasn't close (Ex. mr from fork to origin)
return invalid_mr if !@merge_request.source_project && @merge_request.open?
# Show git not found page
# if there is no saved commits between source & target branch
if @merge_request.commits.blank?
......@@ -534,7 +531,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def invalid_mr
# Render special view for MR with removed source or target branch
# Render special view for MR with removed target branch
render 'invalid'
end
......@@ -595,4 +592,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@diff_notes_disabled = !@merge_request_diff.latest?
@diffs = @merge_request_diff.diffs(diff_options)
end
def close_merge_request_without_source_project
if !@merge_request.source_project && @merge_request.open?
@merge_request.close
end
end
end
......@@ -65,12 +65,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def destroy
@project_member = @project.members.find_by(id: params[:id]) ||
@project.requesters.find_by(id: params[:id])
member = Members::DestroyService.new(@project, current_user, params).
execute(:all)
Members::DestroyService.new(@project_member, current_user).execute
log_audit_event(@project_member, action: :destroy)
log_audit_event(member, action: :destroy)
respond_to do |format|
format.html do
......
......@@ -138,10 +138,10 @@ class ProjectsController < Projects::ApplicationController
noteable =
case params[:type]
when 'Issue'
IssuesFinder.new(current_user, project_id: @project.id, state: 'all').
IssuesFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'MergeRequest'
MergeRequestsFinder.new(current_user, project_id: @project.id, state: 'all').
MergeRequestsFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'Commit'
@project.commit(params[:type_id])
......
......@@ -184,17 +184,12 @@ class IssuableFinder
end
def by_state(items)
case params[:state]
when 'closed'
items.closed
when 'merged'
items.respond_to?(:merged) ? items.merged : items.closed
when 'all'
items
when 'opened'
items.opened
params[:state] ||= 'all'
if items.respond_to?(params[:state])
items.public_send(params[:state])
else
raise 'You must specify default state'
items
end
end
......
# Finder for retrieving public trending projects in a given time range.
class TrendingProjectsFinder
def execute(current_user, start_date = 1.month.ago)
projects_for(current_user).trending(start_date)
# current_user - The currently logged in User, if any.
# 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
private
def projects_for(current_user)
ProjectsFinder.new.execute(current_user)
def cache_key_for(months)
"trending_projects/#{months}"
end
end
......@@ -283,32 +283,6 @@ module ApplicationHelper
end
end
def state_filters_text_for(entity, project)
titles = {
opened: "Open"
}
entity_title = titles[entity] || entity.to_s.humanize
count =
if project.nil?
nil
elsif current_controller?(:issues)
project.issues.visible_to_user(current_user).send(entity).count
elsif current_controller?(:merge_requests)
project.merge_requests.send(entity).count
end
html = content_tag :span, entity_title
if count.present?
html += " "
html += content_tag :span, number_with_delimiter(count), class: 'badge'
end
html.html_safe
end
def truncate_first_line(message, length = 50)
truncate(message.each_line.first.chomp, length: length) if message
end
......
......@@ -7,7 +7,7 @@ module AuditEventsHelper
if key.match(/^(author|target)_.*/)
""
else
"#{key.to_s} <strong>#{value}</strong>"
"#{key} <strong>#{value}</strong>"
end
end
end
......@@ -44,8 +44,9 @@ module DropdownsHelper
end
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
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.html_safe
end
......
......@@ -8,18 +8,12 @@ module IssuablesHelper
end
def multi_label_name(current_labels, default_label)
# current_labels may be a string from before
if current_labels.is_a?(Array)
if current_labels.count > 1
"#{current_labels[0]} +#{current_labels.count - 1} more"
if current_labels && current_labels.any?
title = current_labels.first.try(:title)
if current_labels.size > 1
"#{title} +#{current_labels.size - 1} more"
else
current_labels[0]
end
elsif current_labels.is_a?(String)
if current_labels.nil? || current_labels.empty?
default_label
else
current_labels
title
end
else
default_label
......@@ -94,6 +88,24 @@ module IssuablesHelper
label_names.join(', ')
end
def issuables_state_counter_text(issuable_type, state)
titles = {
opened: "Open"
}
state_title = titles[state] || state.to_s.humanize
count =
Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
issuables_count_for_state(issuable_type, state)
end
html = content_tag(:span, state_title)
html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
html.html_safe
end
private
def sidebar_gutter_collapsed?
......@@ -111,4 +123,22 @@ module IssuablesHelper
issuable.open? ? :opened : :closed
end
end
def issuables_count_for_state(issuable_type, state)
issuables_finder = public_send("#{issuable_type}_finder")
issuables_finder.params[:state] = state
issuables_finder.execute.page(1).total_count
end
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
def issuables_state_counter_cache_key(issuable_type, state)
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
end
end
......@@ -115,8 +115,9 @@ module LabelsHelper
end
def labels_filter_path
if @project
namespace_project_labels_path(@project.namespace, @project, :json)
project = @target_project || @project
if project
namespace_project_labels_path(project.namespace, project, :json)
else
dashboard_labels_path(:json)
end
......
......@@ -71,8 +71,9 @@ module MilestonesHelper
end
def milestones_filter_dropdown_path
if @project
namespace_project_milestones_path(@project.namespace, @project, :json)
project = @target_project || @project
if project
namespace_project_milestones_path(project.namespace, project, :json)
else
dashboard_milestones_path(:json)
end
......
......@@ -92,12 +92,8 @@ module PageLayoutHelper
end
end
def fluid_layout(enabled = false)
if @fluid_layout.nil?
@fluid_layout = (current_user && current_user.layout == "fluid") || enabled
else
@fluid_layout
end
def fluid_layout
current_user && current_user.layout == "fluid"
end
def blank_container(enabled = false)
......
......@@ -139,7 +139,7 @@ module ProjectsHelper
end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe
content_tag(:select, options, name: "project[project_feature_attributes][#{field}]", id: "project_project_feature_attributes_#{field}", class: "pull-right form-control", data: { field: field }).html_safe
end
private
......
......@@ -57,10 +57,9 @@ module SelectsHelper
end
def select2_tag(id, opts = {})
css_class = ''
css_class << 'multiselect ' if opts[:multiple]
css_class << (opts[:class] || '')
opts[:class] << ' multiselect' if opts[:multiple]
value = opts[:selected] || ''
css_class = opts[:class]
hidden_field_tag(id, value, class: css_class, data: { skip_group: opts[:skip_group], url: autocomplete_groups_path })
end
......
......@@ -114,6 +114,26 @@ module TodosHelper
selected_type ? selected_type[:text] : default_type
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
def show_todo_state?(todo)
......
......@@ -155,7 +155,7 @@ module TreeHelper
def disabled_lock_link(label, title, html_options)
html_options['data-toggle'] = 'tooltip'
html_options[:title] = title
html_options[:class] = "#{html_options[:class].to_s} disabled has-tooltip"
html_options[:class] = "#{html_options[:class]} disabled has-tooltip"
content_tag :span, label, html_options
end
......@@ -163,7 +163,7 @@ module TreeHelper
def enabled_lock_link(label, title, html_options)
html_options['data-toggle'] = 'tooltip'
html_options[:title] = title
html_options[:class] = "#{html_options[:class].to_s} has-tooltip"
html_options[:class] = "#{html_options[:class]} has-tooltip"
link_to label, '#', html_options
end
......
......@@ -3,4 +3,12 @@ class DeviseMailer < Devise::Mailer
default reply_to: Gitlab.config.gitlab.email_reply_to
layout 'devise_mailer'
protected
def subject_for(key)
subject = super
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject
end
end
......@@ -45,7 +45,7 @@ module Emails
@token = token
mail(to: member.invite_email,
subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
def member_invite_accepted_email(member_source_type, member_id)
......
......@@ -93,6 +93,7 @@ class Notify < BaseMailer
subject = ""
subject << "#{@project.name} | " if @project
subject << extra.join(' | ') if extra.present?
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject
end
......
......@@ -196,7 +196,7 @@ module Ci
end
def has_warnings?
builds.latest.ignored.any?
builds.latest.failed_but_allowed.any?
end
def config_processor
......
......@@ -2,7 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
LAST_CONTACT_TIME = 2.hours.ago
LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
......
......@@ -80,7 +80,7 @@ class CommitRange
end
def inspect
%(#<#{self.class}:#{object_id} #{to_s}>)
%(#<#{self.class}:#{object_id} #{self}>)
end
def to_s
......
......@@ -24,7 +24,22 @@ class CommitStatus < ActiveRecord::Base
scope :retried, -> { where.not(id: latest) }
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 :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
......@@ -111,7 +126,7 @@ class CommitStatus < ActiveRecord::Base
end
end
def ignored?
def failed_but_allowed?
allow_failure? && (failed? || canceled?)
end
......
......@@ -8,9 +8,6 @@ module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
members.create(
access_level: Gitlab::Access::DEVELOPER,
user: user,
requested_at: Time.now.utc)
Members::RequestAccessService.new(self, user).execute
end
end
......@@ -8,32 +8,32 @@ module HasStatus
class_methods do
def status_sql
scope = all
scope = if respond_to?(:exclude_ignored)
exclude_ignored
else
all
end
builds = scope.select('count(*)').to_sql
created = scope.created.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
running = scope.running.select('count(*)').to_sql
canceled = scope.canceled.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})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
ELSE 'failed'
END)"
deduce_status
end
def status
all.pluck(self.status_sql).first
all.pluck(status_sql).first
end
def started_at
......@@ -43,6 +43,10 @@ module HasStatus
def finished_at
all.maximum(:finished_at)
end
def all_state_names
state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) }
end
end
included do
......
......@@ -43,19 +43,15 @@ module Mentionable
self
end
def all_references(current_user = nil, text = nil, extractor: nil)
def all_references(current_user = nil, extractor: nil)
extractor ||= Gitlab::ReferenceExtractor.
new(project, current_user)
if text
extractor.analyze(text, author: author)
else
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
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)
end
extractor.analyze(text, options)
end
extractor
......@@ -66,8 +62,8 @@ module Mentionable
end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author, text = nil)
refs = all_references(current_user, text)
def referenced_mentionables(current_user = self.author)
refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits)
# We're using this method instead of Array diffing because that requires
......@@ -77,8 +73,8 @@ module Mentionable
end
# 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)
refs = referenced_mentionables(author, text)
def create_cross_references!(author = self.author, without = [])
refs = referenced_mentionables(author)
# 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
......@@ -97,10 +93,7 @@ module Mentionable
return if changes.empty?
original_text = changes.collect { |_, vals| vals.first }.join(' ')
preexisting = referenced_mentionables(author, original_text)
create_cross_references!(author, preexisting)
create_cross_references!(author)
end
private
......
......@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true
after_save :keep_around_commit
after_save :create_ref
def commit
project.commit(sha)
......@@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base
self == environment.last_deployment
end
def keep_around_commit
project.repository.keep_around(self.sha)
def create_ref
project.repository.create_ref(ref, ref_path)
end
def manual_actions
......@@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base
where.not(id: self.id).
take
end
private
def ref_path
File.join(environment.ref_path, 'deployments', id.to_s)
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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