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 haml_lint: *exec
rake scss_lint: *exec rake scss_lint: *exec
rake brakeman: *exec rake brakeman: *exec
rake flay: rake flay: *exec
<<: *exec
allow_failure: yes
license_finder: *exec license_finder: *exec
rake downtime_check: *exec rake downtime_check: *exec

View File

@ -453,6 +453,10 @@ Style/VariableName:
EnforcedStyle: snake_case EnforcedStyle: snake_case
Enabled: true Enabled: true
# Use the configured style when numbering variables.
Style/VariableNumber:
Enabled: false
# Use when x then ... for one-line cases. # Use when x then ... for one-line cases.
Style/WhenThen: Style/WhenThen:
Enabled: true Enabled: true

View File

@ -1,21 +1,21 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 0` # `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 # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 158 # Offense count: 160
Lint/AmbiguousRegexpLiteral: Lint/AmbiguousRegexpLiteral:
Enabled: false Enabled: false
# Offense count: 41 # Offense count: 40
# Configuration parameters: AllowSafeAssignment. # Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition: Lint/AssignmentInCondition:
Enabled: false Enabled: false
# Offense count: 16 # Offense count: 18
Lint/HandleExceptions: Lint/HandleExceptions:
Enabled: false Enabled: false
@ -23,11 +23,21 @@ Lint/HandleExceptions:
Lint/Loop: Lint/Loop:
Enabled: false Enabled: false
# Offense count: 16 # Offense count: 19
Lint/ShadowingOuterLocalVariable: Lint/ShadowingOuterLocalVariable:
Enabled: false 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. # Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument: Lint/UnusedBlockArgument:
@ -39,32 +49,81 @@ Lint/UnusedBlockArgument:
Lint/UnusedMethodArgument: Lint/UnusedMethodArgument:
Enabled: false Enabled: false
# Offense count: 9
# Cop supports --auto-correct.
Performance/PushSplat:
Enabled: false
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
Performance/RedundantBlockCall: Performance/RedundantBlockCall:
Enabled: false Enabled: false
# Offense count: 4 # Offense count: 5
# Cop supports --auto-correct. # Cop supports --auto-correct.
Performance/RedundantMatch: Performance/RedundantMatch:
Enabled: false Enabled: false
# Offense count: 27 # Offense count: 26
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: MaxKeyValuePairs. # Configuration parameters: MaxKeyValuePairs.
Performance/RedundantMerge: Performance/RedundantMerge:
Enabled: false 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: Rails/OutputSafety:
Enabled: false Enabled: false
# Offense count: 129 # Offense count: 151
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: strict, flexible # SupportedStyles: strict, flexible
Rails/TimeZone: Rails/TimeZone:
@ -77,58 +136,63 @@ Rails/TimeZone:
Rails/Validation: Rails/Validation:
Enabled: false Enabled: false
# Offense count: 273 # Offense count: 2
# Cop supports --auto-correct.
Security/JSONLoad:
Enabled: false
# Offense count: 284
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: with_first_parameter, with_fixed_indentation # SupportedStyles: with_first_parameter, with_fixed_indentation
Style/AlignParameters: Style/AlignParameters:
Enabled: false Enabled: false
# Offense count: 30 # Offense count: 28
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: always, conditionals # SupportedStyles: always, conditionals
Style/AndOr: Style/AndOr:
Enabled: false Enabled: false
# Offense count: 50 # Offense count: 52
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: percent_q, bare_percent # SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals: Style/BarePercentLiterals:
Enabled: false Enabled: false
# Offense count: 289 # Offense count: 291
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: braces, no_braces, context_dependent # SupportedStyles: braces, no_braces, context_dependent
Style/BracesAroundHashParameters: Style/BracesAroundHashParameters:
Enabled: false Enabled: false
# Offense count: 5 # Offense count: 6
Style/CaseEquality: Style/CaseEquality:
Enabled: false Enabled: false
# Offense count: 19 # Offense count: 26
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/ColonMethodCall: Style/ColonMethodCall:
Enabled: false Enabled: false
# Offense count: 3 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: Keywords. # Configuration parameters: Keywords.
# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW # Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
Style/CommentAnnotation: Style/CommentAnnotation:
Enabled: false Enabled: false
# Offense count: 33 # Offense count: 30
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly. # Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
# SupportedStyles: assign_to_condition, assign_inside_condition # SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment: Style/ConditionalAssignment:
Enabled: false Enabled: false
# Offense count: 881 # Offense count: 957
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing # SupportedStyles: leading, trailing
@ -139,12 +203,12 @@ Style/DotPosition:
Style/DoubleNegation: Style/DoubleNegation:
Enabled: false Enabled: false
# Offense count: 4 # Offense count: 6
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/EachWithObject: Style/EachWithObject:
Enabled: false Enabled: false
# Offense count: 25 # Offense count: 26
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: empty, nil, both # SupportedStyles: empty, nil, both
@ -156,24 +220,24 @@ Style/EmptyElse:
Style/EmptyLiteral: Style/EmptyLiteral:
Enabled: false Enabled: false
# Offense count: 135 # Offense count: 140
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Style/ExtraSpacing: Style/ExtraSpacing:
Enabled: false Enabled: false
# Offense count: 7 # Offense count: 6
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: format, sprintf, percent # SupportedStyles: format, sprintf, percent
Style/FormatString: Style/FormatString:
Enabled: false Enabled: false
# Offense count: 51 # Offense count: 201
# Configuration parameters: MinBodyLength. # Configuration parameters: MinBodyLength.
Style/GuardClause: Style/GuardClause:
Enabled: false Enabled: false
# Offense count: 9 # Offense count: 11
Style/IfInsideElse: Style/IfInsideElse:
Enabled: false Enabled: false
@ -183,21 +247,21 @@ Style/IfInsideElse:
Style/IfUnlessModifier: Style/IfUnlessModifier:
Enabled: false Enabled: false
# Offense count: 52 # Offense count: 53
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets # SupportedStyles: special_inside_parentheses, consistent, align_brackets
Style/IndentArray: Style/IndentArray:
Enabled: false Enabled: false
# Offense count: 97 # Offense count: 95
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces # SupportedStyles: special_inside_parentheses, consistent, align_braces
Style/IndentHash: Style/IndentHash:
Enabled: false Enabled: false
# Offense count: 12 # Offense count: 29
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: line_count_dependent, lambda, literal # SupportedStyles: line_count_dependent, lambda, literal
@ -209,7 +273,7 @@ Style/Lambda:
Style/LineEndConcatenation: Style/LineEndConcatenation:
Enabled: false Enabled: false
# Offense count: 13 # Offense count: 15
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/MethodCallParentheses: Style/MethodCallParentheses:
Enabled: false Enabled: false
@ -218,7 +282,7 @@ Style/MethodCallParentheses:
Style/MethodMissing: Style/MethodMissing:
Enabled: false Enabled: false
# Offense count: 85 # Offense count: 95
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/MutableConstant: Style/MutableConstant:
Enabled: false Enabled: false
@ -235,14 +299,14 @@ Style/NestedParenthesizedCalls:
Style/Next: Style/Next:
Enabled: false Enabled: false
# Offense count: 8 # Offense count: 12
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles. # Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
# SupportedOctalStyles: zero_with_o, zero_only # SupportedOctalStyles: zero_with_o, zero_only
Style/NumericLiteralPrefix: Style/NumericLiteralPrefix:
Enabled: false Enabled: false
# Offense count: 64 # Offense count: 53
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: predicate, comparison # SupportedStyles: predicate, comparison
@ -254,7 +318,7 @@ Style/NumericPredicate:
Style/ParallelAssignment: Style/ParallelAssignment:
Enabled: false Enabled: false
# Offense count: 264 # Offense count: 294
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters. # Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters: Style/PercentLiteralDelimiters:
@ -272,7 +336,7 @@ Style/PercentQLiterals:
Style/PerlBackrefs: Style/PerlBackrefs:
Enabled: false Enabled: false
# Offense count: 35 # Offense count: 38
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_ # NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_ # NamePrefixBlacklist: is_, has_, have_
@ -280,7 +344,7 @@ Style/PerlBackrefs:
Style/PredicateName: Style/PredicateName:
Enabled: false Enabled: false
# Offense count: 27 # Offense count: 26
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/PreferredHashMethods: Style/PreferredHashMethods:
Enabled: false Enabled: false
@ -312,12 +376,12 @@ Style/RedundantException:
Style/RedundantFreeze: Style/RedundantFreeze:
Enabled: false Enabled: false
# Offense count: 408 # Offense count: 427
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/RedundantSelf: Style/RedundantSelf:
Enabled: false Enabled: false
# Offense count: 93 # Offense count: 97
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed # SupportedStyles: slashes, percent_r, mixed
@ -329,7 +393,12 @@ Style/RegexpLiteral:
Style/RescueModifier: Style/RescueModifier:
Enabled: false Enabled: false
# Offense count: 5 # Offense count: 114
# Cop supports --auto-correct.
Style/SafeNavigation:
Enabled: false
# Offense count: 7
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/SelfAssignment: Style/SelfAssignment:
Enabled: false Enabled: false
@ -346,7 +415,7 @@ Style/SingleLineBlockParams:
Style/SingleLineMethods: Style/SingleLineMethods:
Enabled: false Enabled: false
# Offense count: 124 # Offense count: 125
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space # SupportedStyles: space, no_space
@ -359,19 +428,19 @@ Style/SpaceBeforeBlockBraces:
Style/SpaceBeforeFirstArg: Style/SpaceBeforeFirstArg:
Enabled: false Enabled: false
# Offense count: 141 # Offense count: 145
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space # SupportedStyles: space, no_space
Style/SpaceInsideBlockBraces: Style/SpaceInsideBlockBraces:
Enabled: false Enabled: false
# Offense count: 96 # Offense count: 99
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/SpaceInsideBrackets: Style/SpaceInsideBrackets:
Enabled: false Enabled: false
# Offense count: 62 # Offense count: 65
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/SpaceInsideParens: Style/SpaceInsideParens:
Enabled: false Enabled: false
@ -381,21 +450,21 @@ Style/SpaceInsideParens:
Style/SpaceInsidePercentLiteralDelimiters: Style/SpaceInsidePercentLiteralDelimiters:
Enabled: false Enabled: false
# Offense count: 40 # Offense count: 41
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: SupportedStyles. # Configuration parameters: SupportedStyles.
# SupportedStyles: use_perl_names, use_english_names # SupportedStyles: use_perl_names, use_english_names
Style/SpecialGlobalVars: Style/SpecialGlobalVars:
EnforcedStyle: use_perl_names EnforcedStyle: use_perl_names
# Offense count: 30 # Offense count: 31
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: single_quotes, double_quotes # SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation: Style/StringLiteralsInInterpolation:
Enabled: false Enabled: false
# Offense count: 32 # Offense count: 33
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods. # Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method # IgnoredMethods: respond_to, define_method
@ -409,7 +478,7 @@ Style/SymbolProc:
Style/TernaryParentheses: Style/TernaryParentheses:
Enabled: false Enabled: false
# Offense count: 24 # Offense count: 29
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
# SupportedStyles: comma, consistent_comma, no_comma # 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. Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased) v 8.13.0 (unreleased)
- Update runner version only when updating contacted_at
- Add link from system note to compare with previous version - Add link from system note to compare with previous version
- Use gitlab-shell v3.6.2 (GIT TRACE logging) - Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Fix centering of custom header logos (Ashley Dumaine) - Fix centering of custom header logos (Ashley Dumaine)
@ -8,29 +9,44 @@ v 8.13.0 (unreleased)
- Replaced the check sign to arrow in the show build view. !6501 - Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Speed-up group milestones show page - Speed-up group milestones show page
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps) - Add more tests for calendar contribution (ClemMakesApps)
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
- API: Multi-file commit !6096 (mahcsig)
- Revert "Label list shows all issues (opened or closed) with that label"
- Expose expires_at field when sharing project on API - Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API - Allow the Koding integration to be configured through the API
- Added soft wrap button to repository file/blob editor - Added soft wrap button to repository file/blob editor
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps)
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix that manual jobs would no longer block jobs in the next stage. !6604
- Add configurable email subject suffix (Fu Xu) - Add configurable email subject suffix (Fu Xu)
- Use a ConnectionPool for Rails.cache on Sidekiq servers - Use a ConnectionPool for Rails.cache on Sidekiq servers
- Replace `alias_method_chain` with `Module#prepend` - Replace `alias_method_chain` with `Module#prepend`
- Enable GitLab Import/Export for non-admin users. - Enable GitLab Import/Export for non-admin users.
- Preserve label filters when sorting !6136 (Joseph Frazier) - Preserve label filters when sorting !6136 (Joseph Frazier)
- MergeRequest#new form load diff asynchronously
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496 - Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Prevent flash alert text from being obscured when container is fluid
- Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree - Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts? - Revert avoid touching file system on Build#artifacts?
- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
- Add broadcast messages and alerts below sub-nav - Add broadcast messages and alerts below sub-nav
- Better empty state for Groups view - Better empty state for Groups view
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
@ -40,17 +56,35 @@ v 8.13.0 (unreleased)
- Optimize GitHub importing for speed and memory - Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar) - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- Notify the Merger about merge after successful build (Dimitris Karakasilis) - Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Reduce queries needed to find users using their SSH keys when pushing commits
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- Fix broken repository 500 errors in project list - Fix broken repository 500 errors in project list
- Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone) - Close todos when accepting merge requests via the API !6486 (tonygambone)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer) - Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Grouped pipeline dropdown is a scrollable container
v 8.12.4 (unreleased) v 8.12.5 (unreleased)
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. (lukehowell)
v 8.12.4
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
- Fix padding in build sidebar. !6506
- Changed compare dropdowns to dropdowns with isolated search input. !6550
- Fix race condition on LFS Token. !6592
- Fix type mismatch bug when closing Jira issue. !6619
- Fix lint-doc error. !6623
- Skip wiki creation when GitHub project has wiki enabled. !6665
- Fix issues importing services via Import/Export. !6667
- Restrict failed login attempts for users with 2FA enabled. !6668
- Fix failed project deletion when feature visibility set to private. !6688
- Prevent claiming associated model IDs via import.
- Set GitLab project exported file permissions to owner only
v 8.12.3 v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves - Update Gitlab Shell to support low IO priority for storage moves
v 8.12.2 (unreleased) v 8.12.2
- Fix Import/Export not recognising correctly the imported services. - Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination - Fix snippets pagination
- Fix "Create project" button layout when visibility options are restricted - Fix "Create project" button layout when visibility options are restricted
@ -64,6 +98,7 @@ v 8.12.2 (unreleased)
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv) - Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
- Fix resolve discussion buttons endpoint path - Fix resolve discussion buttons endpoint path
- Refactor remnants of CoffeeScript destructured opts and super !6261
v 8.12.1 v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
@ -104,6 +139,7 @@ v 8.12.0
- Fix long comments in diffs messing with table width - Fix long comments in diffs messing with table width
- Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman) - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page - Fix pagination on user snippets page
- Honor "fixed layout" preference in more places !6422
- Run CI builds with the permissions of users !5735 - Run CI builds with the permissions of users !5735
- Fix sorting of issues in API - Fix sorting of issues in API
- Fix download artifacts button links !6407 - Fix download artifacts button links !6407
@ -144,6 +180,7 @@ v 8.12.0
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
- Add textarea autoresize after comment (ClemMakesApps) - Add textarea autoresize after comment (ClemMakesApps)
- Do not write SSH public key 'comments' to authorized_keys !6381 - Do not write SSH public key 'comments' to authorized_keys !6381
- Add due date to issue todos
- Refresh todos count cache when an Issue/MR is deleted - Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps) - Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions - Hides merge request button on branches page is user doesn't have permissions

View File

@ -130,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0'
gem 'after_commit_queue', '~> 1.3.0' gem 'after_commit_queue', '~> 1.3.0'
# Issue tags # Issue tags
gem 'acts-as-taggable-on', '~> 3.4' gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 4.2' gem 'sidekiq', '~> 4.2'
@ -231,7 +231,7 @@ gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0' gem 'base32', '~> 0.3.0'
# Sentry integration # Sentry integration
gem 'sentry-raven', '~> 1.1.0' gem 'sentry-raven', '~> 2.0.0'
gem 'premailer-rails', '~> 1.9.0' gem 'premailer-rails', '~> 1.9.0'
@ -295,7 +295,7 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.42.0', require: false gem 'rubocop', '~> 0.43.0', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false gem 'haml_lint', '~> 0.18.2', require: false

View File

@ -44,8 +44,8 @@ GEM
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (3.5.0) acts-as-taggable-on (4.0.0)
activerecord (>= 3.2, < 5) activerecord (>= 4.0)
addressable (2.3.8) addressable (2.3.8)
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
@ -487,7 +487,7 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
paranoia (2.1.4) paranoia (2.1.4)
activerecord (~> 4.0) activerecord (~> 4.0)
parser (2.3.1.2) parser (2.3.1.4)
ast (~> 2.2) ast (~> 2.2)
pg (0.18.4) pg (0.18.4)
pkg-config (1.1.7) pkg-config (1.1.7)
@ -620,7 +620,7 @@ GEM
rspec-retry (0.4.5) rspec-retry (0.4.5)
rspec-core rspec-core
rspec-support (3.5.0) rspec-support (3.5.0)
rubocop (0.42.0) rubocop (0.43.0)
parser (>= 2.3.1.1, < 3.0) parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0) rainbow (>= 1.99.1, < 3.0)
@ -665,8 +665,8 @@ GEM
activesupport (>= 3.1) activesupport (>= 3.1)
select2-rails (3.5.9.3) select2-rails (3.5.9.3)
thor (~> 0.14) thor (~> 0.14)
sentry-raven (1.1.0) sentry-raven (2.0.2)
faraday (>= 0.7.6) faraday (>= 0.7.6, < 0.10.x)
settingslogic (2.0.9) settingslogic (2.0.9)
sexp_processor (4.7.0) sexp_processor (4.7.0)
sham_rack (1.3.6) sham_rack (1.3.6)
@ -802,7 +802,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2) RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0) ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0) activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4) acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8) addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0) after_commit_queue (~> 1.3.0)
akismet (~> 2.0) akismet (~> 2.0)
@ -938,7 +938,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0) rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rubocop (~> 0.42.0) rubocop (~> 0.43.0)
rubocop-rspec (~> 1.5.0) rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
@ -948,7 +948,7 @@ DEPENDENCIES
sdoc (~> 0.3.20) sdoc (~> 0.3.20)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
sentry-raven (~> 1.1.0) sentry-raven (~> 2.0.0)
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
@ -986,4 +986,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH 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", namespacesPath: "/api/:version/namespaces.json",
groupProjectsPath: "/api/:version/groups/:id/projects.json", groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true", projectsPath: "/api/:version/projects.json?simple=true",
labelsPath: "/api/:version/projects/:id/labels", labelsPath: "/:namespace_path/:project_path/labels",
licensePath: "/api/:version/licenses/:key", licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key", gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
@ -23,12 +23,13 @@
}, },
// Return groups list. Filtered by query // Return groups list. Filtered by query
// Only active groups retrieved // Only active groups retrieved
groups: function(query, skip_ldap, callback) { groups: function(query, skip_ldap, skip_groups, callback) {
var url = Api.buildUrl(Api.groupsPath); var url = Api.buildUrl(Api.groupsPath);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
search: query, search: query,
skip_groups: skip_groups,
per_page: 20 per_page: 20
}, },
dataType: "json" dataType: "json"
@ -65,13 +66,14 @@
return callback(projects); return callback(projects);
}); });
}, },
newLabel: function(project_id, data, callback) { newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath) var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id); .replace(':namespace_path', namespace_path)
.replace(':project_path', project_path);
return $.ajax({ return $.ajax({
url: url, url: url,
type: "POST", type: "POST",
data: data, data: {'label': data},
dataType: "json" dataType: "json"
}).done(function(label) { }).done(function(label) {
return callback(label); return callback(label);

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; return BlobGitignoreSelector;
})(TemplateSelector); })(gl.TemplateSelector);
}).call(this); }).call(this);

View File

@ -23,6 +23,6 @@
return BlobLicenseSelector; return BlobLicenseSelector;
})(TemplateSelector); })(gl.TemplateSelector);
}).call(this); }).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));
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap(); this.initSoftWrap();
new BlobLicenseSelectors({ new gl.BlobLicenseSelectors({
editor: this.editor editor: this.editor
}); });
new BlobGitignoreSelectors({ new BlobGitignoreSelectors({
editor: this.editor editor: this.editor
}); });
new BlobCiYamlSelectors({ new gl.BlobCiYamlSelectors({
editor: this.editor editor: this.editor
}); });
} }

View File

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

View File

@ -146,7 +146,7 @@
$date = $('.js-artifacts-remove'); $date = $('.js-artifacts-remove');
if ($date.length) { if ($date.length) {
date = $date.text(); date = $date.text();
return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' ')); return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
} }
}; };

View File

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

View File

@ -7,6 +7,9 @@
function Diff() { function Diff() {
$('.files .diff-file').singleFileDiff(); $('.files .diff-file').singleFileDiff();
this.filesCommentButton = $('.files .diff-file').filesCommentButton(); this.filesCommentButton = $('.files .diff-file').filesCommentButton();
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$(document).off('click', '.js-unfold'); $(document).off('click', '.js-unfold');
$(document).on('click', '.js-unfold', (function(_this) { $(document).on('click', '.js-unfold', (function(_this) {
return function(event) { return function(event) {
@ -52,6 +55,10 @@
})(this)); })(this));
} }
Diff.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
Diff.prototype.lineNumbers = function(line) { Diff.prototype.lineNumbers = function(line) {
if (!line.children().length) { if (!line.children().length) {
return [0, 0]; return [0, 0];

View File

@ -26,7 +26,7 @@
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
Issuable.init(); Issuable.init();
new IssuableBulkActions(); new gl.IssuableBulkActions();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
break; break;
case 'projects:issues:show': case 'projects:issues:show':
@ -40,7 +40,7 @@
new Milestone(); new Milestone();
break; break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new Todos(); new gl.Todos();
break; break;
case 'projects:milestones:new': case 'projects:milestones:new':
case 'projects:milestones:edit': case 'projects:milestones:edit':
@ -59,7 +59,9 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.issue-form')); new GLForm($('.issue-form'));
new IssuableForm($('.issue-form')); new IssuableForm($('.issue-form'));
new IssuableTemplateSelectors(); new LabelsSelect();
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
break; break;
case 'projects:merge_requests:new': case 'projects:merge_requests:new':
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
@ -67,7 +69,9 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form')); new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form')); new IssuableForm($('.merge-request-form'));
new IssuableTemplateSelectors(); new LabelsSelect();
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
break; break;
case 'projects:tags:new': case 'projects:tags:new':
new ZenMode(); new ZenMode();
@ -165,7 +169,7 @@
break; break;
case 'projects:labels:index': case 'projects:labels:index':
if ($('.prioritized-labels').length) { if ($('.prioritized-labels').length) {
new LabelManager(); new gl.LabelManager();
} }
break; break;
case 'projects:network:show': case 'projects:network:show':
@ -279,7 +283,7 @@
Dispatcher.prototype.initSearch = function() { Dispatcher.prototype.initSearch = function() {
// Only when search form is present // Only when search form is present
if ($('.search').length) { if ($('.search').length) {
return new SearchAutocomplete(); return new gl.SearchAutocomplete();
} }
}; };

View File

@ -443,6 +443,7 @@
var contentHtml; var contentHtml;
this.resetRows(); this.resetRows();
this.addArrowKeyEvent(); this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) { if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this); this.options.setIndeterminateIds.call(this);
} }
@ -460,9 +461,21 @@
if (this.options.filterable) { if (this.options.filterable) {
this.filterInput.focus(); this.filterInput.focus();
} }
if (this.options.showMenuAbove) {
this.positionMenuAbove();
}
return this.dropdown.trigger('shown.gl.dropdown'); return this.dropdown.trigger('shown.gl.dropdown');
}; };
GitLabDropdown.prototype.positionMenuAbove = function() {
var $button = $(this.el);
var $menu = this.dropdown.find('.dropdown-menu');
$menu.css('top', ($button.height() + $menu.height()) * -1);
};
GitLabDropdown.prototype.hidden = function(e) { GitLabDropdown.prototype.hidden = function(e) {
var $input; var $input;
this.resetRows(); this.resetRows();

View File

@ -5,14 +5,15 @@
function GroupsSelect() { function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) { $('.ajax-groups-select').each((function(_this) {
return function(i, select) { return function(i, select) {
var skip_ldap; var skip_ldap, skip_groups;
skip_ldap = $(select).hasClass('skip_ldap'); skip_ldap = $(select).hasClass('skip_ldap');
skip_groups = $(select).data('skip-groups') || [];
return $(select).select2({ return $(select).select2({
placeholder: "Search for a group", placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'), multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0, minimumInputLength: 0,
query: function(query) { query: function(query) {
return Api.groups(query.term, skip_ldap, function(groups) { return Api.groups(query.term, skip_ldap, skip_groups, function(groups) {
var data; var data;
data = { data = {
results: groups results: groups

View File

@ -51,7 +51,6 @@
}).remove(); }).remove();
// Submit the form to get new data // Submit the form to get new data
Issuable.filterResults($('.filter-form')); Issuable.filterResults($('.filter-form'));
return $('.js-label-select').trigger('update.label');
}); });
}, },
filterResults: (function(_this) { filterResults: (function(_this) {

View File

@ -1,13 +1,10 @@
(function() { ((global) => {
this.IssuableBulkActions = (function() {
function IssuableBulkActions(opts) { class IssuableBulkActions {
// Set defaults constructor({ container, form, issues } = {}) {
var ref, ref1, ref2; this.container = container || $('.content'),
if (opts == null) { this.form = form || this.getElement('.bulk-update');
opts = {}; this.issues = issues || this.getElement('.issues-list .issue');
}
this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li');
// Save instance
this.form.data('bulkActions', this); this.form.data('bulkActions', this);
this.willUpdateLabels = false; this.willUpdateLabels = false;
this.bindEvents(); this.bindEvents();
@ -15,53 +12,46 @@
Issuable.initChecks(); Issuable.initChecks();
} }
IssuableBulkActions.prototype.getElement = function(selector) { getElement(selector) {
return this.container.find(selector); return this.container.find(selector);
}; }
IssuableBulkActions.prototype.bindEvents = function() { bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}; }
IssuableBulkActions.prototype.onFormSubmit = function(e) { onFormSubmit(e) {
e.preventDefault(); e.preventDefault();
return this.submit(); return this.submit();
}; }
IssuableBulkActions.prototype.submit = function() { submit() {
var _this, xhr; const _this = this;
_this = this; const xhr = $.ajax({
xhr = $.ajax({
url: this.form.attr('action'), url: this.form.attr('action'),
method: this.form.attr('method'), method: this.form.attr('method'),
dataType: 'JSON', dataType: 'JSON',
data: this.getFormDataAsObject() data: this.getFormDataAsObject()
}); });
xhr.done(function(response, status, xhr) { xhr.done(() => window.location.reload());
return location.reload(); xhr.fail(() => new Flash("Issue update failed"));
});
xhr.fail(function() {
return new Flash("Issue update failed");
});
return xhr.always(this.onFormSubmitAlways.bind(this)); return xhr.always(this.onFormSubmitAlways.bind(this));
}; }
IssuableBulkActions.prototype.onFormSubmitAlways = function() { onFormSubmitAlways() {
return this.form.find('[type="submit"]').enable(); return this.form.find('[type="submit"]').enable();
}; }
IssuableBulkActions.prototype.getSelectedIssues = function() { getSelectedIssues() {
return this.issues.has('.selected_issue:checked'); return this.issues.has('.selected_issue:checked');
}; }
IssuableBulkActions.prototype.getLabelsFromSelection = function() { getLabelsFromSelection() {
var labels; const labels = [];
labels = [];
this.getSelectedIssues().map(function() { this.getSelectedIssues().map(function() {
var _labels; const labelsData = $(this).data('labels');
_labels = $(this).data('labels'); if (labelsData) {
if (_labels) { return labelsData.map(function(labelId) {
return _labels.map(function(labelId) {
if (labels.indexOf(labelId) === -1) { if (labels.indexOf(labelId) === -1) {
return labels.push(labelId); return labels.push(labelId);
} }
@ -69,7 +59,7 @@
} }
}); });
return labels; return labels;
}; }
/** /**
@ -77,25 +67,21 @@
* @return {Array} Label IDs * @return {Array} Label IDs
*/ */
IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() { getUnmarkedIndeterminedLabels() {
var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result; const result = [];
result = []; const labelsToKeep = [];
labelsToKeep = [];
ref = this.getElement('.labels-filter .is-indeterminate'); this.getElement('.labels-filter .is-indeterminate')
for (i = 0, len = ref.length; i < len; i++) { .each((i, el) => labelsToKeep.push($(el).data('labelId')));
el = ref[i];
labelsToKeep.push($(el).data('labelId')); this.getLabelsFromSelection().forEach((id) => {
}
ref1 = this.getLabelsFromSelection();
for (j = 0, len1 = ref1.length; j < len1; j++) {
id = ref1[j];
// Only the ones that we are not going to keep
if (labelsToKeep.indexOf(id) === -1) { if (labelsToKeep.indexOf(id) === -1) {
result.push(id); result.push(id);
} }
} });
return result; return result;
}; }
/** /**
@ -103,9 +89,8 @@
* Returns key/value pairs from form data * Returns key/value pairs from form data
*/ */
IssuableBulkActions.prototype.getFormDataAsObject = function() { getFormDataAsObject() {
var formData; const formData = {
formData = {
update: { update: {
state_event: this.form.find('input[name="update[state_event]"]').val(), state_event: this.form.find('input[name="update[state_event]"]').val(),
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
@ -125,19 +110,18 @@
}); });
} }
return formData; return formData;
}; }
IssuableBulkActions.prototype.getLabelsToApply = function() { getLabelsToApply() {
var $labels, labelIds; const labelIds = [];
labelIds = []; const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels.each(function(k, label) { $labels.each(function(k, label) {
if (label) { if (label) {
return labelIds.push(parseInt($(label).val())); return labelIds.push(parseInt($(label).val()));
} }
}); });
return labelIds; return labelIds;
}; }
/** /**
@ -145,11 +129,10 @@
* @return {Array} Array of labels IDs * @return {Array} Array of labels IDs
*/ */
IssuableBulkActions.prototype.getLabelsToRemove = function() { getLabelsToRemove() {
var indeterminatedLabels, labelsToApply, result; const result = [];
result = []; const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); const labelsToApply = this.getLabelsToApply();
labelsToApply = this.getLabelsToApply();
indeterminatedLabels.map(function(id) { indeterminatedLabels.map(function(id) {
// We need to exclude label IDs that will be applied // We need to exclude label IDs that will be applied
// By not doing this will cause issues from selection to not add labels at all // By not doing this will cause issues from selection to not add labels at all
@ -158,10 +141,9 @@
} }
}); });
return result; return result;
}; }
}
return IssuableBulkActions; global.IssuableBulkActions = IssuableBulkActions;
})(); })(window.gl || (window.gl = {}));
}).call(this);

View File

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

View File

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

View File

@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs';
class MergeConflictDataProvider { class MergeConflictDataProvider {
getInitialData() { getInitialData() {
// TODO: remove reliance on jQuery and DOM state introspection
const diffViewType = $.cookie('diff_view'); const diffViewType = $.cookie('diff_view');
const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
return { return {
isLoading : true, isLoading : true,
hasError : false, hasError : false,
isParallel : diffViewType === 'parallel', isParallel : diffViewType === 'parallel',
diffViewType : diffViewType, diffViewType : diffViewType,
fixedLayout : fixedLayout,
isSubmitting : false, isSubmitting : false,
conflictsData : {}, conflictsData : {},
resolutionData : {} resolutionData : {}
@ -192,14 +195,17 @@ class MergeConflictDataProvider {
updateViewType(newType) { updateViewType(newType) {
const vi = this.vueInstance; const vi = this.vueInstance;
if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) { if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
return; return;
} }
vi.diffView = newType; vi.diffViewType = newType;
vi.isParallel = newType === 'parallel'; vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType); // TODO: Make sure that cookie path added. $.cookie('diff_view', newType, {
$('.content-wrapper .container-fluid').toggleClass('container-limited'); path: (gon && gon.relative_url_root) || '/'
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
} }

View File

@ -60,9 +60,8 @@ class MergeConflictResolver {
$('#conflicts .js-syntax-highlight').syntaxHighlight(); $('#conflicts .js-syntax-highlight').syntaxHighlight();
}); });
if (this.vue.diffViewType === 'parallel') { $('.content-wrapper .container-fluid')
$('.content-wrapper .container-fluid').removeClass('container-limited'); .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
}
}) })
} }

View File

@ -36,13 +36,10 @@
}; };
MergeRequest.prototype.initTabs = function() { MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') { if (window.mrTabs) {
// `MergeRequests#new` has no tab-persisting or lazy-loading behavior window.mrTabs.unbindEvents();
window.mrTabs = new MergeRequestTabs(this.opts);
} else {
// Show the first tab (Commits)
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
} }
window.mrTabs = new MergeRequestTabs(this.opts);
}; };
MergeRequest.prototype.showAllCommits = function() { MergeRequest.prototype.showAllCommits = function() {

View File

@ -56,9 +56,14 @@
MergeRequestTabs.prototype.commitsLoaded = false; MergeRequestTabs.prototype.commitsLoaded = false;
MergeRequestTabs.prototype.fixedLayoutPref = null;
function MergeRequestTabs(opts) { function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {}; this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
this.buildsLoaded = this.opts.buildsLoaded || false;
this.setCurrentAction = bind(this.setCurrentAction, this); this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this); this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this); this.showTab = bind(this.showTab, this);
@ -70,7 +75,12 @@
MergeRequestTabs.prototype.bindEvents = function() { MergeRequestTabs.prototype.bindEvents = function() {
$(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
return $(document).on('click', '.js-show-tab', this.showTab); $(document).on('click', '.js-show-tab', this.showTab);
};
MergeRequestTabs.prototype.unbindEvents = function() {
$(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
$(document).off('click', '.js-show-tab', this.showTab);
}; };
MergeRequestTabs.prototype.showTab = function(event) { MergeRequestTabs.prototype.showTab = function(event) {
@ -85,11 +95,15 @@
if (action === 'commits') { if (action === 'commits') {
this.loadCommits($target.attr('href')); this.loadCommits($target.attr('href'));
this.expandView(); this.expandView();
} else if (action === 'diffs') { this.resetViewContainer();
} else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView(); this.shrinkView();
} }
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
navBarHeight = $('.navbar-gitlab').outerHeight(); navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo(".merge-request-details .merge-request-tabs", { $.scrollTo(".merge-request-details .merge-request-tabs", {
offset: -navBarHeight offset: -navBarHeight
@ -97,11 +111,14 @@
} else if (action === 'builds') { } else if (action === 'builds') {
this.loadBuilds($target.attr('href')); this.loadBuilds($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
this.loadPipelines($target.attr('href')); this.loadPipelines($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer();
} }
if (this.opts.setUrl) { if (this.opts.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
@ -126,7 +143,7 @@
if (action === 'show') { if (action === 'show') {
action = 'notes'; action = 'notes';
} }
return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
}; };
// Replaces the current Merge Request-specific action in the URL with a new one // Replaces the current Merge Request-specific action in the URL with a new one
@ -156,8 +173,9 @@
action = 'notes'; action = 'notes';
} }
this.currentAction = action; this.currentAction = action;
// Remove a trailing '/commits' or '/diffs' // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, ''); new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes' // Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') { if (action !== 'notes') {
new_state += "/" + action; new_state += "/" + action;
@ -196,8 +214,13 @@
if (this.diffsLoaded) { if (this.diffsLoaded) {
return; return;
} }
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
var url = gl.utils.parseUrl(source);
return this._get({ return this._get({
url: (source + ".json") + this._location.search, url: (url.pathname + ".json") + this._location.search,
success: (function(_this) { success: (function(_this) {
return function(data) { return function(data) {
$('#diffs').html(data.html); $('#diffs').html(data.html);
@ -209,7 +232,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff(); $('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel') { if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer(); _this.expandViewContainer();
} }
_this.diffsLoaded = true; _this.diffsLoaded = true;
@ -308,11 +331,25 @@
MergeRequestTabs.prototype.diffViewType = function() { MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
// Returns diff view type };
MergeRequestTabs.prototype.isDiffAction = function(action) {
return action === 'diffs' || action === 'new/diffs'
}; };
MergeRequestTabs.prototype.expandViewContainer = function() { MergeRequestTabs.prototype.expandViewContainer = function() {
return $('.container-fluid').removeClass('container-limited'); var $wrapper = $('.content-wrapper .container-fluid');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
$wrapper.removeClass('container-limited');
};
MergeRequestTabs.prototype.resetViewContainer = function() {
if (this.fixedLayoutPref !== null) {
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', this.fixedLayoutPref);
}
}; };
MergeRequestTabs.prototype.shrinkView = function() { MergeRequestTabs.prototype.shrinkView = function() {

View File

@ -7,7 +7,7 @@
this.currentProject = JSON.parse(currentProject); this.currentProject = JSON.parse(currentProject);
} }
$('.js-milestone-select').each(function(i, dropdown) { $('.js-milestone-select').each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId; var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
projectId = $dropdown.data('project-id'); projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones'); milestonesUrl = $dropdown.data('milestones');
@ -15,6 +15,7 @@
selectedMilestone = $dropdown.data('selected'); selectedMilestone = $dropdown.data('selected');
showNo = $dropdown.data('show-no'); showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any'); showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming'); showUpcoming = $dropdown.data('show-upcoming');
useId = $dropdown.data('use-id'); useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label'); defaultLabel = $dropdown.data('default-label');
@ -31,12 +32,12 @@
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
} }
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
return $.ajax({ return $.ajax({
url: milestonesUrl url: milestonesUrl
}).done(function(data) { }).done(function(data) {
var extraOptions; var extraOptions = [];
extraOptions = [];
if (showAny) { if (showAny) {
extraOptions.push({ extraOptions.push({
id: 0, id: 0,
@ -58,10 +59,14 @@
title: 'Upcoming' title: 'Upcoming'
}); });
} }
if (extraOptions.length > 2) { if (extraOptions.length) {
extraOptions.push('divider'); extraOptions.push('divider');
} }
return callback(extraOptions.concat(data));
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
}); });
}, },
filterable: true, filterable: true,
@ -69,19 +74,20 @@
fields: ['title'] fields: ['title']
}, },
selectable: true, selectable: true,
toggleLabel: function(selected) { toggleLabel: function(selected, el, e) {
if (selected && 'id' in selected) { if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title; return selected.title;
} else { } else {
return defaultLabel; return defaultLabel;
} }
}, },
defaultLabel: defaultLabel,
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
text: function(milestone) { text: function(milestone) {
return _.escape(milestone.title); return _.escape(milestone.title);
}, },
id: function(milestone) { id: function(milestone) {
if (!useId) { if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name; return milestone.name;
} else { } else {
return milestone.id; return milestone.id;
@ -100,7 +106,8 @@
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
if ($dropdown.hasClass('js-filter-bulk-update')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return; return;
} }
if (page === 'projects:boards:show') { if (page === 'projects:boards:show') {

View File

@ -1,47 +1,45 @@
(function() { ((global) => {
var GitLabCrop,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
GitLabCrop = (function() { // Matches everything but the file name
var FILENAMEREGEX; const FILENAMEREGEX = /^.*[\\\/]/;
// Matches everything but the file name class GitLabCrop {
FILENAMEREGEX = /^.*[\\\/]/; constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
function GitLabCrop(input, opts) { this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
var ref, ref1, ref2, ref3, ref4; this.onModalHide = this.onModalHide.bind(this);
if (opts == null) { this.onModalShow = this.onModalShow.bind(this);
opts = {}; this.onPickImageClick = this.onPickImageClick.bind(this);
}
this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this);
this.onModalHide = bind(this.onModalHide, this);
this.onModalShow = bind(this.onModalShow, this);
this.onPickImageClick = bind(this.onPickImageClick, this);
this.fileInput = $(input); this.fileInput = $(input);
// We should rename to avoid spec to fail
// Form will submit the proper input filed with a file using FormData
this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger");
// Set defaults
this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg;
// Required params
// Ensure needed elements are jquery objects
// If selector is provided we will convert them to a jQuery Object
this.filename = this.getElement(this.filename);
this.previewImage = this.getElement(this.previewImage);
this.pickImageEl = this.getElement(this.pickImageEl);
// Modal elements usually are outside the @form element
this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop;
this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn;
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`);
this.exportWidth = exportWidth;
this.exportHeight = exportHeight;
this.cropBoxWidth = cropBoxWidth;
this.cropBoxHeight = cropBoxHeight;
this.form = this.fileInput.parents('form');
this.filename = filename;
this.previewImage = previewImage;
this.modalCrop = modalCrop;
this.pickImageEl = pickImageEl;
this.uploadImageBtn = uploadImageBtn;
this.modalCropImg = modalCropImg;
this.filename = this.getElement(filename);
this.previewImage = this.getElement(previewImage);
this.pickImageEl = this.getElement(pickImageEl);
this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.cropActionsBtn = this.modalCrop.find('[data-method]'); this.cropActionsBtn = this.modalCrop.find('[data-method]');
this.bindEvents(); this.bindEvents();
} }
GitLabCrop.prototype.getElement = function(selector) { getElement(selector) {
return $(selector, this.form); return $(selector, this.form);
}; }
GitLabCrop.prototype.bindEvents = function() { bindEvents() {
var _this; var _this;
_this = this; _this = this;
this.fileInput.on('change', function(e) { this.fileInput.on('change', function(e) {
@ -57,13 +55,13 @@
return _this.onActionBtnClick(btn); return _this.onActionBtnClick(btn);
}); });
return this.croppedImageBlob = null; return this.croppedImageBlob = null;
}; }
GitLabCrop.prototype.onPickImageClick = function() { onPickImageClick() {
return this.fileInput.trigger('click'); return this.fileInput.trigger('click');
}; }
GitLabCrop.prototype.onModalShow = function() { onModalShow() {
var _this; var _this;
_this = this; _this = this;
return this.modalCropImg.cropper({ return this.modalCropImg.cropper({
@ -95,44 +93,44 @@
}); });
} }
}); });
}; }
GitLabCrop.prototype.onModalHide = function() { onModalHide() {
return this.modalCropImg.attr('src', '').cropper('destroy'); return this.modalCropImg.attr('src', '').cropper('destroy');
}; }
GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image onUploadImageBtnClick(e) {
e.preventDefault(); // Destroy cropper instance e.preventDefault();
this.setBlob(); this.setBlob();
this.setPreview(); this.setPreview();
this.modalCrop.modal('hide'); this.modalCrop.modal('hide');
return this.fileInput.val(''); return this.fileInput.val('');
}; }
GitLabCrop.prototype.onActionBtnClick = function(btn) { onActionBtnClick(btn) {
var data, result; var data, result;
data = $(btn).data(); data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) { if (this.modalCropImg.data('cropper') && data.method) {
return result = this.modalCropImg.cropper(data.method, data.option); return result = this.modalCropImg.cropper(data.method, data.option);
} }
}; }
GitLabCrop.prototype.onFileInputChange = function(e, input) { onFileInputChange(e, input) {
return this.readFile(input); return this.readFile(input);
}; }
GitLabCrop.prototype.readFile = function(input) { readFile(input) {
var _this, reader; var _this, reader;
_this = this; _this = this;
reader = new FileReader; reader = new FileReader;
reader.onload = function() { reader.onload = () => {
_this.modalCropImg.attr('src', reader.result); _this.modalCropImg.attr('src', reader.result);
return _this.modalCrop.modal('show'); return _this.modalCrop.modal('show');
}; };
return reader.readAsDataURL(input.files[0]); return reader.readAsDataURL(input.files[0]);
}; }
GitLabCrop.prototype.dataURLtoBlob = function(dataURL) { dataURLtoBlob(dataURL) {
var array, binary, i, k, len, v; var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]); binary = atob(dataURL.split(',')[1]);
array = []; array = [];
@ -143,35 +141,32 @@
return new Blob([new Uint8Array(array)], { return new Blob([new Uint8Array(array)], {
type: 'image/png' type: 'image/png'
}); });
}; }
GitLabCrop.prototype.setPreview = function() { setPreview() {
var filename; var filename;
this.previewImage.attr('src', this.dataURL); this.previewImage.attr('src', this.dataURL);
filename = this.fileInput.val().replace(FILENAMEREGEX, ''); filename = this.fileInput.val().replace(FILENAMEREGEX, '');
return this.filename.text(filename); return this.filename.text(filename);
}; }
GitLabCrop.prototype.setBlob = function() { setBlob() {
this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
width: 200, width: 200,
height: 200 height: 200
}).toDataURL('image/png'); }).toDataURL('image/png');
return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
}; }
GitLabCrop.prototype.getBlob = function() { getBlob() {
return this.croppedImageBlob; return this.croppedImageBlob;
}; }
}
return GitLabCrop;
})();
$.fn.glCrop = function(opts) { $.fn.glCrop = function(opts) {
return this.each(function() { return this.each(function() {
return $(this).data('glcrop', new GitLabCrop(this, opts)); return $(this).data('glcrop', new GitLabCrop(this, opts));
}); });
}; }
}).call(this); })(window.gl || (window.gl = {}));

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); data = groups.concat(projects);
return finalCallback(data); return finalCallback(data);
}; };
return Api.groups(term, false, groupsCallback); return Api.groups(term, false, false, groupsCallback);
}; };
} else { } else {
projectsCallback = finalCallback; projectsCallback = finalCallback;
@ -72,7 +72,7 @@
data = groups.concat(projects); data = groups.concat(projects);
return finalCallback(data); return finalCallback(data);
}; };
return Api.groups(query.term, false, groupsCallback); return Api.groups(query.term, false, false, groupsCallback);
}; };
} else { } else {
projectsCallback = finalCallback; projectsCallback = finalCallback;

View File

@ -10,7 +10,7 @@
filterable: true, filterable: true,
fieldName: 'group_id', fieldName: 'group_id',
data: function(term, callback) { data: function(term, callback) {
return Api.groups(term, null, function(data) { return Api.groups(term, false, false, function(data) {
data.unshift({ data.unshift({
name: 'Any' name: 'Any'
}); });

View File

@ -1,30 +1,21 @@
(function() { ((global) => {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.SearchAutocomplete = (function() { const KEYCODE = {
var KEYCODE; ESCAPE: 27,
BACKSPACE: 8,
ENTER: 13,
UP: 38,
DOWN: 40
};
KEYCODE = { class SearchAutocomplete {
ESCAPE: 27, constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
BACKSPACE: 8, this.bindEventContext();
ENTER: 13, this.wrap = wrap || $('.search');
UP: 38, this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
DOWN: 40 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') || '');
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
this.dropdown = this.wrap.find('.dropdown'); this.dropdown = this.wrap.find('.dropdown');
this.dropdownContent = this.dropdown.find('.dropdown-content'); this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge'); this.locationBadgeEl = this.getElement('.location-badge');
@ -46,19 +37,27 @@
} }
// Finds an element inside wrapper element // 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); return this.wrap.find(selector);
}; }
SearchAutocomplete.prototype.saveOriginalState = function() { saveOriginalState() {
return this.originalState = this.serializeState(); return this.originalState = this.serializeState();
}; }
SearchAutocomplete.prototype.saveTextLength = function() { saveTextLength() {
return this.lastTextLength = this.searchInput.val().length; return this.lastTextLength = this.searchInput.val().length;
}; }
SearchAutocomplete.prototype.createAutocomplete = function() { createAutocomplete() {
return this.searchInput.glDropdown({ return this.searchInput.glDropdown({
filterInputBlur: false, filterInputBlur: false,
filterable: true, filterable: true,
@ -73,9 +72,9 @@
selectable: true, selectable: true,
clicked: this.onClick.bind(this) clicked: this.onClick.bind(this)
}); });
}; }
SearchAutocomplete.prototype.getData = function(term, callback) { getData(term, callback) {
var _this, contents, jqXHR; var _this, contents, jqXHR;
_this = this; _this = this;
if (!term) { if (!term) {
@ -138,9 +137,9 @@
}).always(function() { }).always(function() {
return _this.loadingSuggestions = false; return _this.loadingSuggestions = false;
}); });
}; }
SearchAutocomplete.prototype.getCategoryContents = function() { getCategoryContents() {
var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
userId = gon.current_user_id; userId = gon.current_user_id;
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
@ -173,9 +172,9 @@
items.splice(0, 1); items.splice(0, 1);
} }
return items; return items;
}; }
SearchAutocomplete.prototype.serializeState = function() { serializeState() {
return { return {
// Search Criteria // Search Criteria
search_project_id: this.projectInputEl.val(), search_project_id: this.projectInputEl.val(),
@ -186,9 +185,9 @@
// Location badge // Location badge
_location: this.locationBadgeEl.text() _location: this.locationBadgeEl.text()
}; };
}; }
SearchAutocomplete.prototype.bindEvents = function() { bindEvents() {
this.searchInput.on('keydown', this.onSearchInputKeyDown); this.searchInput.on('keydown', this.onSearchInputKeyDown);
this.searchInput.on('keyup', this.onSearchInputKeyUp); this.searchInput.on('keyup', this.onSearchInputKeyUp);
this.searchInput.on('click', this.onSearchInputClick); this.searchInput.on('click', this.onSearchInputClick);
@ -200,9 +199,9 @@
return _this.searchInput.focus(); return _this.searchInput.focus();
}; };
})(this)); })(this));
}; }
SearchAutocomplete.prototype.enableAutocomplete = function() { enableAutocomplete() {
var _this; var _this;
// No need to enable anything if user is not logged in // No need to enable anything if user is not logged in
if (!gon.current_user_id) { if (!gon.current_user_id) {
@ -216,12 +215,12 @@
} }
}; };
SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
// Saves last length of the entered text // Saves last length of the entered text
onSearchInputKeyDown() {
return this.saveTextLength(); return this.saveTextLength();
}; }
SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) { onSearchInputKeyUp(e) {
switch (e.keyCode) { switch (e.keyCode) {
case KEYCODE.BACKSPACE: case KEYCODE.BACKSPACE:
// when trying to remove the location badge // when trying to remove the location badge
@ -259,54 +258,53 @@
} }
} }
this.wrap.toggleClass('has-value', !!e.target.value); this.wrap.toggleClass('has-value', !!e.target.value);
}; }
// Avoid falsy value to be returned // Avoid falsy value to be returned
SearchAutocomplete.prototype.onSearchInputClick = function(e) { onSearchInputClick(e) {
// Prevents closing the dropdown menu
return e.stopImmediatePropagation(); return e.stopImmediatePropagation();
}; }
SearchAutocomplete.prototype.onSearchInputFocus = function() { onSearchInputFocus() {
this.isFocused = true; this.isFocused = true;
this.wrap.addClass('search-active'); this.wrap.addClass('search-active');
if (this.getValue() === '') { if (this.getValue() === '') {
return this.getData(); return this.getData();
} }
}; }
SearchAutocomplete.prototype.getValue = function() { getValue() {
return this.searchInput.val(); return this.searchInput.val();
}; }
SearchAutocomplete.prototype.onClearInputClick = function(e) { onClearInputClick(e) {
e.preventDefault(); e.preventDefault();
return this.searchInput.val('').focus(); return this.searchInput.val('').focus();
}; }
SearchAutocomplete.prototype.onSearchInputBlur = function(e) { onSearchInputBlur(e) {
this.isFocused = false; this.isFocused = false;
this.wrap.removeClass('search-active'); this.wrap.removeClass('search-active');
// If input is blank then restore state // If input is blank then restore state
if (this.searchInput.val() === '') { if (this.searchInput.val() === '') {
return this.restoreOriginalState(); return this.restoreOriginalState();
} }
}; }
SearchAutocomplete.prototype.addLocationBadge = function(item) { addLocationBadge(item) {
var badgeText, category, value; var badgeText, category, value;
category = item.category != null ? item.category + ": " : ''; category = item.category != null ? item.category + ": " : '';
value = item.value != null ? item.value : ''; value = item.value != null ? item.value : '';
badgeText = "" + category + value; badgeText = "" + category + value;
this.locationBadgeEl.text(badgeText).show(); this.locationBadgeEl.text(badgeText).show();
return this.wrap.addClass('has-location-badge'); return this.wrap.addClass('has-location-badge');
}; }
SearchAutocomplete.prototype.hasLocationBadge = function() { hasLocationBadge() {
return this.wrap.is('.has-location-badge'); return this.wrap.is('.has-location-badge');
}; };
SearchAutocomplete.prototype.restoreOriginalState = function() { restoreOriginalState() {
var i, input, inputs, len; var i, input, inputs, len;
inputs = Object.keys(this.originalState); inputs = Object.keys(this.originalState);
for (i = 0, len = inputs.length; i < len; i++) { for (i = 0, len = inputs.length; i < len; i++) {
@ -320,13 +318,13 @@
value: this.originalState._location value: this.originalState._location
}); });
} }
}; }
SearchAutocomplete.prototype.badgePresent = function() { badgePresent() {
return this.locationBadgeEl.length; return this.locationBadgeEl.length;
}; }
SearchAutocomplete.prototype.resetSearchState = function() { resetSearchState() {
var i, input, inputs, len, results; var i, input, inputs, len, results;
inputs = Object.keys(this.originalState); inputs = Object.keys(this.originalState);
results = []; results = [];
@ -339,30 +337,30 @@
results.push(this.getElement("#" + input).val('')); results.push(this.getElement("#" + input).val(''));
} }
return results; return results;
}; }
SearchAutocomplete.prototype.removeLocationBadge = function() { removeLocationBadge() {
this.locationBadgeEl.hide(); this.locationBadgeEl.hide();
this.resetSearchState(); this.resetSearchState();
this.wrap.removeClass('has-location-badge'); this.wrap.removeClass('has-location-badge');
return this.disableAutocomplete(); return this.disableAutocomplete();
}; }
SearchAutocomplete.prototype.disableAutocomplete = function() { disableAutocomplete() {
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
this.searchInput.addClass('disabled'); this.searchInput.addClass('disabled');
this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
this.restoreMenu(); this.restoreMenu();
} }
}; }
SearchAutocomplete.prototype.restoreMenu = function() { restoreMenu() {
var html; var html;
html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>"; html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
return this.dropdownContent.html(html); return this.dropdownContent.html(html);
}; };
SearchAutocomplete.prototype.onClick = function(item, $el, e) { onClick(item, $el, e) {
if (location.pathname.indexOf(item.url) !== -1) { if (location.pathname.indexOf(item.url) !== -1) {
e.preventDefault(); e.preventDefault();
if (!this.badgePresent) { if (!this.badgePresent) {
@ -385,9 +383,9 @@
} }
}; };
return SearchAutocomplete; }
})(); global.SearchAutocomplete = SearchAutocomplete;
$(function() { $(function() {
var $projectOptionsDataEl = $('.js-search-project-options'); var $projectOptionsDataEl = $('.js-search-project-options');
@ -408,16 +406,16 @@
if ($groupOptionsDataEl.length) { if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {}; gl.groupOptions = gl.groupOptions || {};
var groupPath = $groupOptionsDataEl.data('group-path'); var groupPath = $groupOptionsDataEl.data('group-path');
gl.groupOptions[groupPath] = { gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'), name: $groupOptionsDataEl.data('name'),
issuesPath: $groupOptionsDataEl.data('issues-path'), issuesPath: $groupOptionsDataEl.data('issues-path'),
mrPath: $groupOptionsDataEl.data('mr-path') mrPath: $groupOptionsDataEl.data('mr-path')
}; };
} }
if ($dashboardOptionsDataEl.length) { if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = { gl.dashboardOptions = {
issuesPath: $dashboardOptionsDataEl.data('issues-path'), 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 */ /*= require ../blob/template_selector */
((global) => { ((global) => {
class IssuableTemplateSelector extends TemplateSelector { class IssuableTemplateSelector extends gl.TemplateSelector {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.projectPath = this.dropdown.data('project-path'); this.projectPath = this.dropdown.data('project-path');
@ -16,7 +16,7 @@
if (initialQuery.name) this.requestFile(initialQuery); if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => { $('.reset-template', this.dropdown.parent()).on('click', () => {
if (this.currentTemplate) this.setInputValueToTemplateContent(); if (this.currentTemplate) this.setInputValueToTemplateContent(false);
}); });
} }
@ -26,26 +26,28 @@
this.currentTemplate = currentTemplate; this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner(); this.stopLoadingSpinner();
this.setInputValueToTemplateContent(); this.setInputValueToTemplateContent(true);
}); });
return; return;
} }
setInputValueToTemplateContent() { setInputValueToTemplateContent(append) {
// `this.requestFileSuccess` sets the value of the description input field // `this.requestFileSuccess` sets the value of the description input field
// to the content of the template selected. // to the content of the template selected. If `append` is true, the
// template content will be appended to the previous value of the field,
// separated by a blank line if the previous value is non-empty.
if (this.titleInput.val() === '') { if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and // If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the 2nd // skip focusing the description input by setting `true` as the
// argument to `requestFileSuccess`. // `skipFocus` option to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, true); this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
this.titleInput.focus(); this.titleInput.focus();
} else { } else {
this.requestFileSuccess(this.currentTemplate); this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
} }
return; return;
} }
} }
global.IssuableTemplateSelector = IssuableTemplateSelector; global.IssuableTemplateSelector = IssuableTemplateSelector;
})(window); })(window.gl || (window.gl = {}));

View File

@ -1,12 +1,12 @@
((global) => { ((global) => {
class IssuableTemplateSelectors { class IssuableTemplateSelectors {
constructor(opts = {}) { constructor({ $dropdowns, editor } = {}) {
this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector'); this.$dropdowns = $dropdowns || $('.js-issuable-selector');
this.editor = opts.editor || this.initEditor(); this.editor = editor || this.initEditor();
this.$dropdowns.each((i, dropdown) => { this.$dropdowns.each((i, dropdown) => {
let $dropdown = $(dropdown); const $dropdown = $(dropdown);
new IssuableTemplateSelector({ new gl.IssuableTemplateSelector({
pattern: /(\.md)/, pattern: /(\.md)/,
data: $dropdown.data('data'), data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-issuable-selector-wrap'), wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
@ -26,4 +26,4 @@
} }
global.IssuableTemplateSelectors = IssuableTemplateSelectors; global.IssuableTemplateSelectors = IssuableTemplateSelectors;
})(window); })(window.gl || (window.gl = {}));

View File

@ -1,34 +1,29 @@
(function() { ((global) => {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.Todos = (function() { class Todos {
function Todos(opts) { constructor({ el } = {}) {
var ref; this.allDoneClicked = this.allDoneClicked.bind(this);
if (opts == null) { this.doneClicked = this.doneClicked.bind(this);
opts = {}; this.el = el || $('.js-todos-options');
}
this.allDoneClicked = bind(this.allDoneClicked, this);
this.doneClicked = bind(this.doneClicked, this);
this.el = (ref = opts.el) != null ? ref : $('.js-todos-options');
this.perPage = this.el.data('perPage'); this.perPage = this.el.data('perPage');
this.clearListeners(); this.clearListeners();
this.initBtnListeners(); this.initBtnListeners();
this.initFilters(); this.initFilters();
} }
Todos.prototype.clearListeners = function() { clearListeners() {
$('.done-todo').off('click'); $('.done-todo').off('click');
$('.js-todos-mark-all').off('click'); $('.js-todos-mark-all').off('click');
return $('.todo').off('click'); return $('.todo').off('click');
}; }
Todos.prototype.initBtnListeners = function() { initBtnListeners() {
$('.done-todo').on('click', this.doneClicked); $('.done-todo').on('click', this.doneClicked);
$('.js-todos-mark-all').on('click', this.allDoneClicked); $('.js-todos-mark-all').on('click', this.allDoneClicked);
return $('.todo').on('click', this.goToTodoUrl); return $('.todo').on('click', this.goToTodoUrl);
}; }
Todos.prototype.initFilters = function() { initFilters() {
new UsersSelect(); new UsersSelect();
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type'); this.initFilterDropdown($('.js-type-search'), 'type');
@ -38,125 +33,117 @@
event.preventDefault(); event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize()); Turbolinks.visit(this.action + '&' + $(this).serialize());
}); });
}; }
Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) { initFilterDropdown($dropdown, fieldName, searchFields) {
$dropdown.glDropdown({ $dropdown.glDropdown({
fieldName,
selectable: true, selectable: true,
filterable: searchFields ? true : false, filterable: searchFields ? true : false,
fieldName: fieldName,
search: { fields: searchFields }, search: { fields: searchFields },
data: $dropdown.data('data'), data: $dropdown.data('data'),
clicked: function() { clicked: function() {
return $dropdown.closest('form.filter-form').submit(); return $dropdown.closest('form.filter-form').submit();
} }
}) })
}; }
Todos.prototype.doneClicked = function(e) { doneClicked(e) {
var $this;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
$this = $(e.currentTarget); const $target = $(e.currentTarget);
$this.disable(); $target.disable();
return $.ajax({ return $.ajax({
type: 'POST', type: 'POST',
url: $this.attr('href'), url: $target.attr('href'),
dataType: 'json', dataType: 'json',
data: { data: {
'_method': 'delete' '_method': 'delete'
}, },
success: (function(_this) { success: (data) => {
return function(data) { this.redirectIfNeeded(data.count);
_this.redirectIfNeeded(data.count); this.clearDone($target.closest('li'));
_this.clearDone($this.closest('li')); return this.updateBadges(data);
return _this.updateBadges(data); }
};
})(this)
}); });
}; }
Todos.prototype.allDoneClicked = function(e) { allDoneClicked(e) {
var $this;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
$this = $(e.currentTarget); $target = $(e.currentTarget);
$this.disable(); $target.disable();
return $.ajax({ return $.ajax({
type: 'POST', type: 'POST',
url: $this.attr('href'), url: $target.attr('href'),
dataType: 'json', dataType: 'json',
data: { data: {
'_method': 'delete' '_method': 'delete'
}, },
success: (function(_this) { success: (data) => {
return function(data) { $target.remove();
$this.remove(); $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
$('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); return this.updateBadges(data);
return _this.updateBadges(data); }
};
})(this)
}); });
}; }
Todos.prototype.clearDone = function($row) { clearDone($row) {
var $ul; const $ul = $row.closest('ul');
$ul = $row.closest('ul');
$row.remove(); $row.remove();
if (!$ul.find('li').length) { if (!$ul.find('li').length) {
return $ul.parents('.panel').remove(); return $ul.parents('.panel').remove();
} }
}; }
Todos.prototype.updateBadges = function(data) { updateBadges(data) {
$('.todos-pending .badge, .todos-pending-count').text(data.count); $('.todos-pending .badge, .todos-pending-count').text(data.count);
return $('.todos-done .badge').text(data.done_count); return $('.todos-done .badge').text(data.done_count);
}; }
Todos.prototype.getTotalPages = function() { getTotalPages() {
return this.el.data('totalPages'); return this.el.data('totalPages');
}; }
Todos.prototype.getCurrentPage = function() { getCurrentPage() {
return this.el.data('currentPage'); return this.el.data('currentPage');
}; }
Todos.prototype.getTodosPerPage = function() { getTodosPerPage() {
return this.el.data('perPage'); return this.el.data('perPage');
}; }
redirectIfNeeded(total) {
const currPages = this.getTotalPages();
const currPage = this.getCurrentPage();
Todos.prototype.redirectIfNeeded = function(total) {
var currPage, currPages, newPages, pageParams, url;
currPages = this.getTotalPages();
currPage = this.getCurrentPage();
// Refresh if no remaining Todos // Refresh if no remaining Todos
if (!total) { if (!total) {
location.reload(); window.location.reload();
return; return;
} }
// Do nothing if no pagination // Do nothing if no pagination
if (!currPages) { if (!currPages) {
return; return;
} }
newPages = Math.ceil(total / this.getTodosPerPage());
// Includes query strings const newPages = Math.ceil(total / this.getTodosPerPage());
url = location.href; let url = location.href;
// If new total of pages is different than we have now
if (newPages !== currPages) { if (newPages !== currPages) {
// Redirect to previous page if there's one available // Redirect to previous page if there's one available
if (currPages > 1 && currPage === currPages) { if (currPages > 1 && currPage === currPages) {
pageParams = { const pageParams = {
page: currPages - 1 page: currPages - 1
}; };
url = gl.utils.mergeUrlParams(pageParams, url); url = gl.utils.mergeUrlParams(pageParams, url);
} }
return Turbolinks.visit(url); return Turbolinks.visit(url);
} }
}; }
Todos.prototype.goToTodoUrl = function(e) { goToTodoUrl(e) {
var todoLink; const todoLink = $(this).data('url');
todoLink = $(this).data('url');
if (!todoLink) { if (!todoLink) {
return; return;
} }
@ -167,10 +154,8 @@
} else { } else {
return Turbolinks.visit(todoLink); return Turbolinks.visit(todoLink);
} }
}; }
}
return Todos; global.Todos = Todos;
})(window.gl || (window.gl = {}));
})();
}).call(this);

View File

@ -1,7 +1,7 @@
(global => { ((global) => {
global.User = class { global.User = class {
constructor(opts) { constructor({ action }) {
this.opts = opts; this.action = action;
this.placeProfileAvatarsToTop(); this.placeProfileAvatarsToTop();
this.initTabs(); this.initTabs();
this.hideProjectLimitMessage(); this.hideProjectLimitMessage();
@ -14,9 +14,9 @@
} }
initTabs() { initTabs() {
return new UserTabs({ return new global.UserTabs({
parentEl: '.user-profile', 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) { $('.js-user-search').each((function(_this) {
return function(i, dropdown) { return function(i, dropdown) {
var options = {}; var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id'); options.projectId = $dropdown.data('project-id');
options.showCurrentUser = $dropdown.data('current-user'); options.showCurrentUser = $dropdown.data('current-user');
showNullUser = $dropdown.data('null-user'); showNullUser = $dropdown.data('null-user');
showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user'); showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user'); firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id'); options.authorId = $dropdown.data('author-id');
@ -73,6 +74,7 @@
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
var isAuthorFilter; var isAuthorFilter;
isAuthorFilter = $('.js-author-search'); isAuthorFilter = $('.js-author-search');
@ -116,8 +118,11 @@
if (showDivider) { if (showDivider) {
users.splice(showDivider, 0, "divider"); users.splice(showDivider, 0, "divider");
} }
// Send the data back
return callback(users); callback(users);
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
}); });
}, },
filterable: true, filterable: true,
@ -127,8 +132,8 @@
}, },
selectable: true, selectable: true,
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
toggleLabel: function(selected) { toggleLabel: function(selected, el) {
if (selected && 'id' in selected) { if (selected && 'id' in selected && $(el).hasClass('is-active')) {
if (selected.text) { if (selected.text) {
return selected.text; return selected.text;
} else { } else {
@ -138,6 +143,7 @@
return defaultLabel; return defaultLabel;
} }
}, },
defaultLabel: defaultLabel,
inputId: 'issue_assignee_id', inputId: 'issue_assignee_id',
hidden: function(e) { hidden: function(e) {
$selectbox.hide(); $selectbox.hide();
@ -149,7 +155,9 @@
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
if ($dropdown.hasClass('js-filter-bulk-update')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
selectedId = user.id;
return; return;
} }
if (page === 'projects:boards:show') { if (page === 'projects:boards:show') {
@ -167,6 +175,9 @@
return assignTo(selected); return assignTo(selected);
} }
}, },
id: function (user) {
return user.id;
},
renderRow: function(user) { renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
username = user.username ? "@" + user.username : ""; username = user.username ? "@" + user.username : "";

View File

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

View File

@ -21,7 +21,8 @@
.flash-notice, .flash-alert { .flash-notice, .flash-alert {
border-radius: $border-radius-default; border-radius: $border-radius-default;
.container-fluid.container-limited.flash-text { .container-fluid,
.container-fluid.container-limited {
background: transparent; background: transparent;
} }
} }
@ -35,12 +36,6 @@
} }
} }
.content-wrapper {
.flash-notice .container-fluid {
background-color: transparent;
}
}
@media (max-width: $screen-md-min) { @media (max-width: $screen-md-min) {
ul.notes { ul.notes {
.flash-container.timeline-content { .flash-container.timeline-content {

View File

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

View File

@ -22,6 +22,11 @@
.table.builds { .table.builds {
min-width: 1200px; min-width: 1200px;
.branch-commit {
width: 33%;
}
} }
} }
@ -385,6 +390,8 @@
left: auto; left: auto;
right: -214px; right: -214px;
top: -9px; top: -9px;
max-height: 245px;
overflow-y: scroll;
a:hover { a:hover {
.ci-status-text { .ci-status-text {

View File

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

View File

@ -51,6 +51,7 @@
-webkit-flex-direction: column; -webkit-flex-direction: column;
flex-direction: column; flex-direction: column;
margin-left: 10px; margin-left: 10px;
min-width: 55px;
} }
.todo-item { .todo-item {
@ -120,6 +121,14 @@
} }
} }
@media (max-width: $screen-sm-max) {
.todos-filters {
.dropdown-menu-toggle {
width: 135px;
}
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.todo { .todo {
.avatar { .avatar {
@ -141,4 +150,14 @@
padding-left: 10px; 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 # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
return locked_user_redirect(user) if user.access_locked?
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
setup_u2f_authentication(user) setup_u2f_authentication(user)
render 'devise/sessions/two_factor' render 'devise/sessions/two_factor'
end end
def locked_user_redirect(user)
flash.now[:alert] = 'Invalid Login or password'
render 'devise/sessions/new'
end
def authenticate_with_two_factor def authenticate_with_two_factor
user = self.resource = find_user user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id] if user.access_locked?
locked_user_redirect(user)
elsif user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user) authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id] elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user) authenticate_with_two_factor_via_u2f(user)
@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.' flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor prompt_for_two_factor(user)
end end
end end
@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.' flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end

View File

@ -15,18 +15,17 @@ module MembershipActions
end end
def leave def leave
@member = membershipable.members.find_by(user_id: current_user) || member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
membershipable.requesters.find_by(user_id: current_user) execute(:all)
Members::DestroyService.new(@member, current_user).execute
source_type = @member.real_source_type.humanize(capitalize: false) source_type = membershipable.class.to_s.humanize(capitalize: false)
notice = notice =
if @member.request? if member.request?
"Your access request to the #{source_type} has been withdrawn." "Your access request to the #{source_type} has been withdrawn."
else else
"You left the \"#{@member.source.human_name}\" #{source_type}." "You left the \"#{membershipable.human_name}\" #{source_type}."
end end
redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice redirect_to redirect_path, notice: notice
end end

View File

@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def trending def trending
@projects = TrendingProjectsFinder.new.execute(current_user) @projects = TrendingProjectsFinder.new.execute
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.page(params[:page]) @projects = @projects.page(params[:page])

View File

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

View File

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

View File

@ -5,17 +5,25 @@ class Projects::GroupLinksController < Projects::ApplicationController
def index def index
@group_links = project.project_group_links.all @group_links = project.project_group_links.all
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << project.group.try(:id)
end end
def create def create
group = Group.find(params[:link_group_id]) group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create( if group
group: group, return render_404 unless can?(current_user, :read_group, group)
group_access: params[:link_group_access],
expires_at: params[:expires_at] 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) redirect_to namespace_project_group_links_path(project.namespace, project)
end end

View File

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

View File

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

View File

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

View File

@ -1,11 +1,16 @@
# Finder for retrieving public trending projects in a given time range.
class TrendingProjectsFinder class TrendingProjectsFinder
def execute(current_user, start_date = 1.month.ago) # current_user - The currently logged in User, if any.
projects_for(current_user).trending(start_date) # last_months - The number of months to limit the trending data to.
def execute(months_limit = 1)
Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do
Project.public_only.trending(months_limit.months.ago)
end
end end
private private
def projects_for(current_user) def cache_key_for(months)
ProjectsFinder.new.execute(current_user) "trending_projects/#{months}"
end end
end end

View File

@ -40,8 +40,9 @@ module DropdownsHelper
end end
def dropdown_toggle(toggle_text, data_attr, options = {}) def dropdown_toggle(toggle_text, data_attr, options = {})
default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down') output << icon('chevron-down')
output.html_safe output.html_safe
end end

View File

@ -8,18 +8,12 @@ module IssuablesHelper
end end
def multi_label_name(current_labels, default_label) def multi_label_name(current_labels, default_label)
# current_labels may be a string from before if current_labels && current_labels.any?
if current_labels.is_a?(Array) title = current_labels.first.try(:title)
if current_labels.count > 1 if current_labels.size > 1
"#{current_labels[0]} +#{current_labels.count - 1} more" "#{title} +#{current_labels.size - 1} more"
else else
current_labels[0] title
end
elsif current_labels.is_a?(String)
if current_labels.nil? || current_labels.empty?
default_label
else
current_labels
end end
else else
default_label default_label

View File

@ -115,8 +115,9 @@ module LabelsHelper
end end
def labels_filter_path def labels_filter_path
if @project project = @target_project || @project
namespace_project_labels_path(@project.namespace, @project, :json) if project
namespace_project_labels_path(project.namespace, project, :json)
else else
dashboard_labels_path(:json) dashboard_labels_path(:json)
end end

View File

@ -71,8 +71,9 @@ module MilestonesHelper
end end
def milestones_filter_dropdown_path def milestones_filter_dropdown_path
if @project project = @target_project || @project
namespace_project_milestones_path(@project.namespace, @project, :json) if project
namespace_project_milestones_path(project.namespace, project, :json)
else else
dashboard_milestones_path(:json) dashboard_milestones_path(:json)
end end

View File

@ -92,12 +92,8 @@ module PageLayoutHelper
end end
end end
def fluid_layout(enabled = false) def fluid_layout
if @fluid_layout.nil? current_user && current_user.layout == "fluid"
@fluid_layout = (current_user && current_user.layout == "fluid") || enabled
else
@fluid_layout
end
end end
def blank_container(enabled = false) def blank_container(enabled = false)

View File

@ -49,12 +49,10 @@ module SelectsHelper
end end
def select2_tag(id, opts = {}) def select2_tag(id, opts = {})
css_class = '' opts[:class] << ' multiselect' if opts[:multiple]
css_class << 'multiselect ' if opts[:multiple]
css_class << (opts[:class] || '')
value = opts[:selected] || '' value = opts[:selected] || ''
hidden_field_tag(id, value, class: css_class) hidden_field_tag(id, value, opts)
end end
private private

View File

@ -114,6 +114,26 @@ module TodosHelper
selected_type ? selected_type[:text] : default_type selected_type ? selected_type[:text] : default_type
end end
def todo_due_date(todo)
return unless todo.target.try(:due_date)
is_due_today = todo.target.due_date.today?
is_overdue = todo.target.overdue?
css_class =
if is_due_today
'text-warning'
elsif is_overdue
'text-danger'
else
''
end
html = "&middot; ".html_safe
html << content_tag(:span, class: css_class) do
"Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}"
end
end
private private
def show_todo_state?(todo) def show_todo_state?(todo)

View File

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

View File

@ -2,7 +2,7 @@ module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Ci::Model extend Ci::Model
LAST_CONTACT_TIME = 2.hours.ago LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online] AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked] FORM_EDITABLE = %i[description tag_list active run_untagged locked]

View File

@ -24,7 +24,22 @@ class CommitStatus < ActiveRecord::Base
scope :retried, -> { where.not(id: latest) } scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) } scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled])
end
scope :exclude_ignored, -> do
quoted_when = connection.quote_column_name('when')
# We want to ignore failed_but_allowed jobs
where("allow_failure = ? OR status IN (?)",
false, all_state_names - [:failed, :canceled]).
# We want to ignore skipped manual jobs
where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped').
# We want to ignore skipped on_failure
where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
end
scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) } scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) } scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
@ -69,13 +84,18 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now commit_status.update_attributes finished_at: Time.now
end end
after_transition any => [:success, :failed, :canceled] do |commit_status|
commit_status.pipeline.try(:process!)
true
end
after_transition do |commit_status, transition| after_transition do |commit_status, transition|
commit_status.pipeline.try(:build_updated) unless transition.loopback? commit_status.pipeline.try do |pipeline|
break if transition.loopback?
if commit_status.complete?
ProcessPipelineWorker.perform_async(pipeline.id)
end
UpdatePipelineWorker.perform_async(pipeline.id)
end
true
end end
after_transition [:created, :pending, :running] => :success do |commit_status| after_transition [:created, :pending, :running] => :success do |commit_status|
@ -111,7 +131,7 @@ class CommitStatus < ActiveRecord::Base
end end
end end
def ignored? def failed_but_allowed?
allow_failure? && (failed? || canceled?) allow_failure? && (failed? || canceled?)
end end

View File

@ -8,32 +8,32 @@ module HasStatus
class_methods do class_methods do
def status_sql def status_sql
scope = all scope = if respond_to?(:exclude_ignored)
exclude_ignored
else
all
end
builds = scope.select('count(*)').to_sql builds = scope.select('count(*)').to_sql
created = scope.created.select('count(*)').to_sql created = scope.created.select('count(*)').to_sql
success = scope.success.select('count(*)').to_sql success = scope.success.select('count(*)').to_sql
ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
ignored ||= '0'
pending = scope.pending.select('count(*)').to_sql pending = scope.pending.select('count(*)').to_sql
running = scope.running.select('count(*)').to_sql running = scope.running.select('count(*)').to_sql
canceled = scope.canceled.select('count(*)').to_sql
skipped = scope.skipped.select('count(*)').to_sql skipped = scope.skipped.select('count(*)').to_sql
canceled = scope.canceled.select('count(*)').to_sql
deduce_status = "(CASE "(CASE
WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created' WHEN (#{builds})=(#{created}) THEN 'created'
WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
ELSE 'failed' ELSE 'failed'
END)" END)"
deduce_status
end end
def status def status
all.pluck(self.status_sql).first all.pluck(status_sql).first
end end
def started_at def started_at
@ -43,6 +43,10 @@ module HasStatus
def finished_at def finished_at
all.maximum(:finished_at) all.maximum(:finished_at)
end end
def all_state_names
state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) }
end
end end
included do included do

View File

@ -43,19 +43,15 @@ module Mentionable
self self
end end
def all_references(current_user = nil, text = nil, extractor: nil) def all_references(current_user = nil, extractor: nil)
extractor ||= Gitlab::ReferenceExtractor. extractor ||= Gitlab::ReferenceExtractor.
new(project, current_user) new(project, current_user)
if text self.class.mentionable_attrs.each do |attr, options|
extractor.analyze(text, author: author) text = __send__(attr)
else options = options.merge(cache_key: [self, attr], author: author)
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
options = options.merge(cache_key: [self, attr], author: author)
extractor.analyze(text, options) extractor.analyze(text, options)
end
end end
extractor extractor
@ -66,8 +62,8 @@ module Mentionable
end end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author, text = nil) def referenced_mentionables(current_user = self.author)
refs = all_references(current_user, text) refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits) refs = (refs.issues + refs.merge_requests + refs.commits)
# We're using this method instead of Array diffing because that requires # We're using this method instead of Array diffing because that requires
@ -77,8 +73,8 @@ module Mentionable
end end
# Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+. # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
def create_cross_references!(author = self.author, without = [], text = nil) def create_cross_references!(author = self.author, without = [])
refs = referenced_mentionables(author, text) refs = referenced_mentionables(author)
# We're using this method instead of Array diffing because that requires # We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the # both of the object's `hash` values to be the same, which may not be the
@ -97,10 +93,7 @@ module Mentionable
return if changes.empty? return if changes.empty?
original_text = changes.collect { |_, vals| vals.first }.join(' ') create_cross_references!(author)
preexisting = referenced_mentionables(author, original_text)
create_cross_references!(author, preexisting)
end end
private private

View File

@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true delegate :name, to: :environment, prefix: true
after_save :keep_around_commit after_save :create_ref
def commit def commit
project.commit(sha) project.commit(sha)
@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base
self == environment.last_deployment self == environment.last_deployment
end end
def keep_around_commit def create_ref
project.repository.keep_around(self.sha) project.repository.create_ref(ref, ref_path)
end end
def manual_actions def manual_actions
@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base
where.not(id: self.id). where.not(id: self.id).
take take
end end
private
def ref_path
File.join(environment.ref_path, 'deployments', id.to_s)
end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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
end end
def file_has_changed?
return false unless @last_commit_sha && last_commit
@last_commit_sha != last_commit.sha
end
def last_commit def last_commit
@last_commit ||= Gitlab::Git::Commit. @last_commit ||= Gitlab::Git::Commit.
last_for_path(@source_project.repository, @source_branch, @file_path) last_for_path(@source_project.repository, @source_branch, @file_path)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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