Merge branch 'master' into members-ui

This commit is contained in:
Phil Hughes 2016-10-06 15:30:34 +01:00
commit 996e802415
246 changed files with 5687 additions and 1918 deletions

View File

@ -207,9 +207,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

View File

@ -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

View File

@ -1,21 +1,21 @@
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 0`
# on 2016-09-14 15:44:53 -0400 using RuboCop version 0.42.0.
# on 2016-10-04 13:16:20 +0200 using RuboCop version 0.43.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 158
# Offense count: 160
Lint/AmbiguousRegexpLiteral:
Enabled: false
# Offense count: 41
# Offense count: 40
# Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition:
Enabled: false
# Offense count: 16
# Offense count: 18
Lint/HandleExceptions:
Enabled: false
@ -23,11 +23,21 @@ Lint/HandleExceptions:
Lint/Loop:
Enabled: false
# Offense count: 16
# Offense count: 19
Lint/ShadowingOuterLocalVariable:
Enabled: false
# Offense count: 49
# Offense count: 9
# Cop supports --auto-correct.
Lint/UnifiedInteger:
Enabled: false
# Offense count: 13
# Cop supports --auto-correct.
Lint/UnneededSplatExpansion:
Enabled: false
# Offense count: 69
# Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
@ -39,32 +49,81 @@ Lint/UnusedBlockArgument:
Lint/UnusedMethodArgument:
Enabled: false
# Offense count: 9
# Cop supports --auto-correct.
Performance/PushSplat:
Enabled: false
# Offense count: 2
# Cop supports --auto-correct.
Performance/RedundantBlockCall:
Enabled: false
# Offense count: 4
# Offense count: 5
# Cop supports --auto-correct.
Performance/RedundantMatch:
Enabled: false
# Offense count: 27
# Offense count: 26
# Cop supports --auto-correct.
# Configuration parameters: MaxKeyValuePairs.
Performance/RedundantMerge:
Enabled: false
# Offense count: 61
# Offense count: 7
RSpec/BeEql:
Enabled: false
# Offense count: 20
# Configuration parameters: CustomIncludeMethods.
RSpec/EmptyExampleGroup:
Enabled: false
# Offense count: 16
RSpec/ExpectActual:
Enabled: false
# Offense count: 34
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example
RSpec/HookArgument:
Enabled: false
# Offense count: 168
RSpec/LeadingSubject:
Enabled: false
# Offense count: 162
RSpec/LetSetup:
Enabled: false
# Offense count: 10
RSpec/MessageChain:
Enabled: false
# Offense count: 714
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: allow, expect
RSpec/MessageExpectation:
Enabled: false
# Offense count: 2423
RSpec/MultipleExpectations:
Max: 36
# Offense count: 1504
RSpec/NamedSubject:
Enabled: false
# Offense count: 1335
# Configuration parameters: MaxNesting.
RSpec/NestedGroups:
Enabled: false
# Offense count: 99
RSpec/SubjectStub:
Enabled: false
# Offense count: 64
Rails/OutputSafety:
Enabled: false
# Offense count: 129
# Offense count: 151
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: strict, flexible
Rails/TimeZone:
@ -77,58 +136,63 @@ Rails/TimeZone:
Rails/Validation:
Enabled: false
# Offense count: 273
# Offense count: 2
# Cop supports --auto-correct.
Security/JSONLoad:
Enabled: false
# Offense count: 284
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: with_first_parameter, with_fixed_indentation
Style/AlignParameters:
Enabled: false
# Offense count: 30
# Offense count: 28
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: always, conditionals
Style/AndOr:
Enabled: false
# Offense count: 50
# Offense count: 52
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
# Offense count: 289
# Offense count: 291
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: braces, no_braces, context_dependent
Style/BracesAroundHashParameters:
Enabled: false
# Offense count: 5
# Offense count: 6
Style/CaseEquality:
Enabled: false
# Offense count: 19
# Offense count: 26
# Cop supports --auto-correct.
Style/ColonMethodCall:
Enabled: false
# Offense count: 3
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: Keywords.
# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
Style/CommentAnnotation:
Enabled: false
# Offense count: 33
# Offense count: 30
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Enabled: false
# Offense count: 881
# Offense count: 957
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing
@ -139,12 +203,12 @@ Style/DotPosition:
Style/DoubleNegation:
Enabled: false
# Offense count: 4
# Offense count: 6
# Cop supports --auto-correct.
Style/EachWithObject:
Enabled: false
# Offense count: 25
# Offense count: 26
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: empty, nil, both
@ -156,24 +220,24 @@ Style/EmptyElse:
Style/EmptyLiteral:
Enabled: false
# Offense count: 135
# Offense count: 140
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Style/ExtraSpacing:
Enabled: false
# Offense count: 7
# Offense count: 6
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: format, sprintf, percent
Style/FormatString:
Enabled: false
# Offense count: 51
# Offense count: 201
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
# Offense count: 9
# Offense count: 11
Style/IfInsideElse:
Enabled: false
@ -183,21 +247,21 @@ Style/IfInsideElse:
Style/IfUnlessModifier:
Enabled: false
# Offense count: 52
# Offense count: 53
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
Style/IndentArray:
Enabled: false
# Offense count: 97
# Offense count: 95
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
Style/IndentHash:
Enabled: false
# Offense count: 12
# Offense count: 29
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: line_count_dependent, lambda, literal
@ -209,7 +273,7 @@ Style/Lambda:
Style/LineEndConcatenation:
Enabled: false
# Offense count: 13
# Offense count: 15
# Cop supports --auto-correct.
Style/MethodCallParentheses:
Enabled: false
@ -218,7 +282,7 @@ Style/MethodCallParentheses:
Style/MethodMissing:
Enabled: false
# Offense count: 85
# Offense count: 95
# Cop supports --auto-correct.
Style/MutableConstant:
Enabled: false
@ -235,14 +299,14 @@ Style/NestedParenthesizedCalls:
Style/Next:
Enabled: false
# Offense count: 8
# Offense count: 12
# Cop supports --auto-correct.
# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
# SupportedOctalStyles: zero_with_o, zero_only
Style/NumericLiteralPrefix:
Enabled: false
# Offense count: 64
# Offense count: 53
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: predicate, comparison
@ -254,7 +318,7 @@ Style/NumericPredicate:
Style/ParallelAssignment:
Enabled: false
# Offense count: 264
# Offense count: 294
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
@ -272,7 +336,7 @@ Style/PercentQLiterals:
Style/PerlBackrefs:
Enabled: false
# Offense count: 35
# Offense count: 38
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
@ -280,7 +344,7 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
# Offense count: 27
# Offense count: 26
# Cop supports --auto-correct.
Style/PreferredHashMethods:
Enabled: false
@ -312,12 +376,12 @@ Style/RedundantException:
Style/RedundantFreeze:
Enabled: false
# Offense count: 408
# Offense count: 427
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
# Offense count: 93
# Offense count: 97
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
@ -329,7 +393,12 @@ Style/RegexpLiteral:
Style/RescueModifier:
Enabled: false
# Offense count: 5
# Offense count: 114
# Cop supports --auto-correct.
Style/SafeNavigation:
Enabled: false
# Offense count: 7
# Cop supports --auto-correct.
Style/SelfAssignment:
Enabled: false
@ -346,7 +415,7 @@ Style/SingleLineBlockParams:
Style/SingleLineMethods:
Enabled: false
# Offense count: 124
# Offense count: 125
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space
@ -359,19 +428,19 @@ Style/SpaceBeforeBlockBraces:
Style/SpaceBeforeFirstArg:
Enabled: false
# Offense count: 141
# Offense count: 145
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
Style/SpaceInsideBlockBraces:
Enabled: false
# Offense count: 96
# Offense count: 99
# Cop supports --auto-correct.
Style/SpaceInsideBrackets:
Enabled: false
# Offense count: 62
# Offense count: 65
# Cop supports --auto-correct.
Style/SpaceInsideParens:
Enabled: false
@ -381,21 +450,21 @@ Style/SpaceInsideParens:
Style/SpaceInsidePercentLiteralDelimiters:
Enabled: false
# Offense count: 40
# Offense count: 41
# Cop supports --auto-correct.
# Configuration parameters: SupportedStyles.
# SupportedStyles: use_perl_names, use_english_names
Style/SpecialGlobalVars:
EnforcedStyle: use_perl_names
# Offense count: 30
# Offense count: 31
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Enabled: false
# Offense count: 32
# Offense count: 33
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
@ -409,7 +478,7 @@ Style/SymbolProc:
Style/TernaryParentheses:
Enabled: false
# Offense count: 24
# Offense count: 29
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
# SupportedStyles: comma, consistent_comma, no_comma

View File

@ -1,6 +1,7 @@
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)
@ -8,29 +9,44 @@ v 8.13.0 (unreleased)
- 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
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- 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
- API: Multi-file commit !6096 (mahcsig)
- Revert "Label list shows all issues (opened or closed) with that label"
- Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API
- 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)
- MergeRequest#new form load diff asynchronously
- 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.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- 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)
@ -40,17 +56,35 @@ v 8.13.0 (unreleased)
- 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)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Grouped pipeline dropdown is a scrollable container
v 8.12.4 (unreleased)
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. (lukehowell)
v 8.12.5 (unreleased)
v 8.12.4
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
- Fix padding in build sidebar. !6506
- Changed compare dropdowns to dropdowns with isolated search input. !6550
- Fix race condition on LFS Token. !6592
- Fix type mismatch bug when closing Jira issue. !6619
- Fix lint-doc error. !6623
- Skip wiki creation when GitHub project has wiki enabled. !6665
- Fix issues importing services via Import/Export. !6667
- Restrict failed login attempts for users with 2FA enabled. !6668
- Fix failed project deletion when feature visibility set to private. !6688
- Prevent claiming associated model IDs via import.
- Set GitLab project exported file permissions to owner only
v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves
v 8.12.2 (unreleased)
v 8.12.2
- Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination
- Fix "Create project" button layout when visibility options are restricted
@ -64,6 +98,7 @@ v 8.12.2 (unreleased)
- 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
@ -104,6 +139,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
@ -144,6 +180,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

View File

@ -130,7 +130,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'
@ -231,7 +231,7 @@ gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
gem 'sentry-raven', '~> 1.1.0'
gem 'sentry-raven', '~> 2.0.0'
gem 'premailer-rails', '~> 1.9.0'
@ -295,7 +295,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

View File

@ -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)
@ -487,7 +487,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)
@ -620,7 +620,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)
@ -665,8 +665,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)
@ -802,7 +802,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)
@ -938,7 +938,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)
@ -948,7 +948,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)
@ -986,4 +986,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.13.1
1.13.2

View File

@ -1,115 +0,0 @@
(function() {
this.LabelManager = (function() {
LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
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');
this.prioritizedLabels.sortable({
items: 'li',
placeholder: 'list-placeholder',
axis: 'y',
update: this.onPrioritySortUpdate.bind(this)
});
this.bindEvents();
}
LabelManager.prototype.bindEvents = function() {
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
};
LabelManager.prototype.onTogglePriorityClick = function(e) {
var $btn, $label, $tooltip, _this, action;
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')));
$tooltip.tooltip('destroy');
return _this.toggleLabelPriority($label, action);
};
LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) {
var $from, $target, _this, url, xhr;
if (persistState == null) {
persistState = true;
}
_this = this;
url = $label.find('.js-toggle-priority').data('url');
$target = this.prioritizedLabels;
$from = this.otherLabels;
// Optimistic update
if (action === 'remove') {
$target = this.otherLabels;
$from = this.prioritizedLabels;
}
if ($from.find('li').length === 1) {
$from.find('.empty-message').removeClass('hidden');
}
if (!$target.find('li').length) {
$target.find('.empty-message').addClass('hidden');
}
$label.detach().appendTo($target);
// Return if we are not persisting state
if (!persistState) {
return;
}
if (action === 'remove') {
xhr = $.ajax({
url: url,
type: 'DELETE'
});
// Restore empty message
if (!$from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
} else {
xhr = this.savePrioritySort($label, action);
}
return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
};
LabelManager.prototype.onPrioritySortUpdate = function() {
var xhr;
xhr = this.savePrioritySort();
return xhr.fail(function() {
return new Flash(this.errorMessage, 'alert');
});
};
LabelManager.prototype.savePrioritySort = function() {
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';
this.toggleLabelPriority($label, action, false);
return new Flash(this.errorMessage, 'alert');
};
LabelManager.prototype.getSortedLabelsIds = function() {
var sortedIds;
sortedIds = [];
this.prioritizedLabels.find('li').each(function() {
return sortedIds.push($(this).data('id'));
});
return sortedIds;
};
return LabelManager;
})();
}).call(this);

View File

@ -0,0 +1,106 @@
((global) => {
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',
axis: 'y',
update: this.onPrioritySortUpdate.bind(this)
});
this.bindEvents();
}
bindEvents() {
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
}
onTogglePriorityClick(e) {
e.preventDefault();
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);
}
toggleLabelPriority($label, action, persistState) {
if (persistState == null) {
persistState = true;
}
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;
}
if ($from.find('li').length === 1) {
$from.find('.empty-message').removeClass('hidden');
}
if (!$target.find('li').length) {
$target.find('.empty-message').addClass('hidden');
}
$label.detach().appendTo($target);
// Return if we are not persisting state
if (!persistState) {
return;
}
if (action === 'remove') {
xhr = $.ajax({
url,
type: 'DELETE'
});
// Restore empty message
if (!$from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
} else {
xhr = this.savePrioritySort($label, action);
}
return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
}
onPrioritySortUpdate() {
const xhr = this.savePrioritySort();
return xhr.fail(function() {
return new Flash(this.errorMessage, 'alert');
});
}
savePrioritySort() {
return $.post({
url: this.prioritizedLabels.data('url'),
data: {
label_ids: this.getSortedLabelsIds()
}
});
}
rollbackLabelPosition($label, originalAction) {
const action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false);
return new Flash(this.errorMessage, 'alert');
}
getSortedLabelsIds() {
const sortedIds = [];
this.prioritizedLabels.find('li').each(function() {
sortedIds.push($(this).data('id'));
});
return sortedIds;
}
}
gl.LabelManager = LabelManager;
})(window.gl || (window.gl = {}));

View File

@ -5,7 +5,7 @@
namespacesPath: "/api/:version/namespaces.json",
groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true",
labelsPath: "/api/:version/projects/:id/labels",
labelsPath: "/:namespace_path/:project_path/labels",
licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
@ -23,12 +23,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"
@ -65,13 +66,14 @@
return callback(projects);
});
},
newLabel: function(project_id, data, callback) {
newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id);
.replace(':namespace_path', namespace_path)
.replace(':project_path', project_path);
return $.ajax({
url: url,
type: "POST",
data: data,
data: {'label': data},
dataType: "json"
}).done(function(label) {
return callback(label);

View File

@ -1,46 +0,0 @@
/*= 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);

View File

@ -0,0 +1,40 @@
/*= 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 = {}));

View File

@ -18,6 +18,6 @@
return BlobGitignoreSelector;
})(TemplateSelector);
})(gl.TemplateSelector);
}).call(this);

View File

@ -23,6 +23,6 @@
return BlobLicenseSelector;
})(TemplateSelector);
})(gl.TemplateSelector);
}).call(this);

View File

@ -1,25 +0,0 @@
(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);

View File

@ -0,0 +1,21 @@
((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 = {}));

View File

@ -1,100 +0,0 @@
(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);

View File

@ -0,0 +1,102 @@
((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 = {}));

View File

@ -23,13 +23,13 @@
})(this));
this.initModePanesAndLinks();
this.initSoftWrap();
new BlobLicenseSelectors({
new gl.BlobLicenseSelectors({
editor: this.editor
});
new BlobGitignoreSelectors({
editor: this.editor
});
new BlobCiYamlSelectors({
new gl.BlobCiYamlSelectors({
editor: this.editor
});
}

View File

@ -3,8 +3,7 @@ $(() => {
$('.js-new-board-list').each(function () {
const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {

View File

@ -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')), ' '));
}
};

View File

@ -1,8 +1,9 @@
(function (w) {
class CreateLabelDropdown {
constructor ($el, projectId) {
constructor ($el, namespacePath, projectPath) {
this.$el = $el;
this.projectId = projectId;
this.namespacePath = namespacePath;
this.projectPath = projectPath;
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
this.$cancelButton = $('.js-cancel-label-btn', this.$el);
this.$newLabelField = $('#new_label_name', this.$el);
@ -91,8 +92,8 @@
e.preventDefault();
e.stopPropagation();
Api.newLabel(this.projectId, {
name: this.$newLabelField.val(),
Api.newLabel(this.namespacePath, this.projectPath, {
title: this.$newLabelField.val(),
color: this.$newColorField.val()
}, (label) => {
this.$newLabelCreateButton.enable();

View File

@ -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];

View File

@ -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':
@ -279,7 +283,7 @@
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
return new gl.SearchAutocomplete();
}
};

View File

@ -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();

View File

@ -5,14 +5,15 @@
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
return function(i, select) {
var skip_ldap;
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 Api.groups(query.term, skip_ldap, function(groups) {
return Api.groups(query.term, skip_ldap, skip_groups, function(groups) {
var data;
data = {
results: groups

View File

@ -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) {

View File

@ -1,13 +1,10 @@
(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 = {}));

View File

@ -4,9 +4,11 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected;
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespace-path');
projectPath = $dropdown.data('project-path');
labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected');
@ -15,6 +17,7 @@
}
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
defaultLabel = $dropdown.data('default-label');
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
@ -24,6 +27,9 @@
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
fieldName = $dropdown.data('field-name');
useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
propertyName = useId ? 'id' : 'title';
initialSelected = $selectbox
.find('input[name="' + $dropdown.data('field-name') + '"]')
.map(function () {
@ -40,12 +46,12 @@
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
}
saveLabelData = function() {
var data, selected;
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
return this.value;
}).get();
@ -75,7 +81,8 @@
if (data.labels.length) {
template = labelHTMLTemplate(data);
labelCount = data.labels.length;
} else {
}
else {
template = labelNoneHTMLTemplate;
}
$value.removeAttr('style').html(template);
@ -92,7 +99,8 @@
}
labelTooltipTitle = labelTitles.join(', ');
} else {
}
else {
labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy');
}
@ -114,6 +122,7 @@
});
};
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
url: labelUrl
@ -133,23 +142,29 @@
};
}).value();
if ($dropdown.hasClass('js-extra-options')) {
var extraData = [];
if (showNo) {
data.unshift({
extraData.unshift({
id: 0,
title: 'No Label'
});
}
if (showAny) {
data.unshift({
extraData.unshift({
isAny: true,
title: 'Any Label'
});
}
if (data.length > 2) {
data.splice(2, 0, 'divider');
if (extraData.length) {
extraData.push('divider');
data = extraData.concat(data);
}
}
return callback(data);
callback(data);
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
});
},
renderRow: function(label, instance) {
@ -157,7 +172,7 @@
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
removesAll = label.id === 0 || (label.id == null);
removesAll = label.id <= 0 || (label.id == null);
if ($dropdown.hasClass('js-filter-bulk-update')) {
indeterminate = instance.indeterminateIds;
active = instance.activeIds;
@ -194,14 +209,16 @@
return color + " " + percentFirst + "%," + color + " " + percentSecond + "% ";
}).join(',');
color = "linear-gradient(" + color + ")";
} else {
}
else {
if (label.color != null) {
color = label.color[0];
}
}
if (color) {
colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
} else {
}
else {
colorEl = '';
}
// We need to identify which items are actually labels
@ -219,30 +236,46 @@
},
selectable: true,
filterable: true,
selected: $dropdown.data('selected') || [],
toggleLabel: function(selected, el) {
var selected_labels;
selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active');
if (selected && (selected.title != null)) {
if (selected_labels.length > 1) {
return selected.title + " +" + (selected_labels.length - 1) + " more";
} else {
return selected.title;
}
} else if (!selected && selected_labels.length !== 0) {
if (selected_labels.length > 1) {
return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more";
} else if (selected_labels.length === 1) {
return $(selected_labels).text();
}
} else {
var isSelected = el !== null ? el.hasClass('is-active') : false;
var title = selected.title;
var selectedLabels = this.selected;
if (selected.id === 0) {
this.selected = [];
return 'No Label';
}
else if (isSelected) {
this.selected.push(title);
}
else {
var index = this.selected.indexOf(title);
this.selected.splice(index, 1);
}
if (selectedLabels.length === 1) {
return selectedLabels;
}
else if (selectedLabels.length) {
return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
}
else {
return defaultLabel;
}
},
fieldName: $dropdown.data('field-name'),
id: function(label) {
if (label.id <= 0) return;
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
return label.id;
}
if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
return label.title;
} else {
}
else {
return label.id;
}
},
@ -254,6 +287,11 @@
$selectbox.hide();
// display:block overrides the hide-collapse rule
$value.removeAttr('style');
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
return;
}
if (page === 'projects:boards:show') {
return;
}
@ -261,9 +299,11 @@
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
}
else if ($dropdown.hasClass('js-filter-submit')) {
$dropdown.closest('form').submit();
} else {
}
else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
}
@ -280,18 +320,28 @@
clicked: function(label, $el, e) {
var isIssueIndex, isMRIndex, page;
_this.enableBulkLabelDropdown();
if ($dropdown.hasClass('js-filter-bulk-update')) {
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown.parent()
.find('.dropdown-clear-active')
.removeClass('is-active')
}
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
return;
}
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
if (page === 'projects:boards:show') {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
} else if ($el.hasClass('is-active')) {
}
else if ($el.hasClass('is-active')) {
gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
} else {
}
else {
var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
filters = filters.filter(function (filteredLabel) {
return filteredLabel !== label.title;
@ -302,17 +352,21 @@
gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault();
return;
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
}
else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form'));
}
} else if ($dropdown.hasClass('js-filter-submit')) {
}
else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else {
}
else {
if ($dropdown.hasClass('js-multiselect')) {
} else {
}
else {
return saveLabelData();
}
}

View File

@ -38,6 +38,11 @@
gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0];
};
gl.utils.parseUrl = function (url) {
var parser = document.createElement('a');
parser.href = url;
return parser;
};
return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor;
if (!time) {

View File

@ -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);
}

View File

@ -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);
})
}

View File

@ -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() {

View File

@ -56,9 +56,14 @@
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;
this.buildsLoaded = this.opts.buildsLoaded || false;
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
@ -70,7 +75,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 +95,15 @@
if (action === 'commits') {
this.loadCommits($target.attr('href'));
this.expandView();
} else if (action === 'diffs') {
this.resetViewContainer();
} else if (this.isDiffAction(action)) {
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 +111,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 +143,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
@ -156,8 +173,9 @@
action = 'notes';
}
this.currentAction = action;
// Remove a trailing '/commits' or '/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
// Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') {
new_state += "/" + action;
@ -196,8 +214,13 @@
if (this.diffsLoaded) {
return;
}
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
var url = gl.utils.parseUrl(source);
return this._get({
url: (source + ".json") + this._location.search,
url: (url.pathname + ".json") + this._location.search,
success: (function(_this) {
return function(data) {
$('#diffs').html(data.html);
@ -209,7 +232,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.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer();
}
_this.diffsLoaded = true;
@ -308,11 +331,25 @@
MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
// Returns diff view type
};
MergeRequestTabs.prototype.isDiffAction = function(action) {
return action === 'diffs' || action === 'new/diffs'
};
MergeRequestTabs.prototype.expandViewContainer = function() {
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() {

View File

@ -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') {

View File

@ -1,47 +1,45 @@
(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 = {}));

View File

@ -1,106 +0,0 @@
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
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");
}
});
this.bindEvents();
cropOpts = {
filename: '.js-avatar-filename',
previewImage: '.avatar-image .avatar',
modalCrop: '.modal-profile-crop',
pickImageEl: '.js-choose-user-avatar-button',
uploadImageBtn: '.js-upload-user-avatar',
modalCropImg: '.modal-profile-crop-image'
};
this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
}
Profile.prototype.bindEvents = function() {
return this.form.on('submit', this.onSubmitForm);
};
Profile.prototype.onSubmitForm = function(e) {
e.preventDefault();
return this.saveForm();
};
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'),
data: formData,
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() {
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?$/);
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') {
return new Profile();
}
});
}).call(this);

View File

@ -0,0 +1,100 @@
((global) => {
class Profile {
constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this);
this.form = form || $('.edit-user');
this.bindEvents();
this.initAvatarGlCrop();
}
initAvatarGlCrop() {
const cropOpts = {
filename: '.js-avatar-filename',
previewImage: '.avatar-image .avatar',
modalCrop: '.modal-profile-crop',
pickImageEl: '.js-choose-user-avatar-button',
uploadImageBtn: '.js-upload-user-avatar',
modalCropImg: '.modal-profile-crop-image'
};
this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
}
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();
}
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();
if (avatarBlob != null) {
formData.append('user[avatar]', avatarBlob, 'avatar.png');
}
return $.ajax({
url: this.form.attr('action'),
type: this.form.attr('method'),
data: formData,
dataType: "json",
processData: false,
contentType: false,
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();
}
});
}
}
$(function() {
$(document).on('focusout.ssh_key', '#key_key', function() {
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 (global.utils.getPagePath() === 'profiles') {
return new Profile();
}
});
})(window.gl || (window.gl = {}));

View File

@ -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;

View File

@ -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'
});

View File

@ -1,30 +1,21 @@
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
((global) => {
this.SearchAutocomplete = (function() {
var KEYCODE;
const KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
ENTER: 13,
UP: 38,
DOWN: 40
};
KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
ENTER: 13,
UP: 38,
DOWN: 40
};
function SearchAutocomplete(opts) {
var ref, ref1, ref2, ref3, ref4;
if (opts == null) {
opts = {};
}
this.onSearchInputBlur = bind(this.onSearchInputBlur, this);
this.onClearInputClick = bind(this.onClearInputClick, this);
this.onSearchInputFocus = bind(this.onSearchInputFocus, this);
this.onSearchInputClick = bind(this.onSearchInputClick, this);
this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this);
this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this);
this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || '';
// Dropdown Element
class SearchAutocomplete {
constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
this.bindEventContext();
this.wrap = wrap || $('.search');
this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
this.dropdown = this.wrap.find('.dropdown');
this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge');
@ -46,19 +37,27 @@
}
// Finds an element inside wrapper element
SearchAutocomplete.prototype.getElement = function(selector) {
bindEventContext() {
this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
this.onClearInputClick = this.onClearInputClick.bind(this);
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
this.onSearchInputClick = this.onSearchInputClick.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
}
getElement(selector) {
return this.wrap.find(selector);
};
}
SearchAutocomplete.prototype.saveOriginalState = function() {
saveOriginalState() {
return this.originalState = this.serializeState();
};
}
SearchAutocomplete.prototype.saveTextLength = function() {
saveTextLength() {
return this.lastTextLength = this.searchInput.val().length;
};
}
SearchAutocomplete.prototype.createAutocomplete = function() {
createAutocomplete() {
return this.searchInput.glDropdown({
filterInputBlur: false,
filterable: true,
@ -73,9 +72,9 @@
selectable: true,
clicked: this.onClick.bind(this)
});
};
}
SearchAutocomplete.prototype.getData = function(term, callback) {
getData(term, callback) {
var _this, contents, jqXHR;
_this = this;
if (!term) {
@ -138,9 +137,9 @@
}).always(function() {
return _this.loadingSuggestions = false;
});
};
}
SearchAutocomplete.prototype.getCategoryContents = function() {
getCategoryContents() {
var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
userId = gon.current_user_id;
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
@ -173,9 +172,9 @@
items.splice(0, 1);
}
return items;
};
}
SearchAutocomplete.prototype.serializeState = function() {
serializeState() {
return {
// Search Criteria
search_project_id: this.projectInputEl.val(),
@ -186,9 +185,9 @@
// Location badge
_location: this.locationBadgeEl.text()
};
};
}
SearchAutocomplete.prototype.bindEvents = function() {
bindEvents() {
this.searchInput.on('keydown', this.onSearchInputKeyDown);
this.searchInput.on('keyup', this.onSearchInputKeyUp);
this.searchInput.on('click', this.onSearchInputClick);
@ -200,9 +199,9 @@
return _this.searchInput.focus();
};
})(this));
};
}
SearchAutocomplete.prototype.enableAutocomplete = function() {
enableAutocomplete() {
var _this;
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
@ -216,12 +215,12 @@
}
};
SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
// Saves last length of the entered text
onSearchInputKeyDown() {
return this.saveTextLength();
};
}
SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) {
onSearchInputKeyUp(e) {
switch (e.keyCode) {
case KEYCODE.BACKSPACE:
// when trying to remove the location badge
@ -259,54 +258,53 @@
}
}
this.wrap.toggleClass('has-value', !!e.target.value);
};
}
// Avoid falsy value to be returned
SearchAutocomplete.prototype.onSearchInputClick = function(e) {
// Prevents closing the dropdown menu
onSearchInputClick(e) {
return e.stopImmediatePropagation();
};
}
SearchAutocomplete.prototype.onSearchInputFocus = function() {
onSearchInputFocus() {
this.isFocused = true;
this.wrap.addClass('search-active');
if (this.getValue() === '') {
return this.getData();
}
};
}
SearchAutocomplete.prototype.getValue = function() {
getValue() {
return this.searchInput.val();
};
}
SearchAutocomplete.prototype.onClearInputClick = function(e) {
onClearInputClick(e) {
e.preventDefault();
return this.searchInput.val('').focus();
};
}
SearchAutocomplete.prototype.onSearchInputBlur = function(e) {
onSearchInputBlur(e) {
this.isFocused = false;
this.wrap.removeClass('search-active');
// If input is blank then restore state
if (this.searchInput.val() === '') {
return this.restoreOriginalState();
}
};
}
SearchAutocomplete.prototype.addLocationBadge = function(item) {
addLocationBadge(item) {
var badgeText, category, value;
category = item.category != null ? item.category + ": " : '';
value = item.value != null ? item.value : '';
badgeText = "" + category + value;
this.locationBadgeEl.text(badgeText).show();
return this.wrap.addClass('has-location-badge');
};
}
SearchAutocomplete.prototype.hasLocationBadge = function() {
hasLocationBadge() {
return this.wrap.is('.has-location-badge');
};
SearchAutocomplete.prototype.restoreOriginalState = function() {
restoreOriginalState() {
var i, input, inputs, len;
inputs = Object.keys(this.originalState);
for (i = 0, len = inputs.length; i < len; i++) {
@ -320,13 +318,13 @@
value: this.originalState._location
});
}
};
}
SearchAutocomplete.prototype.badgePresent = function() {
badgePresent() {
return this.locationBadgeEl.length;
};
}
SearchAutocomplete.prototype.resetSearchState = function() {
resetSearchState() {
var i, input, inputs, len, results;
inputs = Object.keys(this.originalState);
results = [];
@ -339,30 +337,30 @@
results.push(this.getElement("#" + input).val(''));
}
return results;
};
}
SearchAutocomplete.prototype.removeLocationBadge = function() {
removeLocationBadge() {
this.locationBadgeEl.hide();
this.resetSearchState();
this.wrap.removeClass('has-location-badge');
return this.disableAutocomplete();
};
}
SearchAutocomplete.prototype.disableAutocomplete = function() {
disableAutocomplete() {
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
this.searchInput.addClass('disabled');
this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
this.restoreMenu();
}
};
}
SearchAutocomplete.prototype.restoreMenu = function() {
restoreMenu() {
var html;
html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
return this.dropdownContent.html(html);
};
SearchAutocomplete.prototype.onClick = function(item, $el, e) {
onClick(item, $el, e) {
if (location.pathname.indexOf(item.url) !== -1) {
e.preventDefault();
if (!this.badgePresent) {
@ -385,9 +383,9 @@
}
};
return SearchAutocomplete;
}
})();
global.SearchAutocomplete = SearchAutocomplete;
$(function() {
var $projectOptionsDataEl = $('.js-search-project-options');
@ -408,16 +406,16 @@
if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {};
var groupPath = $groupOptionsDataEl.data('group-path');
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
issuesPath: $groupOptionsDataEl.data('issues-path'),
mrPath: $groupOptionsDataEl.data('mr-path')
};
}
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
issuesPath: $dashboardOptionsDataEl.data('issues-path'),
@ -426,4 +424,4 @@
}
});
}).call(this);
})(window.gl || (window.gl = {}));

View File

@ -1,7 +1,7 @@
/*= 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 = {}));

View File

@ -1,12 +1,12 @@
((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 = {}));

View File

@ -1,34 +1,29 @@
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
((global) => {
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');
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 = {}));

View File

@ -1,7 +1,7 @@
(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
});
}

View File

@ -1,188 +0,0 @@
// 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);

View File

@ -0,0 +1,162 @@
/*
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 = {}));

View File

@ -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 : "";

View File

@ -604,3 +604,9 @@
display: block;
color: $gl-placeholder-color;
}
.dropdown-toggle-text {
&.is-default {
color: $gl-placeholder-color;
}
}

View File

@ -21,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;
}
}
@ -35,12 +36,6 @@
}
}
.content-wrapper {
.flash-notice .container-fluid {
background-color: transparent;
}
}
@media (max-width: $screen-md-min) {
ul.notes {
.flash-container.timeline-content {

View File

@ -350,6 +350,10 @@
.issuable-form-select-holder {
display: inline-block;
width: 250px;
.dropdown-menu-toggle {
width: 100%;
}
}
.table-holder {

View File

@ -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 {

View File

@ -146,7 +146,8 @@
}
.project-repo-btn-group,
.notification-dropdown {
.notification-dropdown,
.project-dropdown {
margin-left: 10px;
}

View 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%;
}
}
}

View File

@ -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

View File

@ -15,18 +15,17 @@ 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
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

View File

@ -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])

View File

@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def destroy
@group_member = @group.members.find_by(id: params[:id]) ||
@group.requesters.find_by(id: params[:id])
Members::DestroyService.new(@group_member, current_user).execute
Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }

View File

@ -1,4 +1,6 @@
class Projects::BoardsController < Projects::ApplicationController
include IssuableCollections
respond_to :html
before_action :authorize_read_board!, only: [:show]

View File

@ -5,17 +5,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)
group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
project.project_group_links.create(
group: group,
group_access: params[:link_group_access],
expires_at: params[:expires_at]
)
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

View File

@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController
@label = @project.labels.create(label_params)
if @label.valid?
redirect_to namespace_project_labels_path(@project.namespace, @project)
respond_to do |format|
format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) }
format.json { render json: @label }
end
else
render 'new'
respond_to do |format|
format.html { render 'new' }
format.json { render json: { message: @label.errors.messages }, status: 400 }
end
end
end

View File

@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
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]
before_action :apply_diff_view_cookie!, only: [:new_diffs]
before_action :build_merge_request, only: [:new, :new_diffs]
# Allow read any merge_request
before_action :authorize_read_merge_request!
@ -210,29 +212,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def new
apply_diff_view_cookie!
define_new_vars
end
build_merge_request
@noteable = @merge_request
def new_diffs
respond_to do |format|
format.html do
define_new_vars
render "new"
end
format.json do
@diffs = if @merge_request.can_be_created
@merge_request.diffs(diff_options)
else
[]
end
@diff_notes_disabled = true
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@diffs = @merge_request.diffs(diff_options) if @merge_request.compare
@diff_notes_disabled = true
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
end
end
end
def create
@ -490,6 +489,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
)
end
def define_new_vars
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
end
def invalid_mr
# Render special view for MR with removed target branch
render 'invalid'
@ -521,7 +541,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end
def compared_diff_version

View File

@ -59,10 +59,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def destroy
@project_member = @project.members.find_by(id: params[:id]) ||
@project.requesters.find_by(id: params[:id])
Members::DestroyService.new(@project_member, current_user).execute
Members::DestroyService.new(@project, current_user, params).
execute(:all)
respond_to do |format|
format.html do

View File

@ -1,11 +1,16 @@
# 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

View File

@ -40,8 +40,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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -49,12 +49,10 @@ 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] || ''
hidden_field_tag(id, value, class: css_class)
hidden_field_tag(id, value, opts)
end
private

View File

@ -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)

View File

@ -196,7 +196,7 @@ module Ci
end
def has_warnings?
builds.latest.ignored.any?
builds.latest.failed_but_allowed.any?
end
def config_processor
@ -251,9 +251,8 @@ module Ci
Ci::ProcessPipelineService.new(project, user).execute(self)
end
def build_updated
def update_status
with_lock do
reload
case latest_builds_status
when 'pending' then enqueue
when 'running' then run

View File

@ -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]

View File

@ -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) }
@ -69,13 +84,18 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now
end
after_transition any => [:success, :failed, :canceled] do |commit_status|
commit_status.pipeline.try(:process!)
true
end
after_transition do |commit_status, transition|
commit_status.pipeline.try(:build_updated) unless transition.loopback?
commit_status.pipeline.try do |pipeline|
break if transition.loopback?
if commit_status.complete?
ProcessPipelineWorker.perform_async(pipeline.id)
end
UpdatePipelineWorker.perform_async(pipeline.id)
end
true
end
after_transition [:created, :pending, :running] => :success do |commit_status|
@ -111,7 +131,7 @@ class CommitStatus < ActiveRecord::Base
end
end
def ignored?
def failed_but_allowed?
allow_failure? && (failed? || canceled?)
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base
def update_merge_request_metrics?
self.name == "production"
end
def ref_path
"refs/environments/#{Shellwords.shellescape(name)}"
end
end

View File

@ -328,13 +328,15 @@ class Event < ActiveRecord::Base
def reset_project_activity
return unless project
# Don't even bother obtaining a lock if the last update happened less than
# 60 minutes ago.
# Don't bother updating if we know the project was updated recently.
return if recent_update?
return unless try_obtain_lease
project.update_column(:last_activity_at, created_at)
# At this point it's possible for multiple threads/processes to try to
# update the project. Only one query should actually perform the update,
# hence we add the extra WHERE clause for last_activity_at.
Project.unscoped.where(id: project_id).
where('last_activity_at > ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
update_all(last_activity_at: created_at)
end
private
@ -342,11 +344,4 @@ class Event < ActiveRecord::Base
def recent_update?
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
end
def try_obtain_lease
Gitlab::ExclusiveLease.
new("project:update_last_activity_at:#{project.id}",
timeout: RESET_PROJECT_ACTIVITY_INTERVAL.to_i).
try_obtain
end
end

View File

@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars
# when creating new merge request
attr_accessor :can_be_created, :compare_commits, :compare
attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
state_machine :state, initial: :opened do
event :close do
@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
merge_request_diff.size
diffs(diff_options).size
end
def diff_base_commit
@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base
# `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user = self.author)
return if project.has_external_issue_tracker?
transaction do
self.merge_requests_closing_issues.delete_all
closes_issues(current_user).each do |issue|
self.merge_requests_closing_issues.create!(issue: issue)
end

View File

@ -380,6 +380,7 @@ class Project < ActiveRecord::Base
SELECT project_id, COUNT(*) AS amount
FROM notes
WHERE created_at >= #{sanitize(since)}
AND system IS FALSE
GROUP BY project_id
) join_note_counts ON projects.id = join_note_counts.project_id"

View File

@ -20,7 +20,10 @@ class ProjectFeature < ActiveRecord::Base
FEATURES = %i(issues merge_requests wiki snippets builds)
belongs_to :project
# Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) }
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_access_level, value: ENABLED, allows_nil: false

View File

@ -838,6 +838,52 @@ class Repository
end
end
def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref|
index = rugged.index
parents = []
branch = find_branch(ref)
if branch
last_commit = branch.target
index.read_tree(last_commit.raw_commit.tree)
parents = [last_commit.sha]
end
actions.each do |action|
case action[:action]
when :create, :update, :move
mode =
case action[:action]
when :update
index.get(action[:file_path])[:mode]
when :move
index.get(action[:previous_path])[:mode]
end
mode ||= 0o100644
index.remove(action[:previous_path]) if action[:action] == :move
content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
oid = rugged.write(content, :blob)
index.add(path: action[:file_path], oid: oid, mode: mode)
when :delete
index.remove(action[:file_path])
end
end
options = {
tree: index.write_tree(rugged),
message: message,
parents: parents
}
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
Rugged::Commit.create(rugged, options)
end
end
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
author = Gitlab::Git::committer_hash(email: email, name: name) || committer
@ -997,6 +1043,10 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo)
end
def create_ref(ref, ref_path)
fetch_ref(path_to_repo, ref, ref_path)
end
def update_branch_with_hooks(current_user, branch)
update_autocrlf_option

View File

@ -136,6 +136,7 @@ class Service < ActiveRecord::Base
end
def #{arg}=(value)
self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties['#{arg}'] = value
end

View File

@ -279,6 +279,11 @@ class User < ActiveRecord::Base
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
end
def build_user(attrs = {})
User.new(attrs)
end
@ -827,6 +832,22 @@ class User < ActiveRecord::Base
todos_pending_count(force: true)
end
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
# flow means we don't call that automatically (and can't conveniently do so).
#
# See:
# <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
#
def increment_failed_attempts!
self.failed_attempts ||= 0
self.failed_attempts += 1
if attempts_exceeded?
lock_access! unless access_locked?
else
save(validate: false)
end
end
private
def projects_union(min_access_level = nil)

View File

@ -56,9 +56,8 @@ class BaseService
result
end
def success
{
status: :success
}
def success(pass_back = {})
pass_back[:status] = :success
pass_back
end
end

View File

@ -36,12 +36,7 @@ module Boards
end
def set_state
params[:state] =
case list.list_type.to_sym
when :backlog then 'opened'
when :done then 'closed'
else 'all'
end
params[:state] = list.done? ? 'closed' : 'opened'
end
def board_label_ids

View File

@ -27,8 +27,9 @@ module Files
create_target_branch
end
if commit
success
result = commit
if result
success(result: result)
else
error('Something went wrong. Your changes were not committed')
end
@ -42,6 +43,12 @@ module Files
@source_branch != @target_branch || @source_project != @project
end
def file_has_changed?
return false unless @last_commit_sha && last_commit
@last_commit_sha != last_commit.sha
end
def raise_error(message)
raise ValidationError.new(message)
end

View File

@ -0,0 +1,124 @@
require_relative "base_service"
module Files
class MultiService < Files::BaseService
class FileChangedError < StandardError; end
def commit
repository.multi_action(
user: current_user,
branch: @target_branch,
message: @commit_message,
actions: params[:actions],
author_email: @author_email,
author_name: @author_name
)
end
private
def validate
super
params[:actions].each_with_index do |action, index|
unless action[:file_path].present?
raise_error("You must specify a file_path.")
end
regex_check(action[:file_path])
regex_check(action[:previous_path]) if action[:previous_path]
if project.empty_repo? && action[:action] != :create
raise_error("No files to #{action[:action]}.")
end
validate_file_exists(action)
case action[:action]
when :create
validate_create(action)
when :update
validate_update(action)
when :delete
validate_delete(action)
when :move
validate_move(action, index)
else
raise_error("Unknown action type `#{action[:action]}`.")
end
end
end
def validate_file_exists(action)
return if action[:action] == :create
file_path = action[:file_path]
file_path = action[:previous_path] if action[:action] == :move
blob = repository.blob_at_branch(params[:branch_name], file_path)
unless blob
raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
end
end
def last_commit
Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path)
end
def regex_check(file)
if file =~ Gitlab::Regex.directory_traversal_regex
raise_error(
'Your changes could not be committed, because the file name, `' +
file +
'` ' +
Gitlab::Regex.directory_traversal_regex_message
)
end
unless file =~ Gitlab::Regex.file_path_regex
raise_error(
'Your changes could not be committed, because the file name, `' +
file +
'` ' +
Gitlab::Regex.file_path_regex_message
)
end
end
def validate_create(action)
return if project.empty_repo?
if repository.blob_at_branch(params[:branch_name], action[:file_path])
raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
end
end
def validate_delete(action)
end
def validate_move(action, index)
if action[:previous_path].nil?
raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
end
blob = repository.blob_at_branch(params[:branch_name], action[:file_path])
if blob
raise_error("Move destination `#{action[:file_path]}` already exists.")
end
if action[:content].nil?
blob = repository.blob_at_branch(params[:branch_name], action[:previous_path])
blob.load_all_data!(repository) if blob.truncated?
params[:actions][index][:content] = blob.data
end
end
def validate_update(action)
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
end
end
end
end

View File

@ -23,12 +23,6 @@ module Files
end
end
def file_has_changed?
return false unless @last_commit_sha && last_commit
@last_commit_sha != last_commit.sha
end
def last_commit
@last_commit ||= Gitlab::Git::Commit.
last_for_path(@source_project.repository, @source_branch, @file_path)

View File

@ -14,6 +14,8 @@ module Members
if member.request? && member.user != user
notification_service.decline_access_request(member)
end
member
end
end
end

View File

@ -1,17 +1,42 @@
module Members
class DestroyService < BaseService
attr_accessor :member, :current_user
include MembersHelper
def initialize(member, current_user)
@member = member
attr_accessor :source
ALLOWED_SCOPES = %i[members requesters all]
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params
end
def execute
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
raise Gitlab::Access::AccessDeniedError
end
def execute(scope = :members)
raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
member = find_member!(scope)
raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
AuthorizedDestroyService.new(member, current_user).execute
end
private
def find_member!(scope)
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
case scope
when :all
source.members.find_by(condition) ||
source.requesters.find_by!(condition)
else
source.public_send(scope).find_by!(condition)
end
end
def can_destroy_member?(member)
member && can?(current_user, action_member_permission(:destroy, member), member)
end
end
end

View File

@ -7,6 +7,8 @@ module Projects
def execute
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
@skip_wiki = params.delete(:skip_wiki)
@project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level
@ -15,6 +17,11 @@ module Projects
return @project
end
unless allowed_fork?(forked_from_project_id)
@project.errors.add(:forked_from_project_id, 'is forbidden')
return @project
end
# Set project name from path
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
@ -71,6 +78,13 @@ module Projects
@project.errors.add(:namespace, "is not valid")
end
def allowed_fork?(source_project_id)
return true if source_project_id.nil?
source_project = Project.find_by(id: source_project_id)
current_user.can?(:fork_project, source_project)
end
def allowed_namespace?(user, namespace_id)
namespace = Namespace.find_by(id: namespace_id)
current_user.can?(:create_projects, namespace)
@ -80,7 +94,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import?
@project.create_wiki if @project.feature_available?(:wiki, current_user)
@project.create_wiki unless skip_wiki?
@project.build_missing_services
@project.create_labels
@ -94,6 +108,10 @@ module Projects
end
end
def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
def save_project_and_import_data(import_data)
Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data

View File

@ -16,6 +16,8 @@ module Projects
end
new_project = CreateService.new(current_user, new_params).execute
return new_project unless new_project.persisted?
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)

View File

@ -347,7 +347,7 @@ module SystemNoteService
notes = notes.where(noteable_id: noteable.id)
end
notes_for_mentioner(mentioner, noteable, notes).count > 0
notes_for_mentioner(mentioner, noteable, notes).exists?
end
# Build an Array of lines detailing each commit added in a merge request

View File

@ -63,6 +63,11 @@
Reply by email
%span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
%p
Container Registry
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
.col-md-4
%h4
Components

View File

@ -16,8 +16,7 @@
%tr
%td #{stage.capitalize} Job - #{build[:name]}
%td
%pre
= simple_format build[:commands]
%pre= build[:commands]
%br
%b Tag list:

View File

@ -19,6 +19,7 @@
(removed)
&middot; #{time_ago_with_tooltip(todo.created_at)}
= todo_due_date(todo)
.todo-body
.todo-note

View File

@ -33,8 +33,8 @@
.form-group
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
= f.collection_select :project_ids, @group.projects, :id, :name,
{ selected: @group.projects.map(&:id) }, multiple: true, class: 'select2'
= f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
{ selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
.col-md-6
.form-group

View File

@ -1,5 +1,5 @@
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
%div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
%div{ class: "container-fluid" }
.header-content
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
%span.sr-only Toggle navigation

View File

@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper').find('.container-fluid').removeClass('container-limited')
$('.content-wrapper .container-fluid').removeClass('container-limited')
} else {
$('.content-wrapper').find('.container-fluid').addClass('container-limited')
$('.content-wrapper .container-fluid').addClass('container-limited')
}
// Re-enable the "Save" button

Some files were not shown because too many files have changed in this diff Show More