Merge branch 'master' into members-ui
This commit is contained in:
commit
996e802415
|
|
@ -207,9 +207,7 @@ rubocop: *exec
|
|||
rake haml_lint: *exec
|
||||
rake scss_lint: *exec
|
||||
rake brakeman: *exec
|
||||
rake flay:
|
||||
<<: *exec
|
||||
allow_failure: yes
|
||||
rake flay: *exec
|
||||
license_finder: *exec
|
||||
rake downtime_check: *exec
|
||||
|
||||
|
|
|
|||
|
|
@ -453,6 +453,10 @@ Style/VariableName:
|
|||
EnforcedStyle: snake_case
|
||||
Enabled: true
|
||||
|
||||
# Use the configured style when numbering variables.
|
||||
Style/VariableNumber:
|
||||
Enabled: false
|
||||
|
||||
# Use when x then ... for one-line cases.
|
||||
Style/WhenThen:
|
||||
Enabled: true
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --exclude-limit 0`
|
||||
# on 2016-09-14 15:44:53 -0400 using RuboCop version 0.42.0.
|
||||
# on 2016-10-04 13:16:20 +0200 using RuboCop version 0.43.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 158
|
||||
# Offense count: 160
|
||||
Lint/AmbiguousRegexpLiteral:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 41
|
||||
# Offense count: 40
|
||||
# Configuration parameters: AllowSafeAssignment.
|
||||
Lint/AssignmentInCondition:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 16
|
||||
# Offense count: 18
|
||||
Lint/HandleExceptions:
|
||||
Enabled: false
|
||||
|
||||
|
|
@ -23,11 +23,21 @@ Lint/HandleExceptions:
|
|||
Lint/Loop:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 16
|
||||
# Offense count: 19
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 49
|
||||
# Offense count: 9
|
||||
# Cop supports --auto-correct.
|
||||
Lint/UnifiedInteger:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 13
|
||||
# Cop supports --auto-correct.
|
||||
Lint/UnneededSplatExpansion:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 69
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
|
||||
Lint/UnusedBlockArgument:
|
||||
|
|
@ -39,32 +49,81 @@ Lint/UnusedBlockArgument:
|
|||
Lint/UnusedMethodArgument:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 9
|
||||
# Cop supports --auto-correct.
|
||||
Performance/PushSplat:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
Performance/RedundantBlockCall:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 4
|
||||
# Offense count: 5
|
||||
# Cop supports --auto-correct.
|
||||
Performance/RedundantMatch:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 27
|
||||
# Offense count: 26
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: MaxKeyValuePairs.
|
||||
Performance/RedundantMerge:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 61
|
||||
# Offense count: 7
|
||||
RSpec/BeEql:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 20
|
||||
# Configuration parameters: CustomIncludeMethods.
|
||||
RSpec/EmptyExampleGroup:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 16
|
||||
RSpec/ExpectActual:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 34
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: implicit, each, example
|
||||
RSpec/HookArgument:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 168
|
||||
RSpec/LeadingSubject:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 162
|
||||
RSpec/LetSetup:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 10
|
||||
RSpec/MessageChain:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 714
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: allow, expect
|
||||
RSpec/MessageExpectation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 2423
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 36
|
||||
|
||||
# Offense count: 1504
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1335
|
||||
# Configuration parameters: MaxNesting.
|
||||
RSpec/NestedGroups:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 99
|
||||
RSpec/SubjectStub:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 64
|
||||
Rails/OutputSafety:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 129
|
||||
# Offense count: 151
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: strict, flexible
|
||||
Rails/TimeZone:
|
||||
|
|
@ -77,58 +136,63 @@ Rails/TimeZone:
|
|||
Rails/Validation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 273
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
Security/JSONLoad:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 284
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
|
||||
# SupportedStyles: with_first_parameter, with_fixed_indentation
|
||||
Style/AlignParameters:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 30
|
||||
# Offense count: 28
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: always, conditionals
|
||||
Style/AndOr:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 50
|
||||
# Offense count: 52
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: percent_q, bare_percent
|
||||
Style/BarePercentLiterals:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 289
|
||||
# Offense count: 291
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: braces, no_braces, context_dependent
|
||||
Style/BracesAroundHashParameters:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 5
|
||||
# Offense count: 6
|
||||
Style/CaseEquality:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 19
|
||||
# Offense count: 26
|
||||
# Cop supports --auto-correct.
|
||||
Style/ColonMethodCall:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 3
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: Keywords.
|
||||
# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
|
||||
Style/CommentAnnotation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 33
|
||||
# Offense count: 30
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
|
||||
# SupportedStyles: assign_to_condition, assign_inside_condition
|
||||
Style/ConditionalAssignment:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 881
|
||||
# Offense count: 957
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: leading, trailing
|
||||
|
|
@ -139,12 +203,12 @@ Style/DotPosition:
|
|||
Style/DoubleNegation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 4
|
||||
# Offense count: 6
|
||||
# Cop supports --auto-correct.
|
||||
Style/EachWithObject:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 25
|
||||
# Offense count: 26
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: empty, nil, both
|
||||
|
|
@ -156,24 +220,24 @@ Style/EmptyElse:
|
|||
Style/EmptyLiteral:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 135
|
||||
# Offense count: 140
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
|
||||
Style/ExtraSpacing:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 7
|
||||
# Offense count: 6
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: format, sprintf, percent
|
||||
Style/FormatString:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 51
|
||||
# Offense count: 201
|
||||
# Configuration parameters: MinBodyLength.
|
||||
Style/GuardClause:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 9
|
||||
# Offense count: 11
|
||||
Style/IfInsideElse:
|
||||
Enabled: false
|
||||
|
||||
|
|
@ -183,21 +247,21 @@ Style/IfInsideElse:
|
|||
Style/IfUnlessModifier:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 52
|
||||
# Offense count: 53
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
|
||||
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
|
||||
Style/IndentArray:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 97
|
||||
# Offense count: 95
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
|
||||
# SupportedStyles: special_inside_parentheses, consistent, align_braces
|
||||
Style/IndentHash:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 12
|
||||
# Offense count: 29
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: line_count_dependent, lambda, literal
|
||||
|
|
@ -209,7 +273,7 @@ Style/Lambda:
|
|||
Style/LineEndConcatenation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 13
|
||||
# Offense count: 15
|
||||
# Cop supports --auto-correct.
|
||||
Style/MethodCallParentheses:
|
||||
Enabled: false
|
||||
|
|
@ -218,7 +282,7 @@ Style/MethodCallParentheses:
|
|||
Style/MethodMissing:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 85
|
||||
# Offense count: 95
|
||||
# Cop supports --auto-correct.
|
||||
Style/MutableConstant:
|
||||
Enabled: false
|
||||
|
|
@ -235,14 +299,14 @@ Style/NestedParenthesizedCalls:
|
|||
Style/Next:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 8
|
||||
# Offense count: 12
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
|
||||
# SupportedOctalStyles: zero_with_o, zero_only
|
||||
Style/NumericLiteralPrefix:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 64
|
||||
# Offense count: 53
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: predicate, comparison
|
||||
|
|
@ -254,7 +318,7 @@ Style/NumericPredicate:
|
|||
Style/ParallelAssignment:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 264
|
||||
# Offense count: 294
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: PreferredDelimiters.
|
||||
Style/PercentLiteralDelimiters:
|
||||
|
|
@ -272,7 +336,7 @@ Style/PercentQLiterals:
|
|||
Style/PerlBackrefs:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 35
|
||||
# Offense count: 38
|
||||
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
|
||||
# NamePrefix: is_, has_, have_
|
||||
# NamePrefixBlacklist: is_, has_, have_
|
||||
|
|
@ -280,7 +344,7 @@ Style/PerlBackrefs:
|
|||
Style/PredicateName:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 27
|
||||
# Offense count: 26
|
||||
# Cop supports --auto-correct.
|
||||
Style/PreferredHashMethods:
|
||||
Enabled: false
|
||||
|
|
@ -312,12 +376,12 @@ Style/RedundantException:
|
|||
Style/RedundantFreeze:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 408
|
||||
# Offense count: 427
|
||||
# Cop supports --auto-correct.
|
||||
Style/RedundantSelf:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 93
|
||||
# Offense count: 97
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
|
||||
# SupportedStyles: slashes, percent_r, mixed
|
||||
|
|
@ -329,7 +393,12 @@ Style/RegexpLiteral:
|
|||
Style/RescueModifier:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 5
|
||||
# Offense count: 114
|
||||
# Cop supports --auto-correct.
|
||||
Style/SafeNavigation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
Style/SelfAssignment:
|
||||
Enabled: false
|
||||
|
|
@ -346,7 +415,7 @@ Style/SingleLineBlockParams:
|
|||
Style/SingleLineMethods:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 124
|
||||
# Offense count: 125
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: space, no_space
|
||||
|
|
@ -359,19 +428,19 @@ Style/SpaceBeforeBlockBraces:
|
|||
Style/SpaceBeforeFirstArg:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 141
|
||||
# Offense count: 145
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
|
||||
# SupportedStyles: space, no_space
|
||||
Style/SpaceInsideBlockBraces:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 96
|
||||
# Offense count: 99
|
||||
# Cop supports --auto-correct.
|
||||
Style/SpaceInsideBrackets:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 62
|
||||
# Offense count: 65
|
||||
# Cop supports --auto-correct.
|
||||
Style/SpaceInsideParens:
|
||||
Enabled: false
|
||||
|
|
@ -381,21 +450,21 @@ Style/SpaceInsideParens:
|
|||
Style/SpaceInsidePercentLiteralDelimiters:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 40
|
||||
# Offense count: 41
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: SupportedStyles.
|
||||
# SupportedStyles: use_perl_names, use_english_names
|
||||
Style/SpecialGlobalVars:
|
||||
EnforcedStyle: use_perl_names
|
||||
|
||||
# Offense count: 30
|
||||
# Offense count: 31
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: single_quotes, double_quotes
|
||||
Style/StringLiteralsInInterpolation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 32
|
||||
# Offense count: 33
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: IgnoredMethods.
|
||||
# IgnoredMethods: respond_to, define_method
|
||||
|
|
@ -409,7 +478,7 @@ Style/SymbolProc:
|
|||
Style/TernaryParentheses:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 24
|
||||
# Offense count: 29
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
|
||||
# SupportedStyles: comma, consistent_comma, no_comma
|
||||
|
|
|
|||
45
CHANGELOG
45
CHANGELOG
|
|
@ -1,6 +1,7 @@
|
|||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
v 8.13.0 (unreleased)
|
||||
- Update runner version only when updating contacted_at
|
||||
- Add link from system note to compare with previous version
|
||||
- Use gitlab-shell v3.6.2 (GIT TRACE logging)
|
||||
- Fix centering of custom header logos (Ashley Dumaine)
|
||||
|
|
@ -8,29 +9,44 @@ v 8.13.0 (unreleased)
|
|||
- Replaced the check sign to arrow in the show build view. !6501
|
||||
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
|
||||
- Speed-up group milestones show page
|
||||
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
|
||||
- Keep refs for each deployment
|
||||
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
|
||||
- Add more tests for calendar contribution (ClemMakesApps)
|
||||
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
|
||||
- Simplify Mentionable concern instance methods
|
||||
- Fix permission for setting an issue's due date
|
||||
- API: Multi-file commit !6096 (mahcsig)
|
||||
- Revert "Label list shows all issues (opened or closed) with that label"
|
||||
- Expose expires_at field when sharing project on API
|
||||
- Fix VueJS template tags being rendered in code comments
|
||||
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
|
||||
- Add Issue Board API support (andrebsguedes)
|
||||
- Allow the Koding integration to be configured through the API
|
||||
- Added soft wrap button to repository file/blob editor
|
||||
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
|
||||
- Fix todos page mobile viewport layout (ClemMakesApps)
|
||||
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
|
||||
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Fix that manual jobs would no longer block jobs in the next stage. !6604
|
||||
- Add configurable email subject suffix (Fu Xu)
|
||||
- Use a ConnectionPool for Rails.cache on Sidekiq servers
|
||||
- Replace `alias_method_chain` with `Module#prepend`
|
||||
- Enable GitLab Import/Export for non-admin users.
|
||||
- Preserve label filters when sorting !6136 (Joseph Frazier)
|
||||
- MergeRequest#new form load diff asynchronously
|
||||
- Only update issuable labels if they have been changed
|
||||
- Take filters in account in issuable counters. !6496
|
||||
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
|
||||
- Prevent flash alert text from being obscured when container is fluid
|
||||
- Append issue template to existing description !6149 (Joseph Frazier)
|
||||
- Trending projects now only show public projects and the list of projects is cached for a day
|
||||
- Revoke button in Applications Settings underlines on hover.
|
||||
- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
|
||||
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Fix Long commit messages overflow viewport in file tree
|
||||
- Revert avoid touching file system on Build#artifacts?
|
||||
- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
|
||||
- Add broadcast messages and alerts below sub-nav
|
||||
- Better empty state for Groups view
|
||||
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
|
||||
|
|
@ -40,17 +56,35 @@ v 8.13.0 (unreleased)
|
|||
- Optimize GitHub importing for speed and memory
|
||||
- API: expose pipeline data in builds API (!6502, Guilherme Salazar)
|
||||
- Notify the Merger about merge after successful build (Dimitris Karakasilis)
|
||||
- Reduce queries needed to find users using their SSH keys when pushing commits
|
||||
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Fix broken repository 500 errors in project list
|
||||
- Fix Pipeline list commit column width should be adjusted
|
||||
- Close todos when accepting merge requests via the API !6486 (tonygambone)
|
||||
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
|
||||
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
|
||||
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
|
||||
- Grouped pipeline dropdown is a scrollable container
|
||||
|
||||
v 8.12.4 (unreleased)
|
||||
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. (lukehowell)
|
||||
v 8.12.5 (unreleased)
|
||||
|
||||
v 8.12.4
|
||||
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
|
||||
- Fix padding in build sidebar. !6506
|
||||
- Changed compare dropdowns to dropdowns with isolated search input. !6550
|
||||
- Fix race condition on LFS Token. !6592
|
||||
- Fix type mismatch bug when closing Jira issue. !6619
|
||||
- Fix lint-doc error. !6623
|
||||
- Skip wiki creation when GitHub project has wiki enabled. !6665
|
||||
- Fix issues importing services via Import/Export. !6667
|
||||
- Restrict failed login attempts for users with 2FA enabled. !6668
|
||||
- Fix failed project deletion when feature visibility set to private. !6688
|
||||
- Prevent claiming associated model IDs via import.
|
||||
- Set GitLab project exported file permissions to owner only
|
||||
|
||||
v 8.12.3
|
||||
- Update Gitlab Shell to support low IO priority for storage moves
|
||||
|
||||
v 8.12.2 (unreleased)
|
||||
v 8.12.2
|
||||
- Fix Import/Export not recognising correctly the imported services.
|
||||
- Fix snippets pagination
|
||||
- Fix "Create project" button layout when visibility options are restricted
|
||||
|
|
@ -64,6 +98,7 @@ v 8.12.2 (unreleased)
|
|||
- Only update issuable labels if they have been changed
|
||||
- Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
|
||||
- Fix resolve discussion buttons endpoint path
|
||||
- Refactor remnants of CoffeeScript destructured opts and super !6261
|
||||
|
||||
v 8.12.1
|
||||
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
|
||||
|
|
@ -104,6 +139,7 @@ v 8.12.0
|
|||
- Fix long comments in diffs messing with table width
|
||||
- Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
|
||||
- Fix pagination on user snippets page
|
||||
- Honor "fixed layout" preference in more places !6422
|
||||
- Run CI builds with the permissions of users !5735
|
||||
- Fix sorting of issues in API
|
||||
- Fix download artifacts button links !6407
|
||||
|
|
@ -144,6 +180,7 @@ v 8.12.0
|
|||
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
|
||||
- Add textarea autoresize after comment (ClemMakesApps)
|
||||
- Do not write SSH public key 'comments' to authorized_keys !6381
|
||||
- Add due date to issue todos
|
||||
- Refresh todos count cache when an Issue/MR is deleted
|
||||
- Fix branches page dropdown sort alignment (ClemMakesApps)
|
||||
- Hides merge request button on branches page is user doesn't have permissions
|
||||
|
|
|
|||
6
Gemfile
6
Gemfile
|
|
@ -130,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0'
|
|||
gem 'after_commit_queue', '~> 1.3.0'
|
||||
|
||||
# Issue tags
|
||||
gem 'acts-as-taggable-on', '~> 3.4'
|
||||
gem 'acts-as-taggable-on', '~> 4.0'
|
||||
|
||||
# Background jobs
|
||||
gem 'sidekiq', '~> 4.2'
|
||||
|
|
@ -231,7 +231,7 @@ gem 'net-ssh', '~> 3.0.1'
|
|||
gem 'base32', '~> 0.3.0'
|
||||
|
||||
# Sentry integration
|
||||
gem 'sentry-raven', '~> 1.1.0'
|
||||
gem 'sentry-raven', '~> 2.0.0'
|
||||
|
||||
gem 'premailer-rails', '~> 1.9.0'
|
||||
|
||||
|
|
@ -295,7 +295,7 @@ group :development, :test do
|
|||
gem 'spring-commands-spinach', '~> 1.1.0'
|
||||
gem 'spring-commands-teaspoon', '~> 0.0.2'
|
||||
|
||||
gem 'rubocop', '~> 0.42.0', require: false
|
||||
gem 'rubocop', '~> 0.43.0', require: false
|
||||
gem 'rubocop-rspec', '~> 1.5.0', require: false
|
||||
gem 'scss_lint', '~> 0.47.0', require: false
|
||||
gem 'haml_lint', '~> 0.18.2', require: false
|
||||
|
|
|
|||
20
Gemfile.lock
20
Gemfile.lock
|
|
@ -44,8 +44,8 @@ GEM
|
|||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
acts-as-taggable-on (3.5.0)
|
||||
activerecord (>= 3.2, < 5)
|
||||
acts-as-taggable-on (4.0.0)
|
||||
activerecord (>= 4.0)
|
||||
addressable (2.3.8)
|
||||
after_commit_queue (1.3.0)
|
||||
activerecord (>= 3.0)
|
||||
|
|
@ -487,7 +487,7 @@ GEM
|
|||
orm_adapter (0.5.0)
|
||||
paranoia (2.1.4)
|
||||
activerecord (~> 4.0)
|
||||
parser (2.3.1.2)
|
||||
parser (2.3.1.4)
|
||||
ast (~> 2.2)
|
||||
pg (0.18.4)
|
||||
pkg-config (1.1.7)
|
||||
|
|
@ -620,7 +620,7 @@ GEM
|
|||
rspec-retry (0.4.5)
|
||||
rspec-core
|
||||
rspec-support (3.5.0)
|
||||
rubocop (0.42.0)
|
||||
rubocop (0.43.0)
|
||||
parser (>= 2.3.1.1, < 3.0)
|
||||
powerpack (~> 0.1)
|
||||
rainbow (>= 1.99.1, < 3.0)
|
||||
|
|
@ -665,8 +665,8 @@ GEM
|
|||
activesupport (>= 3.1)
|
||||
select2-rails (3.5.9.3)
|
||||
thor (~> 0.14)
|
||||
sentry-raven (1.1.0)
|
||||
faraday (>= 0.7.6)
|
||||
sentry-raven (2.0.2)
|
||||
faraday (>= 0.7.6, < 0.10.x)
|
||||
settingslogic (2.0.9)
|
||||
sexp_processor (4.7.0)
|
||||
sham_rack (1.3.6)
|
||||
|
|
@ -802,7 +802,7 @@ DEPENDENCIES
|
|||
RedCloth (~> 4.3.2)
|
||||
ace-rails-ap (~> 4.1.0)
|
||||
activerecord-session_store (~> 1.0.0)
|
||||
acts-as-taggable-on (~> 3.4)
|
||||
acts-as-taggable-on (~> 4.0)
|
||||
addressable (~> 2.3.8)
|
||||
after_commit_queue (~> 1.3.0)
|
||||
akismet (~> 2.0)
|
||||
|
|
@ -938,7 +938,7 @@ DEPENDENCIES
|
|||
rqrcode-rails3 (~> 0.1.7)
|
||||
rspec-rails (~> 3.5.0)
|
||||
rspec-retry (~> 0.4.5)
|
||||
rubocop (~> 0.42.0)
|
||||
rubocop (~> 0.43.0)
|
||||
rubocop-rspec (~> 1.5.0)
|
||||
ruby-fogbugz (~> 0.2.1)
|
||||
ruby-prof (~> 0.16.2)
|
||||
|
|
@ -948,7 +948,7 @@ DEPENDENCIES
|
|||
sdoc (~> 0.3.20)
|
||||
seed-fu (~> 2.3.5)
|
||||
select2-rails (~> 3.5.9)
|
||||
sentry-raven (~> 1.1.0)
|
||||
sentry-raven (~> 2.0.0)
|
||||
settingslogic (~> 2.0.9)
|
||||
sham_rack (~> 1.3.6)
|
||||
shoulda-matchers (~> 2.8.0)
|
||||
|
|
@ -986,4 +986,4 @@ DEPENDENCIES
|
|||
wikicloth (= 0.8.1)
|
||||
|
||||
BUNDLED WITH
|
||||
1.13.1
|
||||
1.13.2
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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 = {}));
|
||||
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
namespacesPath: "/api/:version/namespaces.json",
|
||||
groupProjectsPath: "/api/:version/groups/:id/projects.json",
|
||||
projectsPath: "/api/:version/projects.json?simple=true",
|
||||
labelsPath: "/api/:version/projects/:id/labels",
|
||||
labelsPath: "/:namespace_path/:project_path/labels",
|
||||
licensePath: "/api/:version/licenses/:key",
|
||||
gitignorePath: "/api/:version/gitignores/:key",
|
||||
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
|
||||
|
|
@ -23,12 +23,13 @@
|
|||
},
|
||||
// Return groups list. Filtered by query
|
||||
// Only active groups retrieved
|
||||
groups: function(query, skip_ldap, callback) {
|
||||
groups: function(query, skip_ldap, skip_groups, callback) {
|
||||
var url = Api.buildUrl(Api.groupsPath);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
search: query,
|
||||
skip_groups: skip_groups,
|
||||
per_page: 20
|
||||
},
|
||||
dataType: "json"
|
||||
|
|
@ -65,13 +66,14 @@
|
|||
return callback(projects);
|
||||
});
|
||||
},
|
||||
newLabel: function(project_id, data, callback) {
|
||||
newLabel: function(namespace_path, project_path, data, callback) {
|
||||
var url = Api.buildUrl(Api.labelsPath)
|
||||
.replace(':id', project_id);
|
||||
.replace(':namespace_path', namespace_path)
|
||||
.replace(':project_path', project_path);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
data: data,
|
||||
data: {'label': data},
|
||||
dataType: "json"
|
||||
}).done(function(label) {
|
||||
return callback(label);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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 = {}));
|
||||
|
|
@ -18,6 +18,6 @@
|
|||
|
||||
return BlobGitignoreSelector;
|
||||
|
||||
})(TemplateSelector);
|
||||
})(gl.TemplateSelector);
|
||||
|
||||
}).call(this);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@
|
|||
|
||||
return BlobLicenseSelector;
|
||||
|
||||
})(TemplateSelector);
|
||||
})(gl.TemplateSelector);
|
||||
|
||||
}).call(this);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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 = {}));
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 = {}));
|
||||
|
||||
|
|
@ -23,13 +23,13 @@
|
|||
})(this));
|
||||
this.initModePanesAndLinks();
|
||||
this.initSoftWrap();
|
||||
new BlobLicenseSelectors({
|
||||
new gl.BlobLicenseSelectors({
|
||||
editor: this.editor
|
||||
});
|
||||
new BlobGitignoreSelectors({
|
||||
editor: this.editor
|
||||
});
|
||||
new BlobCiYamlSelectors({
|
||||
new gl.BlobCiYamlSelectors({
|
||||
editor: this.editor
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ $(() => {
|
|||
|
||||
$('.js-new-board-list').each(function () {
|
||||
const $this = $(this);
|
||||
|
||||
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
|
||||
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
|
||||
|
||||
$this.glDropdown({
|
||||
data(term, callback) {
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@
|
|||
$date = $('.js-artifacts-remove');
|
||||
if ($date.length) {
|
||||
date = $date.text();
|
||||
return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' '));
|
||||
return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
(function (w) {
|
||||
class CreateLabelDropdown {
|
||||
constructor ($el, projectId) {
|
||||
constructor ($el, namespacePath, projectPath) {
|
||||
this.$el = $el;
|
||||
this.projectId = projectId;
|
||||
this.namespacePath = namespacePath;
|
||||
this.projectPath = projectPath;
|
||||
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
|
||||
this.$cancelButton = $('.js-cancel-label-btn', this.$el);
|
||||
this.$newLabelField = $('#new_label_name', this.$el);
|
||||
|
|
@ -91,8 +92,8 @@
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
Api.newLabel(this.projectId, {
|
||||
name: this.$newLabelField.val(),
|
||||
Api.newLabel(this.namespacePath, this.projectPath, {
|
||||
title: this.$newLabelField.val(),
|
||||
color: this.$newColorField.val()
|
||||
}, (label) => {
|
||||
this.$newLabelCreateButton.enable();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
function Diff() {
|
||||
$('.files .diff-file').singleFileDiff();
|
||||
this.filesCommentButton = $('.files .diff-file').filesCommentButton();
|
||||
if (this.diffViewType() === 'parallel') {
|
||||
$('.content-wrapper .container-fluid').removeClass('container-limited');
|
||||
}
|
||||
$(document).off('click', '.js-unfold');
|
||||
$(document).on('click', '.js-unfold', (function(_this) {
|
||||
return function(event) {
|
||||
|
|
@ -52,6 +55,10 @@
|
|||
})(this));
|
||||
}
|
||||
|
||||
Diff.prototype.diffViewType = function() {
|
||||
return $('.inline-parallel-buttons a.active').data('view-type');
|
||||
}
|
||||
|
||||
Diff.prototype.lineNumbers = function(line) {
|
||||
if (!line.children().length) {
|
||||
return [0, 0];
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
case 'projects:merge_requests:index':
|
||||
case 'projects:issues:index':
|
||||
Issuable.init();
|
||||
new IssuableBulkActions();
|
||||
new gl.IssuableBulkActions();
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
break;
|
||||
case 'projects:issues:show':
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
new Milestone();
|
||||
break;
|
||||
case 'dashboard:todos:index':
|
||||
new Todos();
|
||||
new gl.Todos();
|
||||
break;
|
||||
case 'projects:milestones:new':
|
||||
case 'projects:milestones:edit':
|
||||
|
|
@ -59,7 +59,9 @@
|
|||
shortcut_handler = new ShortcutsNavigation();
|
||||
new GLForm($('.issue-form'));
|
||||
new IssuableForm($('.issue-form'));
|
||||
new IssuableTemplateSelectors();
|
||||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
new gl.IssuableTemplateSelectors();
|
||||
break;
|
||||
case 'projects:merge_requests:new':
|
||||
case 'projects:merge_requests:edit':
|
||||
|
|
@ -67,7 +69,9 @@
|
|||
shortcut_handler = new ShortcutsNavigation();
|
||||
new GLForm($('.merge-request-form'));
|
||||
new IssuableForm($('.merge-request-form'));
|
||||
new IssuableTemplateSelectors();
|
||||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
new gl.IssuableTemplateSelectors();
|
||||
break;
|
||||
case 'projects:tags:new':
|
||||
new ZenMode();
|
||||
|
|
@ -165,7 +169,7 @@
|
|||
break;
|
||||
case 'projects:labels:index':
|
||||
if ($('.prioritized-labels').length) {
|
||||
new LabelManager();
|
||||
new gl.LabelManager();
|
||||
}
|
||||
break;
|
||||
case 'projects:network:show':
|
||||
|
|
@ -279,7 +283,7 @@
|
|||
Dispatcher.prototype.initSearch = function() {
|
||||
// Only when search form is present
|
||||
if ($('.search').length) {
|
||||
return new SearchAutocomplete();
|
||||
return new gl.SearchAutocomplete();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -443,6 +443,7 @@
|
|||
var contentHtml;
|
||||
this.resetRows();
|
||||
this.addArrowKeyEvent();
|
||||
|
||||
if (this.options.setIndeterminateIds) {
|
||||
this.options.setIndeterminateIds.call(this);
|
||||
}
|
||||
|
|
@ -460,9 +461,21 @@
|
|||
if (this.options.filterable) {
|
||||
this.filterInput.focus();
|
||||
}
|
||||
|
||||
if (this.options.showMenuAbove) {
|
||||
this.positionMenuAbove();
|
||||
}
|
||||
|
||||
return this.dropdown.trigger('shown.gl.dropdown');
|
||||
};
|
||||
|
||||
GitLabDropdown.prototype.positionMenuAbove = function() {
|
||||
var $button = $(this.el);
|
||||
var $menu = this.dropdown.find('.dropdown-menu');
|
||||
|
||||
$menu.css('top', ($button.height() + $menu.height()) * -1);
|
||||
};
|
||||
|
||||
GitLabDropdown.prototype.hidden = function(e) {
|
||||
var $input;
|
||||
this.resetRows();
|
||||
|
|
|
|||
|
|
@ -5,14 +5,15 @@
|
|||
function GroupsSelect() {
|
||||
$('.ajax-groups-select').each((function(_this) {
|
||||
return function(i, select) {
|
||||
var skip_ldap;
|
||||
var skip_ldap, skip_groups;
|
||||
skip_ldap = $(select).hasClass('skip_ldap');
|
||||
skip_groups = $(select).data('skip-groups') || [];
|
||||
return $(select).select2({
|
||||
placeholder: "Search for a group",
|
||||
multiple: $(select).hasClass('multiselect'),
|
||||
minimumInputLength: 0,
|
||||
query: function(query) {
|
||||
return Api.groups(query.term, skip_ldap, function(groups) {
|
||||
return Api.groups(query.term, skip_ldap, skip_groups, function(groups) {
|
||||
var data;
|
||||
data = {
|
||||
results: groups
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@
|
|||
}).remove();
|
||||
// Submit the form to get new data
|
||||
Issuable.filterResults($('.filter-form'));
|
||||
return $('.js-label-select').trigger('update.label');
|
||||
});
|
||||
},
|
||||
filterResults: (function(_this) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
(function() {
|
||||
this.IssuableBulkActions = (function() {
|
||||
function IssuableBulkActions(opts) {
|
||||
// Set defaults
|
||||
var ref, ref1, ref2;
|
||||
if (opts == null) {
|
||||
opts = {};
|
||||
}
|
||||
this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li');
|
||||
// Save instance
|
||||
((global) => {
|
||||
|
||||
class IssuableBulkActions {
|
||||
constructor({ container, form, issues } = {}) {
|
||||
this.container = container || $('.content'),
|
||||
this.form = form || this.getElement('.bulk-update');
|
||||
this.issues = issues || this.getElement('.issues-list .issue');
|
||||
this.form.data('bulkActions', this);
|
||||
this.willUpdateLabels = false;
|
||||
this.bindEvents();
|
||||
|
|
@ -15,53 +12,46 @@
|
|||
Issuable.initChecks();
|
||||
}
|
||||
|
||||
IssuableBulkActions.prototype.getElement = function(selector) {
|
||||
getElement(selector) {
|
||||
return this.container.find(selector);
|
||||
};
|
||||
}
|
||||
|
||||
IssuableBulkActions.prototype.bindEvents = function() {
|
||||
bindEvents() {
|
||||
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
|
||||
};
|
||||
}
|
||||
|
||||
IssuableBulkActions.prototype.onFormSubmit = function(e) {
|
||||
onFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
return this.submit();
|
||||
};
|
||||
}
|
||||
|
||||
IssuableBulkActions.prototype.submit = function() {
|
||||
var _this, xhr;
|
||||
_this = this;
|
||||
xhr = $.ajax({
|
||||
submit() {
|
||||
const _this = this;
|
||||
const xhr = $.ajax({
|
||||
url: this.form.attr('action'),
|
||||
method: this.form.attr('method'),
|
||||
dataType: 'JSON',
|
||||
data: this.getFormDataAsObject()
|
||||
});
|
||||
xhr.done(function(response, status, xhr) {
|
||||
return location.reload();
|
||||
});
|
||||
xhr.fail(function() {
|
||||
return new Flash("Issue update failed");
|
||||
});
|
||||
xhr.done(() => window.location.reload());
|
||||
xhr.fail(() => new Flash("Issue update failed"));
|
||||
return xhr.always(this.onFormSubmitAlways.bind(this));
|
||||
};
|
||||
}
|
||||
|
||||
IssuableBulkActions.prototype.onFormSubmitAlways = function() {
|
||||
onFormSubmitAlways() {
|
||||
return this.form.find('[type="submit"]').enable();
|
||||
};
|
||||
}
|
||||
|
||||
IssuableBulkActions.prototype.getSelectedIssues = function() {
|
||||
getSelectedIssues() {
|
||||
return this.issues.has('.selected_issue:checked');
|
||||
};
|
||||
}
|
||||
|
||||
IssuableBulkActions.prototype.getLabelsFromSelection = function() {
|
||||
var labels;
|
||||
labels = [];
|
||||
getLabelsFromSelection() {
|
||||
const labels = [];
|
||||
this.getSelectedIssues().map(function() {
|
||||
var _labels;
|
||||
_labels = $(this).data('labels');
|
||||
if (_labels) {
|
||||
return _labels.map(function(labelId) {
|
||||
const labelsData = $(this).data('labels');
|
||||
if (labelsData) {
|
||||
return labelsData.map(function(labelId) {
|
||||
if (labels.indexOf(labelId) === -1) {
|
||||
return labels.push(labelId);
|
||||
}
|
||||
|
|
@ -69,7 +59,7 @@
|
|||
}
|
||||
});
|
||||
return labels;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -77,25 +67,21 @@
|
|||
* @return {Array} Label IDs
|
||||
*/
|
||||
|
||||
IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() {
|
||||
var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result;
|
||||
result = [];
|
||||
labelsToKeep = [];
|
||||
ref = this.getElement('.labels-filter .is-indeterminate');
|
||||
for (i = 0, len = ref.length; i < len; i++) {
|
||||
el = ref[i];
|
||||
labelsToKeep.push($(el).data('labelId'));
|
||||
}
|
||||
ref1 = this.getLabelsFromSelection();
|
||||
for (j = 0, len1 = ref1.length; j < len1; j++) {
|
||||
id = ref1[j];
|
||||
// Only the ones that we are not going to keep
|
||||
getUnmarkedIndeterminedLabels() {
|
||||
const result = [];
|
||||
const labelsToKeep = [];
|
||||
|
||||
this.getElement('.labels-filter .is-indeterminate')
|
||||
.each((i, el) => labelsToKeep.push($(el).data('labelId')));
|
||||
|
||||
this.getLabelsFromSelection().forEach((id) => {
|
||||
if (labelsToKeep.indexOf(id) === -1) {
|
||||
result.push(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -103,9 +89,8 @@
|
|||
* Returns key/value pairs from form data
|
||||
*/
|
||||
|
||||
IssuableBulkActions.prototype.getFormDataAsObject = function() {
|
||||
var formData;
|
||||
formData = {
|
||||
getFormDataAsObject() {
|
||||
const formData = {
|
||||
update: {
|
||||
state_event: this.form.find('input[name="update[state_event]"]').val(),
|
||||
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
|
||||
|
|
@ -125,19 +110,18 @@
|
|||
});
|
||||
}
|
||||
return formData;
|
||||
};
|
||||
}
|
||||
|
||||
IssuableBulkActions.prototype.getLabelsToApply = function() {
|
||||
var $labels, labelIds;
|
||||
labelIds = [];
|
||||
$labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
|
||||
getLabelsToApply() {
|
||||
const labelIds = [];
|
||||
const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
|
||||
$labels.each(function(k, label) {
|
||||
if (label) {
|
||||
return labelIds.push(parseInt($(label).val()));
|
||||
}
|
||||
});
|
||||
return labelIds;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -145,11 +129,10 @@
|
|||
* @return {Array} Array of labels IDs
|
||||
*/
|
||||
|
||||
IssuableBulkActions.prototype.getLabelsToRemove = function() {
|
||||
var indeterminatedLabels, labelsToApply, result;
|
||||
result = [];
|
||||
indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
|
||||
labelsToApply = this.getLabelsToApply();
|
||||
getLabelsToRemove() {
|
||||
const result = [];
|
||||
const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
|
||||
const labelsToApply = this.getLabelsToApply();
|
||||
indeterminatedLabels.map(function(id) {
|
||||
// We need to exclude label IDs that will be applied
|
||||
// By not doing this will cause issues from selection to not add labels at all
|
||||
|
|
@ -158,10 +141,9 @@
|
|||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return IssuableBulkActions;
|
||||
global.IssuableBulkActions = IssuableBulkActions;
|
||||
|
||||
})();
|
||||
|
||||
}).call(this);
|
||||
})(window.gl || (window.gl = {}));
|
||||
|
|
@ -4,9 +4,11 @@
|
|||
var _this;
|
||||
_this = this;
|
||||
$('.js-label-select').each(function(i, dropdown) {
|
||||
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected;
|
||||
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
|
||||
$dropdown = $(dropdown);
|
||||
projectId = $dropdown.data('project-id');
|
||||
$toggleText = $dropdown.find('.dropdown-toggle-text');
|
||||
namespacePath = $dropdown.data('namespace-path');
|
||||
projectPath = $dropdown.data('project-path');
|
||||
labelUrl = $dropdown.data('labels');
|
||||
issueUpdateURL = $dropdown.data('issueUpdate');
|
||||
selectedLabel = $dropdown.data('selected');
|
||||
|
|
@ -15,6 +17,7 @@
|
|||
}
|
||||
showNo = $dropdown.data('show-no');
|
||||
showAny = $dropdown.data('show-any');
|
||||
showMenuAbove = $dropdown.data('showMenuAbove');
|
||||
defaultLabel = $dropdown.data('default-label');
|
||||
abilityName = $dropdown.data('ability-name');
|
||||
$selectbox = $dropdown.closest('.selectbox');
|
||||
|
|
@ -24,6 +27,9 @@
|
|||
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
|
||||
$value = $block.find('.value');
|
||||
$loading = $block.find('.block-loading').fadeOut();
|
||||
fieldName = $dropdown.data('field-name');
|
||||
useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
|
||||
propertyName = useId ? 'id' : 'title';
|
||||
initialSelected = $selectbox
|
||||
.find('input[name="' + $dropdown.data('field-name') + '"]')
|
||||
.map(function () {
|
||||
|
|
@ -40,12 +46,12 @@
|
|||
$sidebarLabelTooltip.tooltip();
|
||||
|
||||
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
|
||||
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
|
||||
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
|
||||
}
|
||||
|
||||
saveLabelData = function() {
|
||||
var data, selected;
|
||||
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
|
||||
selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
|
||||
return this.value;
|
||||
}).get();
|
||||
|
||||
|
|
@ -75,7 +81,8 @@
|
|||
if (data.labels.length) {
|
||||
template = labelHTMLTemplate(data);
|
||||
labelCount = data.labels.length;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
template = labelNoneHTMLTemplate;
|
||||
}
|
||||
$value.removeAttr('style').html(template);
|
||||
|
|
@ -92,7 +99,8 @@
|
|||
}
|
||||
|
||||
labelTooltipTitle = labelTitles.join(', ');
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
labelTooltipTitle = '';
|
||||
$sidebarLabelTooltip.tooltip('destroy');
|
||||
}
|
||||
|
|
@ -114,6 +122,7 @@
|
|||
});
|
||||
};
|
||||
return $dropdown.glDropdown({
|
||||
showMenuAbove: showMenuAbove,
|
||||
data: function(term, callback) {
|
||||
return $.ajax({
|
||||
url: labelUrl
|
||||
|
|
@ -133,23 +142,29 @@
|
|||
};
|
||||
}).value();
|
||||
if ($dropdown.hasClass('js-extra-options')) {
|
||||
var extraData = [];
|
||||
if (showNo) {
|
||||
data.unshift({
|
||||
extraData.unshift({
|
||||
id: 0,
|
||||
title: 'No Label'
|
||||
});
|
||||
}
|
||||
if (showAny) {
|
||||
data.unshift({
|
||||
extraData.unshift({
|
||||
isAny: true,
|
||||
title: 'Any Label'
|
||||
});
|
||||
}
|
||||
if (data.length > 2) {
|
||||
data.splice(2, 0, 'divider');
|
||||
if (extraData.length) {
|
||||
extraData.push('divider');
|
||||
data = extraData.concat(data);
|
||||
}
|
||||
}
|
||||
return callback(data);
|
||||
|
||||
callback(data);
|
||||
if (showMenuAbove) {
|
||||
$dropdown.data('glDropdown').positionMenuAbove();
|
||||
}
|
||||
});
|
||||
},
|
||||
renderRow: function(label, instance) {
|
||||
|
|
@ -157,7 +172,7 @@
|
|||
$li = $('<li>');
|
||||
$a = $('<a href="#">');
|
||||
selectedClass = [];
|
||||
removesAll = label.id === 0 || (label.id == null);
|
||||
removesAll = label.id <= 0 || (label.id == null);
|
||||
if ($dropdown.hasClass('js-filter-bulk-update')) {
|
||||
indeterminate = instance.indeterminateIds;
|
||||
active = instance.activeIds;
|
||||
|
|
@ -194,14 +209,16 @@
|
|||
return color + " " + percentFirst + "%," + color + " " + percentSecond + "% ";
|
||||
}).join(',');
|
||||
color = "linear-gradient(" + color + ")";
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if (label.color != null) {
|
||||
color = label.color[0];
|
||||
}
|
||||
}
|
||||
if (color) {
|
||||
colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
colorEl = '';
|
||||
}
|
||||
// We need to identify which items are actually labels
|
||||
|
|
@ -219,30 +236,46 @@
|
|||
},
|
||||
selectable: true,
|
||||
filterable: true,
|
||||
selected: $dropdown.data('selected') || [],
|
||||
toggleLabel: function(selected, el) {
|
||||
var selected_labels;
|
||||
selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active');
|
||||
if (selected && (selected.title != null)) {
|
||||
if (selected_labels.length > 1) {
|
||||
return selected.title + " +" + (selected_labels.length - 1) + " more";
|
||||
} else {
|
||||
return selected.title;
|
||||
}
|
||||
} else if (!selected && selected_labels.length !== 0) {
|
||||
if (selected_labels.length > 1) {
|
||||
return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more";
|
||||
} else if (selected_labels.length === 1) {
|
||||
return $(selected_labels).text();
|
||||
}
|
||||
} else {
|
||||
var isSelected = el !== null ? el.hasClass('is-active') : false;
|
||||
var title = selected.title;
|
||||
var selectedLabels = this.selected;
|
||||
|
||||
if (selected.id === 0) {
|
||||
this.selected = [];
|
||||
return 'No Label';
|
||||
}
|
||||
else if (isSelected) {
|
||||
this.selected.push(title);
|
||||
}
|
||||
else {
|
||||
var index = this.selected.indexOf(title);
|
||||
this.selected.splice(index, 1);
|
||||
}
|
||||
|
||||
if (selectedLabels.length === 1) {
|
||||
return selectedLabels;
|
||||
}
|
||||
else if (selectedLabels.length) {
|
||||
return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
|
||||
}
|
||||
else {
|
||||
return defaultLabel;
|
||||
}
|
||||
},
|
||||
fieldName: $dropdown.data('field-name'),
|
||||
id: function(label) {
|
||||
if (label.id <= 0) return;
|
||||
|
||||
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
|
||||
return label.id;
|
||||
}
|
||||
|
||||
if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
|
||||
return label.title;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return label.id;
|
||||
}
|
||||
},
|
||||
|
|
@ -254,6 +287,11 @@
|
|||
$selectbox.hide();
|
||||
// display:block overrides the hide-collapse rule
|
||||
$value.removeAttr('style');
|
||||
|
||||
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (page === 'projects:boards:show') {
|
||||
return;
|
||||
}
|
||||
|
|
@ -261,9 +299,11 @@
|
|||
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
|
||||
selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
|
||||
Issuable.filterResults($dropdown.closest('form'));
|
||||
} else if ($dropdown.hasClass('js-filter-submit')) {
|
||||
}
|
||||
else if ($dropdown.hasClass('js-filter-submit')) {
|
||||
$dropdown.closest('form').submit();
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if (!$dropdown.hasClass('js-filter-bulk-update')) {
|
||||
saveLabelData();
|
||||
}
|
||||
|
|
@ -280,18 +320,28 @@
|
|||
clicked: function(label, $el, e) {
|
||||
var isIssueIndex, isMRIndex, page;
|
||||
_this.enableBulkLabelDropdown();
|
||||
if ($dropdown.hasClass('js-filter-bulk-update')) {
|
||||
|
||||
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
|
||||
$dropdown.parent()
|
||||
.find('.dropdown-clear-active')
|
||||
.removeClass('is-active')
|
||||
}
|
||||
|
||||
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
|
||||
return;
|
||||
}
|
||||
|
||||
page = $('body').data('page');
|
||||
isIssueIndex = page === 'projects:issues:index';
|
||||
isMRIndex = page === 'projects:merge_requests:index';
|
||||
if (page === 'projects:boards:show') {
|
||||
if (label.isAny) {
|
||||
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
|
||||
} else if ($el.hasClass('is-active')) {
|
||||
}
|
||||
else if ($el.hasClass('is-active')) {
|
||||
gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
|
||||
filters = filters.filter(function (filteredLabel) {
|
||||
return filteredLabel !== label.title;
|
||||
|
|
@ -302,17 +352,21 @@
|
|||
gl.issueBoards.BoardsStore.updateFiltersUrl();
|
||||
e.preventDefault();
|
||||
return;
|
||||
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
|
||||
}
|
||||
else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
|
||||
if (!$dropdown.hasClass('js-multiselect')) {
|
||||
selectedLabel = label.title;
|
||||
return Issuable.filterResults($dropdown.closest('form'));
|
||||
}
|
||||
} else if ($dropdown.hasClass('js-filter-submit')) {
|
||||
}
|
||||
else if ($dropdown.hasClass('js-filter-submit')) {
|
||||
return $dropdown.closest('form').submit();
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if ($dropdown.hasClass('js-multiselect')) {
|
||||
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return saveLabelData();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@
|
|||
gl.utils.getPagePath = function() {
|
||||
return $('body').data('page').split(':')[0];
|
||||
};
|
||||
gl.utils.parseUrl = function (url) {
|
||||
var parser = document.createElement('a');
|
||||
parser.href = url;
|
||||
return parser;
|
||||
};
|
||||
return jQuery.timefor = function(time, suffix, expiredLabel) {
|
||||
var suffixFromNow, timefor;
|
||||
if (!time) {
|
||||
|
|
|
|||
|
|
@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs';
|
|||
class MergeConflictDataProvider {
|
||||
|
||||
getInitialData() {
|
||||
// TODO: remove reliance on jQuery and DOM state introspection
|
||||
const diffViewType = $.cookie('diff_view');
|
||||
const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
|
||||
|
||||
return {
|
||||
isLoading : true,
|
||||
hasError : false,
|
||||
isParallel : diffViewType === 'parallel',
|
||||
diffViewType : diffViewType,
|
||||
fixedLayout : fixedLayout,
|
||||
isSubmitting : false,
|
||||
conflictsData : {},
|
||||
resolutionData : {}
|
||||
|
|
@ -192,14 +195,17 @@ class MergeConflictDataProvider {
|
|||
updateViewType(newType) {
|
||||
const vi = this.vueInstance;
|
||||
|
||||
if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
|
||||
if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
|
||||
return;
|
||||
}
|
||||
|
||||
vi.diffView = newType;
|
||||
vi.isParallel = newType === 'parallel';
|
||||
$.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
|
||||
$('.content-wrapper .container-fluid').toggleClass('container-limited');
|
||||
vi.diffViewType = newType;
|
||||
vi.isParallel = newType === 'parallel';
|
||||
$.cookie('diff_view', newType, {
|
||||
path: (gon && gon.relative_url_root) || '/'
|
||||
});
|
||||
$('.content-wrapper .container-fluid')
|
||||
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -60,9 +60,8 @@ class MergeConflictResolver {
|
|||
$('#conflicts .js-syntax-highlight').syntaxHighlight();
|
||||
});
|
||||
|
||||
if (this.vue.diffViewType === 'parallel') {
|
||||
$('.content-wrapper .container-fluid').removeClass('container-limited');
|
||||
}
|
||||
$('.content-wrapper .container-fluid')
|
||||
.toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,13 +36,10 @@
|
|||
};
|
||||
|
||||
MergeRequest.prototype.initTabs = function() {
|
||||
if (this.opts.action !== 'new') {
|
||||
// `MergeRequests#new` has no tab-persisting or lazy-loading behavior
|
||||
window.mrTabs = new MergeRequestTabs(this.opts);
|
||||
} else {
|
||||
// Show the first tab (Commits)
|
||||
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
|
||||
if (window.mrTabs) {
|
||||
window.mrTabs.unbindEvents();
|
||||
}
|
||||
window.mrTabs = new MergeRequestTabs(this.opts);
|
||||
};
|
||||
|
||||
MergeRequest.prototype.showAllCommits = function() {
|
||||
|
|
|
|||
|
|
@ -56,9 +56,14 @@
|
|||
|
||||
MergeRequestTabs.prototype.commitsLoaded = false;
|
||||
|
||||
MergeRequestTabs.prototype.fixedLayoutPref = null;
|
||||
|
||||
function MergeRequestTabs(opts) {
|
||||
this.opts = opts != null ? opts : {};
|
||||
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
|
||||
|
||||
this.buildsLoaded = this.opts.buildsLoaded || false;
|
||||
|
||||
this.setCurrentAction = bind(this.setCurrentAction, this);
|
||||
this.tabShown = bind(this.tabShown, this);
|
||||
this.showTab = bind(this.showTab, this);
|
||||
|
|
@ -70,7 +75,12 @@
|
|||
|
||||
MergeRequestTabs.prototype.bindEvents = function() {
|
||||
$(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
|
||||
return $(document).on('click', '.js-show-tab', this.showTab);
|
||||
$(document).on('click', '.js-show-tab', this.showTab);
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.unbindEvents = function() {
|
||||
$(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
|
||||
$(document).off('click', '.js-show-tab', this.showTab);
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.showTab = function(event) {
|
||||
|
|
@ -85,11 +95,15 @@
|
|||
if (action === 'commits') {
|
||||
this.loadCommits($target.attr('href'));
|
||||
this.expandView();
|
||||
} else if (action === 'diffs') {
|
||||
this.resetViewContainer();
|
||||
} else if (this.isDiffAction(action)) {
|
||||
this.loadDiff($target.attr('href'));
|
||||
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
|
||||
this.shrinkView();
|
||||
}
|
||||
if (this.diffViewType() === 'parallel') {
|
||||
this.expandViewContainer();
|
||||
}
|
||||
navBarHeight = $('.navbar-gitlab').outerHeight();
|
||||
$.scrollTo(".merge-request-details .merge-request-tabs", {
|
||||
offset: -navBarHeight
|
||||
|
|
@ -97,11 +111,14 @@
|
|||
} else if (action === 'builds') {
|
||||
this.loadBuilds($target.attr('href'));
|
||||
this.expandView();
|
||||
this.resetViewContainer();
|
||||
} else if (action === 'pipelines') {
|
||||
this.loadPipelines($target.attr('href'));
|
||||
this.expandView();
|
||||
this.resetViewContainer();
|
||||
} else {
|
||||
this.expandView();
|
||||
this.resetViewContainer();
|
||||
}
|
||||
if (this.opts.setUrl) {
|
||||
this.setCurrentAction(action);
|
||||
|
|
@ -126,7 +143,7 @@
|
|||
if (action === 'show') {
|
||||
action = 'notes';
|
||||
}
|
||||
return $(".merge-request-tabs a[data-action='" + action + "']").tab('show');
|
||||
$(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
|
||||
};
|
||||
|
||||
// Replaces the current Merge Request-specific action in the URL with a new one
|
||||
|
|
@ -156,8 +173,9 @@
|
|||
action = 'notes';
|
||||
}
|
||||
this.currentAction = action;
|
||||
// Remove a trailing '/commits' or '/diffs'
|
||||
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
|
||||
// Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
|
||||
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
|
||||
|
||||
// Append the new action if we're on a tab other than 'notes'
|
||||
if (action !== 'notes') {
|
||||
new_state += "/" + action;
|
||||
|
|
@ -196,8 +214,13 @@
|
|||
if (this.diffsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We extract pathname for the current Changes tab anchor href
|
||||
// some pages like MergeRequestsController#new has query parameters on that anchor
|
||||
var url = gl.utils.parseUrl(source);
|
||||
|
||||
return this._get({
|
||||
url: (source + ".json") + this._location.search,
|
||||
url: (url.pathname + ".json") + this._location.search,
|
||||
success: (function(_this) {
|
||||
return function(data) {
|
||||
$('#diffs').html(data.html);
|
||||
|
|
@ -209,7 +232,7 @@
|
|||
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
|
||||
$('#diffs .js-syntax-highlight').syntaxHighlight();
|
||||
$('#diffs .diff-file').singleFileDiff();
|
||||
if (_this.diffViewType() === 'parallel') {
|
||||
if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
|
||||
_this.expandViewContainer();
|
||||
}
|
||||
_this.diffsLoaded = true;
|
||||
|
|
@ -308,11 +331,25 @@
|
|||
|
||||
MergeRequestTabs.prototype.diffViewType = function() {
|
||||
return $('.inline-parallel-buttons a.active').data('view-type');
|
||||
// Returns diff view type
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.isDiffAction = function(action) {
|
||||
return action === 'diffs' || action === 'new/diffs'
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.expandViewContainer = function() {
|
||||
return $('.container-fluid').removeClass('container-limited');
|
||||
var $wrapper = $('.content-wrapper .container-fluid');
|
||||
if (this.fixedLayoutPref === null) {
|
||||
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
|
||||
}
|
||||
$wrapper.removeClass('container-limited');
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.resetViewContainer = function() {
|
||||
if (this.fixedLayoutPref !== null) {
|
||||
$('.content-wrapper .container-fluid')
|
||||
.toggleClass('container-limited', this.fixedLayoutPref);
|
||||
}
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.shrinkView = function() {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
this.currentProject = JSON.parse(currentProject);
|
||||
}
|
||||
$('.js-milestone-select').each(function(i, dropdown) {
|
||||
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId;
|
||||
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
|
||||
$dropdown = $(dropdown);
|
||||
projectId = $dropdown.data('project-id');
|
||||
milestonesUrl = $dropdown.data('milestones');
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
selectedMilestone = $dropdown.data('selected');
|
||||
showNo = $dropdown.data('show-no');
|
||||
showAny = $dropdown.data('show-any');
|
||||
showMenuAbove = $dropdown.data('showMenuAbove');
|
||||
showUpcoming = $dropdown.data('show-upcoming');
|
||||
useId = $dropdown.data('use-id');
|
||||
defaultLabel = $dropdown.data('default-label');
|
||||
|
|
@ -31,12 +32,12 @@
|
|||
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
|
||||
}
|
||||
return $dropdown.glDropdown({
|
||||
showMenuAbove: showMenuAbove,
|
||||
data: function(term, callback) {
|
||||
return $.ajax({
|
||||
url: milestonesUrl
|
||||
}).done(function(data) {
|
||||
var extraOptions;
|
||||
extraOptions = [];
|
||||
var extraOptions = [];
|
||||
if (showAny) {
|
||||
extraOptions.push({
|
||||
id: 0,
|
||||
|
|
@ -58,10 +59,14 @@
|
|||
title: 'Upcoming'
|
||||
});
|
||||
}
|
||||
if (extraOptions.length > 2) {
|
||||
if (extraOptions.length) {
|
||||
extraOptions.push('divider');
|
||||
}
|
||||
return callback(extraOptions.concat(data));
|
||||
|
||||
callback(extraOptions.concat(data));
|
||||
if (showMenuAbove) {
|
||||
$dropdown.data('glDropdown').positionMenuAbove();
|
||||
}
|
||||
});
|
||||
},
|
||||
filterable: true,
|
||||
|
|
@ -69,19 +74,20 @@
|
|||
fields: ['title']
|
||||
},
|
||||
selectable: true,
|
||||
toggleLabel: function(selected) {
|
||||
if (selected && 'id' in selected) {
|
||||
toggleLabel: function(selected, el, e) {
|
||||
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
|
||||
return selected.title;
|
||||
} else {
|
||||
return defaultLabel;
|
||||
}
|
||||
},
|
||||
defaultLabel: defaultLabel,
|
||||
fieldName: $dropdown.data('field-name'),
|
||||
text: function(milestone) {
|
||||
return _.escape(milestone.title);
|
||||
},
|
||||
id: function(milestone) {
|
||||
if (!useId) {
|
||||
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
|
||||
return milestone.name;
|
||||
} else {
|
||||
return milestone.id;
|
||||
|
|
@ -100,7 +106,8 @@
|
|||
page = $('body').data('page');
|
||||
isIssueIndex = page === 'projects:issues:index';
|
||||
isMRIndex = (page === page && page === 'projects:merge_requests:index');
|
||||
if ($dropdown.hasClass('js-filter-bulk-update')) {
|
||||
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (page === 'projects:boards:show') {
|
||||
|
|
|
|||
|
|
@ -1,47 +1,45 @@
|
|||
(function() {
|
||||
var GitLabCrop,
|
||||
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
((global) => {
|
||||
|
||||
GitLabCrop = (function() {
|
||||
var FILENAMEREGEX;
|
||||
// Matches everything but the file name
|
||||
const FILENAMEREGEX = /^.*[\\\/]/;
|
||||
|
||||
// Matches everything but the file name
|
||||
FILENAMEREGEX = /^.*[\\\/]/;
|
||||
class GitLabCrop {
|
||||
constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
|
||||
exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
|
||||
|
||||
function GitLabCrop(input, opts) {
|
||||
var ref, ref1, ref2, ref3, ref4;
|
||||
if (opts == null) {
|
||||
opts = {};
|
||||
}
|
||||
this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this);
|
||||
this.onModalHide = bind(this.onModalHide, this);
|
||||
this.onModalShow = bind(this.onModalShow, this);
|
||||
this.onPickImageClick = bind(this.onPickImageClick, this);
|
||||
this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
|
||||
this.onModalHide = this.onModalHide.bind(this);
|
||||
this.onModalShow = this.onModalShow.bind(this);
|
||||
this.onPickImageClick = this.onPickImageClick.bind(this);
|
||||
this.fileInput = $(input);
|
||||
// We should rename to avoid spec to fail
|
||||
// Form will submit the proper input filed with a file using FormData
|
||||
this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger");
|
||||
// Set defaults
|
||||
this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg;
|
||||
// Required params
|
||||
// Ensure needed elements are jquery objects
|
||||
// If selector is provided we will convert them to a jQuery Object
|
||||
this.filename = this.getElement(this.filename);
|
||||
this.previewImage = this.getElement(this.previewImage);
|
||||
this.pickImageEl = this.getElement(this.pickImageEl);
|
||||
// Modal elements usually are outside the @form element
|
||||
this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop;
|
||||
this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn;
|
||||
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
|
||||
this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`);
|
||||
this.exportWidth = exportWidth;
|
||||
this.exportHeight = exportHeight;
|
||||
this.cropBoxWidth = cropBoxWidth;
|
||||
this.cropBoxHeight = cropBoxHeight;
|
||||
this.form = this.fileInput.parents('form');
|
||||
this.filename = filename;
|
||||
this.previewImage = previewImage;
|
||||
this.modalCrop = modalCrop;
|
||||
this.pickImageEl = pickImageEl;
|
||||
this.uploadImageBtn = uploadImageBtn;
|
||||
this.modalCropImg = modalCropImg;
|
||||
this.filename = this.getElement(filename);
|
||||
this.previewImage = this.getElement(previewImage);
|
||||
this.pickImageEl = this.getElement(pickImageEl);
|
||||
this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
|
||||
this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
|
||||
this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
|
||||
this.cropActionsBtn = this.modalCrop.find('[data-method]');
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.getElement = function(selector) {
|
||||
getElement(selector) {
|
||||
return $(selector, this.form);
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.bindEvents = function() {
|
||||
bindEvents() {
|
||||
var _this;
|
||||
_this = this;
|
||||
this.fileInput.on('change', function(e) {
|
||||
|
|
@ -57,13 +55,13 @@
|
|||
return _this.onActionBtnClick(btn);
|
||||
});
|
||||
return this.croppedImageBlob = null;
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.onPickImageClick = function() {
|
||||
onPickImageClick() {
|
||||
return this.fileInput.trigger('click');
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.onModalShow = function() {
|
||||
onModalShow() {
|
||||
var _this;
|
||||
_this = this;
|
||||
return this.modalCropImg.cropper({
|
||||
|
|
@ -95,44 +93,44 @@
|
|||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.onModalHide = function() {
|
||||
onModalHide() {
|
||||
return this.modalCropImg.attr('src', '').cropper('destroy');
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image
|
||||
e.preventDefault(); // Destroy cropper instance
|
||||
onUploadImageBtnClick(e) {
|
||||
e.preventDefault();
|
||||
this.setBlob();
|
||||
this.setPreview();
|
||||
this.modalCrop.modal('hide');
|
||||
return this.fileInput.val('');
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.onActionBtnClick = function(btn) {
|
||||
onActionBtnClick(btn) {
|
||||
var data, result;
|
||||
data = $(btn).data();
|
||||
if (this.modalCropImg.data('cropper') && data.method) {
|
||||
return result = this.modalCropImg.cropper(data.method, data.option);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.onFileInputChange = function(e, input) {
|
||||
onFileInputChange(e, input) {
|
||||
return this.readFile(input);
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.readFile = function(input) {
|
||||
readFile(input) {
|
||||
var _this, reader;
|
||||
_this = this;
|
||||
reader = new FileReader;
|
||||
reader.onload = function() {
|
||||
reader.onload = () => {
|
||||
_this.modalCropImg.attr('src', reader.result);
|
||||
return _this.modalCrop.modal('show');
|
||||
};
|
||||
return reader.readAsDataURL(input.files[0]);
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.dataURLtoBlob = function(dataURL) {
|
||||
dataURLtoBlob(dataURL) {
|
||||
var array, binary, i, k, len, v;
|
||||
binary = atob(dataURL.split(',')[1]);
|
||||
array = [];
|
||||
|
|
@ -143,35 +141,32 @@
|
|||
return new Blob([new Uint8Array(array)], {
|
||||
type: 'image/png'
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.setPreview = function() {
|
||||
setPreview() {
|
||||
var filename;
|
||||
this.previewImage.attr('src', this.dataURL);
|
||||
filename = this.fileInput.val().replace(FILENAMEREGEX, '');
|
||||
return this.filename.text(filename);
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.setBlob = function() {
|
||||
setBlob() {
|
||||
this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
|
||||
width: 200,
|
||||
height: 200
|
||||
}).toDataURL('image/png');
|
||||
return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
|
||||
};
|
||||
}
|
||||
|
||||
GitLabCrop.prototype.getBlob = function() {
|
||||
getBlob() {
|
||||
return this.croppedImageBlob;
|
||||
};
|
||||
|
||||
return GitLabCrop;
|
||||
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
$.fn.glCrop = function(opts) {
|
||||
return this.each(function() {
|
||||
return $(this).data('glcrop', new GitLabCrop(this, opts));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
}).call(this);
|
||||
})(window.gl || (window.gl = {}));
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 = {}));
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
data = groups.concat(projects);
|
||||
return finalCallback(data);
|
||||
};
|
||||
return Api.groups(term, false, groupsCallback);
|
||||
return Api.groups(term, false, false, groupsCallback);
|
||||
};
|
||||
} else {
|
||||
projectsCallback = finalCallback;
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
data = groups.concat(projects);
|
||||
return finalCallback(data);
|
||||
};
|
||||
return Api.groups(query.term, false, groupsCallback);
|
||||
return Api.groups(query.term, false, false, groupsCallback);
|
||||
};
|
||||
} else {
|
||||
projectsCallback = finalCallback;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
filterable: true,
|
||||
fieldName: 'group_id',
|
||||
data: function(term, callback) {
|
||||
return Api.groups(term, null, function(data) {
|
||||
return Api.groups(term, false, false, function(data) {
|
||||
data.unshift({
|
||||
name: 'Any'
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,30 +1,21 @@
|
|||
(function() {
|
||||
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
((global) => {
|
||||
|
||||
this.SearchAutocomplete = (function() {
|
||||
var KEYCODE;
|
||||
const KEYCODE = {
|
||||
ESCAPE: 27,
|
||||
BACKSPACE: 8,
|
||||
ENTER: 13,
|
||||
UP: 38,
|
||||
DOWN: 40
|
||||
};
|
||||
|
||||
KEYCODE = {
|
||||
ESCAPE: 27,
|
||||
BACKSPACE: 8,
|
||||
ENTER: 13,
|
||||
UP: 38,
|
||||
DOWN: 40
|
||||
};
|
||||
|
||||
function SearchAutocomplete(opts) {
|
||||
var ref, ref1, ref2, ref3, ref4;
|
||||
if (opts == null) {
|
||||
opts = {};
|
||||
}
|
||||
this.onSearchInputBlur = bind(this.onSearchInputBlur, this);
|
||||
this.onClearInputClick = bind(this.onClearInputClick, this);
|
||||
this.onSearchInputFocus = bind(this.onSearchInputFocus, this);
|
||||
this.onSearchInputClick = bind(this.onSearchInputClick, this);
|
||||
this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this);
|
||||
this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this);
|
||||
this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || '';
|
||||
// Dropdown Element
|
||||
class SearchAutocomplete {
|
||||
constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
|
||||
this.bindEventContext();
|
||||
this.wrap = wrap || $('.search');
|
||||
this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
|
||||
this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
|
||||
this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
|
||||
this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
|
||||
this.dropdown = this.wrap.find('.dropdown');
|
||||
this.dropdownContent = this.dropdown.find('.dropdown-content');
|
||||
this.locationBadgeEl = this.getElement('.location-badge');
|
||||
|
|
@ -46,19 +37,27 @@
|
|||
}
|
||||
|
||||
// Finds an element inside wrapper element
|
||||
SearchAutocomplete.prototype.getElement = function(selector) {
|
||||
bindEventContext() {
|
||||
this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
|
||||
this.onClearInputClick = this.onClearInputClick.bind(this);
|
||||
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
|
||||
this.onSearchInputClick = this.onSearchInputClick.bind(this);
|
||||
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
|
||||
this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
|
||||
}
|
||||
getElement(selector) {
|
||||
return this.wrap.find(selector);
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.saveOriginalState = function() {
|
||||
saveOriginalState() {
|
||||
return this.originalState = this.serializeState();
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.saveTextLength = function() {
|
||||
saveTextLength() {
|
||||
return this.lastTextLength = this.searchInput.val().length;
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.createAutocomplete = function() {
|
||||
createAutocomplete() {
|
||||
return this.searchInput.glDropdown({
|
||||
filterInputBlur: false,
|
||||
filterable: true,
|
||||
|
|
@ -73,9 +72,9 @@
|
|||
selectable: true,
|
||||
clicked: this.onClick.bind(this)
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.getData = function(term, callback) {
|
||||
getData(term, callback) {
|
||||
var _this, contents, jqXHR;
|
||||
_this = this;
|
||||
if (!term) {
|
||||
|
|
@ -138,9 +137,9 @@
|
|||
}).always(function() {
|
||||
return _this.loadingSuggestions = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.getCategoryContents = function() {
|
||||
getCategoryContents() {
|
||||
var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
|
||||
userId = gon.current_user_id;
|
||||
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
|
||||
|
|
@ -173,9 +172,9 @@
|
|||
items.splice(0, 1);
|
||||
}
|
||||
return items;
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.serializeState = function() {
|
||||
serializeState() {
|
||||
return {
|
||||
// Search Criteria
|
||||
search_project_id: this.projectInputEl.val(),
|
||||
|
|
@ -186,9 +185,9 @@
|
|||
// Location badge
|
||||
_location: this.locationBadgeEl.text()
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.bindEvents = function() {
|
||||
bindEvents() {
|
||||
this.searchInput.on('keydown', this.onSearchInputKeyDown);
|
||||
this.searchInput.on('keyup', this.onSearchInputKeyUp);
|
||||
this.searchInput.on('click', this.onSearchInputClick);
|
||||
|
|
@ -200,9 +199,9 @@
|
|||
return _this.searchInput.focus();
|
||||
};
|
||||
})(this));
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.enableAutocomplete = function() {
|
||||
enableAutocomplete() {
|
||||
var _this;
|
||||
// No need to enable anything if user is not logged in
|
||||
if (!gon.current_user_id) {
|
||||
|
|
@ -216,12 +215,12 @@
|
|||
}
|
||||
};
|
||||
|
||||
SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
|
||||
// Saves last length of the entered text
|
||||
onSearchInputKeyDown() {
|
||||
return this.saveTextLength();
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) {
|
||||
onSearchInputKeyUp(e) {
|
||||
switch (e.keyCode) {
|
||||
case KEYCODE.BACKSPACE:
|
||||
// when trying to remove the location badge
|
||||
|
|
@ -259,54 +258,53 @@
|
|||
}
|
||||
}
|
||||
this.wrap.toggleClass('has-value', !!e.target.value);
|
||||
};
|
||||
}
|
||||
|
||||
// Avoid falsy value to be returned
|
||||
SearchAutocomplete.prototype.onSearchInputClick = function(e) {
|
||||
// Prevents closing the dropdown menu
|
||||
onSearchInputClick(e) {
|
||||
return e.stopImmediatePropagation();
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.onSearchInputFocus = function() {
|
||||
onSearchInputFocus() {
|
||||
this.isFocused = true;
|
||||
this.wrap.addClass('search-active');
|
||||
if (this.getValue() === '') {
|
||||
return this.getData();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.getValue = function() {
|
||||
getValue() {
|
||||
return this.searchInput.val();
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.onClearInputClick = function(e) {
|
||||
onClearInputClick(e) {
|
||||
e.preventDefault();
|
||||
return this.searchInput.val('').focus();
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.onSearchInputBlur = function(e) {
|
||||
onSearchInputBlur(e) {
|
||||
this.isFocused = false;
|
||||
this.wrap.removeClass('search-active');
|
||||
// If input is blank then restore state
|
||||
if (this.searchInput.val() === '') {
|
||||
return this.restoreOriginalState();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.addLocationBadge = function(item) {
|
||||
addLocationBadge(item) {
|
||||
var badgeText, category, value;
|
||||
category = item.category != null ? item.category + ": " : '';
|
||||
value = item.value != null ? item.value : '';
|
||||
badgeText = "" + category + value;
|
||||
this.locationBadgeEl.text(badgeText).show();
|
||||
return this.wrap.addClass('has-location-badge');
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.hasLocationBadge = function() {
|
||||
hasLocationBadge() {
|
||||
return this.wrap.is('.has-location-badge');
|
||||
};
|
||||
|
||||
SearchAutocomplete.prototype.restoreOriginalState = function() {
|
||||
restoreOriginalState() {
|
||||
var i, input, inputs, len;
|
||||
inputs = Object.keys(this.originalState);
|
||||
for (i = 0, len = inputs.length; i < len; i++) {
|
||||
|
|
@ -320,13 +318,13 @@
|
|||
value: this.originalState._location
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.badgePresent = function() {
|
||||
badgePresent() {
|
||||
return this.locationBadgeEl.length;
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.resetSearchState = function() {
|
||||
resetSearchState() {
|
||||
var i, input, inputs, len, results;
|
||||
inputs = Object.keys(this.originalState);
|
||||
results = [];
|
||||
|
|
@ -339,30 +337,30 @@
|
|||
results.push(this.getElement("#" + input).val(''));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.removeLocationBadge = function() {
|
||||
removeLocationBadge() {
|
||||
this.locationBadgeEl.hide();
|
||||
this.resetSearchState();
|
||||
this.wrap.removeClass('has-location-badge');
|
||||
return this.disableAutocomplete();
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.disableAutocomplete = function() {
|
||||
disableAutocomplete() {
|
||||
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
|
||||
this.searchInput.addClass('disabled');
|
||||
this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
|
||||
this.restoreMenu();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
SearchAutocomplete.prototype.restoreMenu = function() {
|
||||
restoreMenu() {
|
||||
var html;
|
||||
html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
|
||||
return this.dropdownContent.html(html);
|
||||
};
|
||||
|
||||
SearchAutocomplete.prototype.onClick = function(item, $el, e) {
|
||||
onClick(item, $el, e) {
|
||||
if (location.pathname.indexOf(item.url) !== -1) {
|
||||
e.preventDefault();
|
||||
if (!this.badgePresent) {
|
||||
|
|
@ -385,9 +383,9 @@
|
|||
}
|
||||
};
|
||||
|
||||
return SearchAutocomplete;
|
||||
}
|
||||
|
||||
})();
|
||||
global.SearchAutocomplete = SearchAutocomplete;
|
||||
|
||||
$(function() {
|
||||
var $projectOptionsDataEl = $('.js-search-project-options');
|
||||
|
|
@ -408,16 +406,16 @@
|
|||
|
||||
if ($groupOptionsDataEl.length) {
|
||||
gl.groupOptions = gl.groupOptions || {};
|
||||
|
||||
|
||||
var groupPath = $groupOptionsDataEl.data('group-path');
|
||||
|
||||
|
||||
gl.groupOptions[groupPath] = {
|
||||
name: $groupOptionsDataEl.data('name'),
|
||||
issuesPath: $groupOptionsDataEl.data('issues-path'),
|
||||
mrPath: $groupOptionsDataEl.data('mr-path')
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if ($dashboardOptionsDataEl.length) {
|
||||
gl.dashboardOptions = {
|
||||
issuesPath: $dashboardOptionsDataEl.data('issues-path'),
|
||||
|
|
@ -426,4 +424,4 @@
|
|||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
})(window.gl || (window.gl = {}));
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
/*= require ../blob/template_selector */
|
||||
|
||||
((global) => {
|
||||
class IssuableTemplateSelector extends TemplateSelector {
|
||||
class IssuableTemplateSelector extends gl.TemplateSelector {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.projectPath = this.dropdown.data('project-path');
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
if (initialQuery.name) this.requestFile(initialQuery);
|
||||
|
||||
$('.reset-template', this.dropdown.parent()).on('click', () => {
|
||||
if (this.currentTemplate) this.setInputValueToTemplateContent();
|
||||
if (this.currentTemplate) this.setInputValueToTemplateContent(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -26,26 +26,28 @@
|
|||
this.currentTemplate = currentTemplate;
|
||||
if (err) return; // Error handled by global AJAX error handler
|
||||
this.stopLoadingSpinner();
|
||||
this.setInputValueToTemplateContent();
|
||||
this.setInputValueToTemplateContent(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValueToTemplateContent() {
|
||||
setInputValueToTemplateContent(append) {
|
||||
// `this.requestFileSuccess` sets the value of the description input field
|
||||
// to the content of the template selected.
|
||||
// to the content of the template selected. If `append` is true, the
|
||||
// template content will be appended to the previous value of the field,
|
||||
// separated by a blank line if the previous value is non-empty.
|
||||
if (this.titleInput.val() === '') {
|
||||
// If the title has not yet been set, focus the title input and
|
||||
// skip focusing the description input by setting `true` as the 2nd
|
||||
// argument to `requestFileSuccess`.
|
||||
this.requestFileSuccess(this.currentTemplate, true);
|
||||
// skip focusing the description input by setting `true` as the
|
||||
// `skipFocus` option to `requestFileSuccess`.
|
||||
this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
|
||||
this.titleInput.focus();
|
||||
} else {
|
||||
this.requestFileSuccess(this.currentTemplate);
|
||||
this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
global.IssuableTemplateSelector = IssuableTemplateSelector;
|
||||
})(window);
|
||||
})(window.gl || (window.gl = {}));
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
((global) => {
|
||||
class IssuableTemplateSelectors {
|
||||
constructor(opts = {}) {
|
||||
this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
|
||||
this.editor = opts.editor || this.initEditor();
|
||||
constructor({ $dropdowns, editor } = {}) {
|
||||
this.$dropdowns = $dropdowns || $('.js-issuable-selector');
|
||||
this.editor = editor || this.initEditor();
|
||||
|
||||
this.$dropdowns.each((i, dropdown) => {
|
||||
let $dropdown = $(dropdown);
|
||||
new IssuableTemplateSelector({
|
||||
const $dropdown = $(dropdown);
|
||||
new gl.IssuableTemplateSelector({
|
||||
pattern: /(\.md)/,
|
||||
data: $dropdown.data('data'),
|
||||
wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
|
||||
|
|
@ -26,4 +26,4 @@
|
|||
}
|
||||
|
||||
global.IssuableTemplateSelectors = IssuableTemplateSelectors;
|
||||
})(window);
|
||||
})(window.gl || (window.gl = {}));
|
||||
|
|
|
|||
|
|
@ -1,34 +1,29 @@
|
|||
(function() {
|
||||
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
((global) => {
|
||||
|
||||
this.Todos = (function() {
|
||||
function Todos(opts) {
|
||||
var ref;
|
||||
if (opts == null) {
|
||||
opts = {};
|
||||
}
|
||||
this.allDoneClicked = bind(this.allDoneClicked, this);
|
||||
this.doneClicked = bind(this.doneClicked, this);
|
||||
this.el = (ref = opts.el) != null ? ref : $('.js-todos-options');
|
||||
class Todos {
|
||||
constructor({ el } = {}) {
|
||||
this.allDoneClicked = this.allDoneClicked.bind(this);
|
||||
this.doneClicked = this.doneClicked.bind(this);
|
||||
this.el = el || $('.js-todos-options');
|
||||
this.perPage = this.el.data('perPage');
|
||||
this.clearListeners();
|
||||
this.initBtnListeners();
|
||||
this.initFilters();
|
||||
}
|
||||
|
||||
Todos.prototype.clearListeners = function() {
|
||||
clearListeners() {
|
||||
$('.done-todo').off('click');
|
||||
$('.js-todos-mark-all').off('click');
|
||||
return $('.todo').off('click');
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.initBtnListeners = function() {
|
||||
initBtnListeners() {
|
||||
$('.done-todo').on('click', this.doneClicked);
|
||||
$('.js-todos-mark-all').on('click', this.allDoneClicked);
|
||||
return $('.todo').on('click', this.goToTodoUrl);
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.initFilters = function() {
|
||||
initFilters() {
|
||||
new UsersSelect();
|
||||
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
|
||||
this.initFilterDropdown($('.js-type-search'), 'type');
|
||||
|
|
@ -38,125 +33,117 @@
|
|||
event.preventDefault();
|
||||
Turbolinks.visit(this.action + '&' + $(this).serialize());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) {
|
||||
initFilterDropdown($dropdown, fieldName, searchFields) {
|
||||
$dropdown.glDropdown({
|
||||
fieldName,
|
||||
selectable: true,
|
||||
filterable: searchFields ? true : false,
|
||||
fieldName: fieldName,
|
||||
search: { fields: searchFields },
|
||||
data: $dropdown.data('data'),
|
||||
clicked: function() {
|
||||
return $dropdown.closest('form.filter-form').submit();
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.doneClicked = function(e) {
|
||||
var $this;
|
||||
doneClicked(e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
$this = $(e.currentTarget);
|
||||
$this.disable();
|
||||
const $target = $(e.currentTarget);
|
||||
$target.disable();
|
||||
return $.ajax({
|
||||
type: 'POST',
|
||||
url: $this.attr('href'),
|
||||
url: $target.attr('href'),
|
||||
dataType: 'json',
|
||||
data: {
|
||||
'_method': 'delete'
|
||||
},
|
||||
success: (function(_this) {
|
||||
return function(data) {
|
||||
_this.redirectIfNeeded(data.count);
|
||||
_this.clearDone($this.closest('li'));
|
||||
return _this.updateBadges(data);
|
||||
};
|
||||
})(this)
|
||||
success: (data) => {
|
||||
this.redirectIfNeeded(data.count);
|
||||
this.clearDone($target.closest('li'));
|
||||
return this.updateBadges(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.allDoneClicked = function(e) {
|
||||
var $this;
|
||||
allDoneClicked(e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
$this = $(e.currentTarget);
|
||||
$this.disable();
|
||||
$target = $(e.currentTarget);
|
||||
$target.disable();
|
||||
return $.ajax({
|
||||
type: 'POST',
|
||||
url: $this.attr('href'),
|
||||
url: $target.attr('href'),
|
||||
dataType: 'json',
|
||||
data: {
|
||||
'_method': 'delete'
|
||||
},
|
||||
success: (function(_this) {
|
||||
return function(data) {
|
||||
$this.remove();
|
||||
$('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
|
||||
return _this.updateBadges(data);
|
||||
};
|
||||
})(this)
|
||||
success: (data) => {
|
||||
$target.remove();
|
||||
$('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
|
||||
return this.updateBadges(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.clearDone = function($row) {
|
||||
var $ul;
|
||||
$ul = $row.closest('ul');
|
||||
clearDone($row) {
|
||||
const $ul = $row.closest('ul');
|
||||
$row.remove();
|
||||
if (!$ul.find('li').length) {
|
||||
return $ul.parents('.panel').remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.updateBadges = function(data) {
|
||||
updateBadges(data) {
|
||||
$('.todos-pending .badge, .todos-pending-count').text(data.count);
|
||||
return $('.todos-done .badge').text(data.done_count);
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.getTotalPages = function() {
|
||||
getTotalPages() {
|
||||
return this.el.data('totalPages');
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.getCurrentPage = function() {
|
||||
getCurrentPage() {
|
||||
return this.el.data('currentPage');
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.getTodosPerPage = function() {
|
||||
getTodosPerPage() {
|
||||
return this.el.data('perPage');
|
||||
};
|
||||
}
|
||||
|
||||
redirectIfNeeded(total) {
|
||||
const currPages = this.getTotalPages();
|
||||
const currPage = this.getCurrentPage();
|
||||
|
||||
Todos.prototype.redirectIfNeeded = function(total) {
|
||||
var currPage, currPages, newPages, pageParams, url;
|
||||
currPages = this.getTotalPages();
|
||||
currPage = this.getCurrentPage();
|
||||
// Refresh if no remaining Todos
|
||||
if (!total) {
|
||||
location.reload();
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
// Do nothing if no pagination
|
||||
if (!currPages) {
|
||||
return;
|
||||
}
|
||||
newPages = Math.ceil(total / this.getTodosPerPage());
|
||||
// Includes query strings
|
||||
url = location.href;
|
||||
// If new total of pages is different than we have now
|
||||
|
||||
const newPages = Math.ceil(total / this.getTodosPerPage());
|
||||
let url = location.href;
|
||||
|
||||
if (newPages !== currPages) {
|
||||
// Redirect to previous page if there's one available
|
||||
if (currPages > 1 && currPage === currPages) {
|
||||
pageParams = {
|
||||
const pageParams = {
|
||||
page: currPages - 1
|
||||
};
|
||||
url = gl.utils.mergeUrlParams(pageParams, url);
|
||||
}
|
||||
return Turbolinks.visit(url);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Todos.prototype.goToTodoUrl = function(e) {
|
||||
var todoLink;
|
||||
todoLink = $(this).data('url');
|
||||
goToTodoUrl(e) {
|
||||
const todoLink = $(this).data('url');
|
||||
if (!todoLink) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -167,10 +154,8 @@
|
|||
} else {
|
||||
return Turbolinks.visit(todoLink);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Todos;
|
||||
|
||||
})();
|
||||
|
||||
}).call(this);
|
||||
global.Todos = Todos;
|
||||
})(window.gl || (window.gl = {}));
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
(global => {
|
||||
((global) => {
|
||||
global.User = class {
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
constructor({ action }) {
|
||||
this.action = action;
|
||||
this.placeProfileAvatarsToTop();
|
||||
this.initTabs();
|
||||
this.hideProjectLimitMessage();
|
||||
|
|
@ -14,9 +14,9 @@
|
|||
}
|
||||
|
||||
initTabs() {
|
||||
return new UserTabs({
|
||||
return new global.UserTabs({
|
||||
parentEl: '.user-profile',
|
||||
action: this.opts.action
|
||||
action: this.action
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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 = {}));
|
||||
|
|
@ -14,11 +14,12 @@
|
|||
$('.js-user-search').each((function(_this) {
|
||||
return function(i, dropdown) {
|
||||
var options = {};
|
||||
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser;
|
||||
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
|
||||
$dropdown = $(dropdown);
|
||||
options.projectId = $dropdown.data('project-id');
|
||||
options.showCurrentUser = $dropdown.data('current-user');
|
||||
showNullUser = $dropdown.data('null-user');
|
||||
showMenuAbove = $dropdown.data('showMenuAbove');
|
||||
showAnyUser = $dropdown.data('any-user');
|
||||
firstUser = $dropdown.data('first-user');
|
||||
options.authorId = $dropdown.data('author-id');
|
||||
|
|
@ -73,6 +74,7 @@
|
|||
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
|
||||
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
|
||||
return $dropdown.glDropdown({
|
||||
showMenuAbove: showMenuAbove,
|
||||
data: function(term, callback) {
|
||||
var isAuthorFilter;
|
||||
isAuthorFilter = $('.js-author-search');
|
||||
|
|
@ -116,8 +118,11 @@
|
|||
if (showDivider) {
|
||||
users.splice(showDivider, 0, "divider");
|
||||
}
|
||||
// Send the data back
|
||||
return callback(users);
|
||||
|
||||
callback(users);
|
||||
if (showMenuAbove) {
|
||||
$dropdown.data('glDropdown').positionMenuAbove();
|
||||
}
|
||||
});
|
||||
},
|
||||
filterable: true,
|
||||
|
|
@ -127,8 +132,8 @@
|
|||
},
|
||||
selectable: true,
|
||||
fieldName: $dropdown.data('field-name'),
|
||||
toggleLabel: function(selected) {
|
||||
if (selected && 'id' in selected) {
|
||||
toggleLabel: function(selected, el) {
|
||||
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
|
||||
if (selected.text) {
|
||||
return selected.text;
|
||||
} else {
|
||||
|
|
@ -138,6 +143,7 @@
|
|||
return defaultLabel;
|
||||
}
|
||||
},
|
||||
defaultLabel: defaultLabel,
|
||||
inputId: 'issue_assignee_id',
|
||||
hidden: function(e) {
|
||||
$selectbox.hide();
|
||||
|
|
@ -149,7 +155,9 @@
|
|||
page = $('body').data('page');
|
||||
isIssueIndex = page === 'projects:issues:index';
|
||||
isMRIndex = (page === page && page === 'projects:merge_requests:index');
|
||||
if ($dropdown.hasClass('js-filter-bulk-update')) {
|
||||
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
|
||||
e.preventDefault();
|
||||
selectedId = user.id;
|
||||
return;
|
||||
}
|
||||
if (page === 'projects:boards:show') {
|
||||
|
|
@ -167,6 +175,9 @@
|
|||
return assignTo(selected);
|
||||
}
|
||||
},
|
||||
id: function (user) {
|
||||
return user.id;
|
||||
},
|
||||
renderRow: function(user) {
|
||||
var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
|
||||
username = user.username ? "@" + user.username : "";
|
||||
|
|
|
|||
|
|
@ -604,3 +604,9 @@
|
|||
display: block;
|
||||
color: $gl-placeholder-color;
|
||||
}
|
||||
|
||||
.dropdown-toggle-text {
|
||||
&.is-default {
|
||||
color: $gl-placeholder-color;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@
|
|||
.flash-notice, .flash-alert {
|
||||
border-radius: $border-radius-default;
|
||||
|
||||
.container-fluid.container-limited.flash-text {
|
||||
.container-fluid,
|
||||
.container-fluid.container-limited {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,12 +36,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
.flash-notice .container-fluid {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-md-min) {
|
||||
ul.notes {
|
||||
.flash-container.timeline-content {
|
||||
|
|
|
|||
|
|
@ -350,6 +350,10 @@
|
|||
.issuable-form-select-holder {
|
||||
display: inline-block;
|
||||
width: 250px;
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.table-holder {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@
|
|||
|
||||
.table.builds {
|
||||
min-width: 1200px;
|
||||
|
||||
.branch-commit {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -385,6 +390,8 @@
|
|||
left: auto;
|
||||
right: -214px;
|
||||
top: -9px;
|
||||
max-height: 245px;
|
||||
overflow-y: scroll;
|
||||
|
||||
a:hover {
|
||||
.ci-status-text {
|
||||
|
|
|
|||
|
|
@ -146,7 +146,8 @@
|
|||
}
|
||||
|
||||
.project-repo-btn-group,
|
||||
.notification-dropdown {
|
||||
.notification-dropdown,
|
||||
.project-dropdown {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
margin-left: 10px;
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
|
|
@ -120,6 +121,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-sm-max) {
|
||||
.todos-filters {
|
||||
.dropdown-menu-toggle {
|
||||
width: 135px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
.todo {
|
||||
.avatar {
|
||||
|
|
@ -141,4 +150,14 @@
|
|||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.todos-filters {
|
||||
.row-content-block {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
|
|||
#
|
||||
# Returns nil
|
||||
def prompt_for_two_factor(user)
|
||||
return locked_user_redirect(user) if user.access_locked?
|
||||
|
||||
session[:otp_user_id] = user.id
|
||||
setup_u2f_authentication(user)
|
||||
render 'devise/sessions/two_factor'
|
||||
end
|
||||
|
||||
def locked_user_redirect(user)
|
||||
flash.now[:alert] = 'Invalid Login or password'
|
||||
render 'devise/sessions/new'
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor
|
||||
user = self.resource = find_user
|
||||
|
||||
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
||||
if user.access_locked?
|
||||
locked_user_redirect(user)
|
||||
elsif user_params[:otp_attempt].present? && session[:otp_user_id]
|
||||
authenticate_with_two_factor_via_otp(user)
|
||||
elsif user_params[:device_response].present? && session[:otp_user_id]
|
||||
authenticate_with_two_factor_via_u2f(user)
|
||||
|
|
@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
|
|||
remember_me(user) if user_params[:remember_me] == '1'
|
||||
sign_in(user)
|
||||
else
|
||||
user.increment_failed_attempts!
|
||||
flash.now[:alert] = 'Invalid two-factor code.'
|
||||
render :two_factor
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
|
|||
remember_me(user) if user_params[:remember_me] == '1'
|
||||
sign_in(user)
|
||||
else
|
||||
user.increment_failed_attempts!
|
||||
flash.now[:alert] = 'Authentication via U2F device failed.'
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,18 +15,17 @@ module MembershipActions
|
|||
end
|
||||
|
||||
def leave
|
||||
@member = membershipable.members.find_by(user_id: current_user) ||
|
||||
membershipable.requesters.find_by(user_id: current_user)
|
||||
Members::DestroyService.new(@member, current_user).execute
|
||||
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
|
||||
execute(:all)
|
||||
|
||||
source_type = @member.real_source_type.humanize(capitalize: false)
|
||||
source_type = membershipable.class.to_s.humanize(capitalize: false)
|
||||
notice =
|
||||
if @member.request?
|
||||
if member.request?
|
||||
"Your access request to the #{source_type} has been withdrawn."
|
||||
else
|
||||
"You left the \"#{@member.source.human_name}\" #{source_type}."
|
||||
"You left the \"#{membershipable.human_name}\" #{source_type}."
|
||||
end
|
||||
redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize]
|
||||
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
|
||||
|
||||
redirect_to redirect_path, notice: notice
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
|
|||
end
|
||||
|
||||
def trending
|
||||
@projects = TrendingProjectsFinder.new.execute(current_user)
|
||||
@projects = TrendingProjectsFinder.new.execute
|
||||
@projects = filter_projects(@projects)
|
||||
@projects = @projects.page(params[:page])
|
||||
|
||||
|
|
|
|||
|
|
@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@group_member = @group.members.find_by(id: params[:id]) ||
|
||||
@group.requesters.find_by(id: params[:id])
|
||||
|
||||
Members::DestroyService.new(@group_member, current_user).execute
|
||||
Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
class Projects::BoardsController < Projects::ApplicationController
|
||||
include IssuableCollections
|
||||
|
||||
respond_to :html
|
||||
|
||||
before_action :authorize_read_board!, only: [:show]
|
||||
|
|
|
|||
|
|
@ -5,17 +5,25 @@ class Projects::GroupLinksController < Projects::ApplicationController
|
|||
|
||||
def index
|
||||
@group_links = project.project_group_links.all
|
||||
|
||||
@skip_groups = @group_links.pluck(:group_id)
|
||||
@skip_groups << project.group.try(:id)
|
||||
end
|
||||
|
||||
def create
|
||||
group = Group.find(params[:link_group_id])
|
||||
return render_404 unless can?(current_user, :read_group, group)
|
||||
group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
|
||||
|
||||
project.project_group_links.create(
|
||||
group: group,
|
||||
group_access: params[:link_group_access],
|
||||
expires_at: params[:expires_at]
|
||||
)
|
||||
if group
|
||||
return render_404 unless can?(current_user, :read_group, group)
|
||||
|
||||
project.project_group_links.create(
|
||||
group: group,
|
||||
group_access: params[:link_group_access],
|
||||
expires_at: params[:expires_at]
|
||||
)
|
||||
else
|
||||
flash[:alert] = 'Please select a group.'
|
||||
end
|
||||
|
||||
redirect_to namespace_project_group_links_path(project.namespace, project)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController
|
|||
@label = @project.labels.create(label_params)
|
||||
|
||||
if @label.valid?
|
||||
redirect_to namespace_project_labels_path(@project.namespace, @project)
|
||||
respond_to do |format|
|
||||
format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) }
|
||||
format.json { render json: @label }
|
||||
end
|
||||
else
|
||||
render 'new'
|
||||
respond_to do |format|
|
||||
format.html { render 'new' }
|
||||
format.json { render json: { message: @label.errors.messages }, status: 400 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
before_action :define_diff_comment_vars, only: [:diffs]
|
||||
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
|
||||
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
|
||||
before_action :apply_diff_view_cookie!, only: [:new_diffs]
|
||||
before_action :build_merge_request, only: [:new, :new_diffs]
|
||||
|
||||
# Allow read any merge_request
|
||||
before_action :authorize_read_merge_request!
|
||||
|
|
@ -210,29 +212,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def new
|
||||
apply_diff_view_cookie!
|
||||
define_new_vars
|
||||
end
|
||||
|
||||
build_merge_request
|
||||
@noteable = @merge_request
|
||||
def new_diffs
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
define_new_vars
|
||||
render "new"
|
||||
end
|
||||
format.json do
|
||||
@diffs = if @merge_request.can_be_created
|
||||
@merge_request.diffs(diff_options)
|
||||
else
|
||||
[]
|
||||
end
|
||||
@diff_notes_disabled = true
|
||||
|
||||
@target_branches = if @merge_request.target_project
|
||||
@merge_request.target_project.repository.branch_names
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
@target_project = merge_request.target_project
|
||||
@source_project = merge_request.source_project
|
||||
@commits = @merge_request.compare_commits.reverse
|
||||
@commit = @merge_request.diff_head_commit
|
||||
@base_commit = @merge_request.diff_base_commit
|
||||
@diffs = @merge_request.diffs(diff_options) if @merge_request.compare
|
||||
@diff_notes_disabled = true
|
||||
@pipeline = @merge_request.pipeline
|
||||
@statuses = @pipeline.statuses.relevant if @pipeline
|
||||
|
||||
@note_counts = Note.where(commit_id: @commits.map(&:id)).
|
||||
group(:commit_id).count
|
||||
render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
@ -490,6 +489,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
)
|
||||
end
|
||||
|
||||
def define_new_vars
|
||||
@noteable = @merge_request
|
||||
|
||||
@target_branches = if @merge_request.target_project
|
||||
@merge_request.target_project.repository.branch_names
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
@target_project = merge_request.target_project
|
||||
@source_project = merge_request.source_project
|
||||
@commits = @merge_request.compare_commits.reverse
|
||||
@commit = @merge_request.diff_head_commit
|
||||
@base_commit = @merge_request.diff_base_commit
|
||||
|
||||
@pipeline = @merge_request.pipeline
|
||||
@statuses = @pipeline.statuses.relevant if @pipeline
|
||||
@note_counts = Note.where(commit_id: @commits.map(&:id)).
|
||||
group(:commit_id).count
|
||||
end
|
||||
|
||||
def invalid_mr
|
||||
# Render special view for MR with removed target branch
|
||||
render 'invalid'
|
||||
|
|
@ -521,7 +541,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
|
||||
def build_merge_request
|
||||
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
|
||||
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
|
||||
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
|
||||
end
|
||||
|
||||
def compared_diff_version
|
||||
|
|
|
|||
|
|
@ -59,10 +59,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@project_member = @project.members.find_by(id: params[:id]) ||
|
||||
@project.requesters.find_by(id: params[:id])
|
||||
|
||||
Members::DestroyService.new(@project_member, current_user).execute
|
||||
Members::DestroyService.new(@project, current_user, params).
|
||||
execute(:all)
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
# Finder for retrieving public trending projects in a given time range.
|
||||
class TrendingProjectsFinder
|
||||
def execute(current_user, start_date = 1.month.ago)
|
||||
projects_for(current_user).trending(start_date)
|
||||
# current_user - The currently logged in User, if any.
|
||||
# last_months - The number of months to limit the trending data to.
|
||||
def execute(months_limit = 1)
|
||||
Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do
|
||||
Project.public_only.trending(months_limit.months.ago)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def projects_for(current_user)
|
||||
ProjectsFinder.new.execute(current_user)
|
||||
def cache_key_for(months)
|
||||
"trending_projects/#{months}"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -40,8 +40,9 @@ module DropdownsHelper
|
|||
end
|
||||
|
||||
def dropdown_toggle(toggle_text, data_attr, options = {})
|
||||
default_label = data_attr[:default_label]
|
||||
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
|
||||
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
|
||||
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
|
||||
output << icon('chevron-down')
|
||||
output.html_safe
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,18 +8,12 @@ module IssuablesHelper
|
|||
end
|
||||
|
||||
def multi_label_name(current_labels, default_label)
|
||||
# current_labels may be a string from before
|
||||
if current_labels.is_a?(Array)
|
||||
if current_labels.count > 1
|
||||
"#{current_labels[0]} +#{current_labels.count - 1} more"
|
||||
if current_labels && current_labels.any?
|
||||
title = current_labels.first.try(:title)
|
||||
if current_labels.size > 1
|
||||
"#{title} +#{current_labels.size - 1} more"
|
||||
else
|
||||
current_labels[0]
|
||||
end
|
||||
elsif current_labels.is_a?(String)
|
||||
if current_labels.nil? || current_labels.empty?
|
||||
default_label
|
||||
else
|
||||
current_labels
|
||||
title
|
||||
end
|
||||
else
|
||||
default_label
|
||||
|
|
|
|||
|
|
@ -115,8 +115,9 @@ module LabelsHelper
|
|||
end
|
||||
|
||||
def labels_filter_path
|
||||
if @project
|
||||
namespace_project_labels_path(@project.namespace, @project, :json)
|
||||
project = @target_project || @project
|
||||
if project
|
||||
namespace_project_labels_path(project.namespace, project, :json)
|
||||
else
|
||||
dashboard_labels_path(:json)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -71,8 +71,9 @@ module MilestonesHelper
|
|||
end
|
||||
|
||||
def milestones_filter_dropdown_path
|
||||
if @project
|
||||
namespace_project_milestones_path(@project.namespace, @project, :json)
|
||||
project = @target_project || @project
|
||||
if project
|
||||
namespace_project_milestones_path(project.namespace, project, :json)
|
||||
else
|
||||
dashboard_milestones_path(:json)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -92,12 +92,8 @@ module PageLayoutHelper
|
|||
end
|
||||
end
|
||||
|
||||
def fluid_layout(enabled = false)
|
||||
if @fluid_layout.nil?
|
||||
@fluid_layout = (current_user && current_user.layout == "fluid") || enabled
|
||||
else
|
||||
@fluid_layout
|
||||
end
|
||||
def fluid_layout
|
||||
current_user && current_user.layout == "fluid"
|
||||
end
|
||||
|
||||
def blank_container(enabled = false)
|
||||
|
|
|
|||
|
|
@ -49,12 +49,10 @@ module SelectsHelper
|
|||
end
|
||||
|
||||
def select2_tag(id, opts = {})
|
||||
css_class = ''
|
||||
css_class << 'multiselect ' if opts[:multiple]
|
||||
css_class << (opts[:class] || '')
|
||||
opts[:class] << ' multiselect' if opts[:multiple]
|
||||
value = opts[:selected] || ''
|
||||
|
||||
hidden_field_tag(id, value, class: css_class)
|
||||
hidden_field_tag(id, value, opts)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -114,6 +114,26 @@ module TodosHelper
|
|||
selected_type ? selected_type[:text] : default_type
|
||||
end
|
||||
|
||||
def todo_due_date(todo)
|
||||
return unless todo.target.try(:due_date)
|
||||
|
||||
is_due_today = todo.target.due_date.today?
|
||||
is_overdue = todo.target.overdue?
|
||||
css_class =
|
||||
if is_due_today
|
||||
'text-warning'
|
||||
elsif is_overdue
|
||||
'text-danger'
|
||||
else
|
||||
''
|
||||
end
|
||||
|
||||
html = "· ".html_safe
|
||||
html << content_tag(:span, class: css_class) do
|
||||
"Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def show_todo_state?(todo)
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ module Ci
|
|||
end
|
||||
|
||||
def has_warnings?
|
||||
builds.latest.ignored.any?
|
||||
builds.latest.failed_but_allowed.any?
|
||||
end
|
||||
|
||||
def config_processor
|
||||
|
|
@ -251,9 +251,8 @@ module Ci
|
|||
Ci::ProcessPipelineService.new(project, user).execute(self)
|
||||
end
|
||||
|
||||
def build_updated
|
||||
def update_status
|
||||
with_lock do
|
||||
reload
|
||||
case latest_builds_status
|
||||
when 'pending' then enqueue
|
||||
when 'running' then run
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ module Ci
|
|||
class Runner < ActiveRecord::Base
|
||||
extend Ci::Model
|
||||
|
||||
LAST_CONTACT_TIME = 2.hours.ago
|
||||
LAST_CONTACT_TIME = 1.hour.ago
|
||||
AVAILABLE_SCOPES = %w[specific shared active paused online]
|
||||
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,22 @@ class CommitStatus < ActiveRecord::Base
|
|||
|
||||
scope :retried, -> { where.not(id: latest) }
|
||||
scope :ordered, -> { order(:name) }
|
||||
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
|
||||
|
||||
scope :failed_but_allowed, -> do
|
||||
where(allow_failure: true, status: [:failed, :canceled])
|
||||
end
|
||||
|
||||
scope :exclude_ignored, -> do
|
||||
quoted_when = connection.quote_column_name('when')
|
||||
# We want to ignore failed_but_allowed jobs
|
||||
where("allow_failure = ? OR status IN (?)",
|
||||
false, all_state_names - [:failed, :canceled]).
|
||||
# We want to ignore skipped manual jobs
|
||||
where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped').
|
||||
# We want to ignore skipped on_failure
|
||||
where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
|
||||
end
|
||||
|
||||
scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
|
||||
scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
|
||||
|
||||
|
|
@ -69,13 +84,18 @@ class CommitStatus < ActiveRecord::Base
|
|||
commit_status.update_attributes finished_at: Time.now
|
||||
end
|
||||
|
||||
after_transition any => [:success, :failed, :canceled] do |commit_status|
|
||||
commit_status.pipeline.try(:process!)
|
||||
true
|
||||
end
|
||||
|
||||
after_transition do |commit_status, transition|
|
||||
commit_status.pipeline.try(:build_updated) unless transition.loopback?
|
||||
commit_status.pipeline.try do |pipeline|
|
||||
break if transition.loopback?
|
||||
|
||||
if commit_status.complete?
|
||||
ProcessPipelineWorker.perform_async(pipeline.id)
|
||||
end
|
||||
|
||||
UpdatePipelineWorker.perform_async(pipeline.id)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
after_transition [:created, :pending, :running] => :success do |commit_status|
|
||||
|
|
@ -111,7 +131,7 @@ class CommitStatus < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def ignored?
|
||||
def failed_but_allowed?
|
||||
allow_failure? && (failed? || canceled?)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -8,32 +8,32 @@ module HasStatus
|
|||
|
||||
class_methods do
|
||||
def status_sql
|
||||
scope = all
|
||||
scope = if respond_to?(:exclude_ignored)
|
||||
exclude_ignored
|
||||
else
|
||||
all
|
||||
end
|
||||
builds = scope.select('count(*)').to_sql
|
||||
created = scope.created.select('count(*)').to_sql
|
||||
success = scope.success.select('count(*)').to_sql
|
||||
ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
|
||||
ignored ||= '0'
|
||||
pending = scope.pending.select('count(*)').to_sql
|
||||
running = scope.running.select('count(*)').to_sql
|
||||
canceled = scope.canceled.select('count(*)').to_sql
|
||||
skipped = scope.skipped.select('count(*)').to_sql
|
||||
canceled = scope.canceled.select('count(*)').to_sql
|
||||
|
||||
deduce_status = "(CASE
|
||||
"(CASE
|
||||
WHEN (#{builds})=(#{success}) THEN 'success'
|
||||
WHEN (#{builds})=(#{created}) THEN 'created'
|
||||
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
|
||||
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
|
||||
WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
|
||||
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
|
||||
WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped'
|
||||
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
|
||||
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
|
||||
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
|
||||
ELSE 'failed'
|
||||
END)"
|
||||
|
||||
deduce_status
|
||||
end
|
||||
|
||||
def status
|
||||
all.pluck(self.status_sql).first
|
||||
all.pluck(status_sql).first
|
||||
end
|
||||
|
||||
def started_at
|
||||
|
|
@ -43,6 +43,10 @@ module HasStatus
|
|||
def finished_at
|
||||
all.maximum(:finished_at)
|
||||
end
|
||||
|
||||
def all_state_names
|
||||
state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) }
|
||||
end
|
||||
end
|
||||
|
||||
included do
|
||||
|
|
|
|||
|
|
@ -43,19 +43,15 @@ module Mentionable
|
|||
self
|
||||
end
|
||||
|
||||
def all_references(current_user = nil, text = nil, extractor: nil)
|
||||
def all_references(current_user = nil, extractor: nil)
|
||||
extractor ||= Gitlab::ReferenceExtractor.
|
||||
new(project, current_user)
|
||||
|
||||
if text
|
||||
extractor.analyze(text, author: author)
|
||||
else
|
||||
self.class.mentionable_attrs.each do |attr, options|
|
||||
text = __send__(attr)
|
||||
options = options.merge(cache_key: [self, attr], author: author)
|
||||
self.class.mentionable_attrs.each do |attr, options|
|
||||
text = __send__(attr)
|
||||
options = options.merge(cache_key: [self, attr], author: author)
|
||||
|
||||
extractor.analyze(text, options)
|
||||
end
|
||||
extractor.analyze(text, options)
|
||||
end
|
||||
|
||||
extractor
|
||||
|
|
@ -66,8 +62,8 @@ module Mentionable
|
|||
end
|
||||
|
||||
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
|
||||
def referenced_mentionables(current_user = self.author, text = nil)
|
||||
refs = all_references(current_user, text)
|
||||
def referenced_mentionables(current_user = self.author)
|
||||
refs = all_references(current_user)
|
||||
refs = (refs.issues + refs.merge_requests + refs.commits)
|
||||
|
||||
# We're using this method instead of Array diffing because that requires
|
||||
|
|
@ -77,8 +73,8 @@ module Mentionable
|
|||
end
|
||||
|
||||
# Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
|
||||
def create_cross_references!(author = self.author, without = [], text = nil)
|
||||
refs = referenced_mentionables(author, text)
|
||||
def create_cross_references!(author = self.author, without = [])
|
||||
refs = referenced_mentionables(author)
|
||||
|
||||
# We're using this method instead of Array diffing because that requires
|
||||
# both of the object's `hash` values to be the same, which may not be the
|
||||
|
|
@ -97,10 +93,7 @@ module Mentionable
|
|||
|
||||
return if changes.empty?
|
||||
|
||||
original_text = changes.collect { |_, vals| vals.first }.join(' ')
|
||||
|
||||
preexisting = referenced_mentionables(author, original_text)
|
||||
create_cross_references!(author, preexisting)
|
||||
create_cross_references!(author)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
|
|||
|
||||
delegate :name, to: :environment, prefix: true
|
||||
|
||||
after_save :keep_around_commit
|
||||
after_save :create_ref
|
||||
|
||||
def commit
|
||||
project.commit(sha)
|
||||
|
|
@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base
|
|||
self == environment.last_deployment
|
||||
end
|
||||
|
||||
def keep_around_commit
|
||||
project.repository.keep_around(self.sha)
|
||||
def create_ref
|
||||
project.repository.create_ref(ref, ref_path)
|
||||
end
|
||||
|
||||
def manual_actions
|
||||
|
|
@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base
|
|||
where.not(id: self.id).
|
||||
take
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ref_path
|
||||
File.join(environment.ref_path, 'deployments', id.to_s)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base
|
|||
def update_merge_request_metrics?
|
||||
self.name == "production"
|
||||
end
|
||||
|
||||
def ref_path
|
||||
"refs/environments/#{Shellwords.shellescape(name)}"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -328,13 +328,15 @@ class Event < ActiveRecord::Base
|
|||
def reset_project_activity
|
||||
return unless project
|
||||
|
||||
# Don't even bother obtaining a lock if the last update happened less than
|
||||
# 60 minutes ago.
|
||||
# Don't bother updating if we know the project was updated recently.
|
||||
return if recent_update?
|
||||
|
||||
return unless try_obtain_lease
|
||||
|
||||
project.update_column(:last_activity_at, created_at)
|
||||
# At this point it's possible for multiple threads/processes to try to
|
||||
# update the project. Only one query should actually perform the update,
|
||||
# hence we add the extra WHERE clause for last_activity_at.
|
||||
Project.unscoped.where(id: project_id).
|
||||
where('last_activity_at > ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
|
||||
update_all(last_activity_at: created_at)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -342,11 +344,4 @@ class Event < ActiveRecord::Base
|
|||
def recent_update?
|
||||
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
|
||||
end
|
||||
|
||||
def try_obtain_lease
|
||||
Gitlab::ExclusiveLease.
|
||||
new("project:update_last_activity_at:#{project.id}",
|
||||
timeout: RESET_PROJECT_ACTIVITY_INTERVAL.to_i).
|
||||
try_obtain
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
|
||||
# Temporary fields to store compare vars
|
||||
# when creating new merge request
|
||||
attr_accessor :can_be_created, :compare_commits, :compare
|
||||
attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
|
||||
|
||||
state_machine :state, initial: :opened do
|
||||
event :close do
|
||||
|
|
@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def diff_size
|
||||
merge_request_diff.size
|
||||
diffs(diff_options).size
|
||||
end
|
||||
|
||||
def diff_base_commit
|
||||
|
|
@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base
|
|||
# `MergeRequestsClosingIssues` model. This is a performance optimization.
|
||||
# Calculating this information for a number of merge requests requires
|
||||
# running `ReferenceExtractor` on each of them separately.
|
||||
# This optimization does not apply to issues from external sources.
|
||||
def cache_merge_request_closes_issues!(current_user = self.author)
|
||||
return if project.has_external_issue_tracker?
|
||||
|
||||
transaction do
|
||||
self.merge_requests_closing_issues.delete_all
|
||||
|
||||
closes_issues(current_user).each do |issue|
|
||||
self.merge_requests_closing_issues.create!(issue: issue)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -380,6 +380,7 @@ class Project < ActiveRecord::Base
|
|||
SELECT project_id, COUNT(*) AS amount
|
||||
FROM notes
|
||||
WHERE created_at >= #{sanitize(since)}
|
||||
AND system IS FALSE
|
||||
GROUP BY project_id
|
||||
) join_note_counts ON projects.id = join_note_counts.project_id"
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ class ProjectFeature < ActiveRecord::Base
|
|||
|
||||
FEATURES = %i(issues merge_requests wiki snippets builds)
|
||||
|
||||
belongs_to :project
|
||||
# Default scopes force us to unscope here since a service may need to check
|
||||
# permissions for a project in pending_delete
|
||||
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
|
||||
belongs_to :project, -> { unscope(where: :pending_delete) }
|
||||
|
||||
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
|
||||
default_value_for :issues_access_level, value: ENABLED, allows_nil: false
|
||||
|
|
|
|||
|
|
@ -838,6 +838,52 @@ class Repository
|
|||
end
|
||||
end
|
||||
|
||||
def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
|
||||
update_branch_with_hooks(user, branch) do |ref|
|
||||
index = rugged.index
|
||||
parents = []
|
||||
branch = find_branch(ref)
|
||||
|
||||
if branch
|
||||
last_commit = branch.target
|
||||
index.read_tree(last_commit.raw_commit.tree)
|
||||
parents = [last_commit.sha]
|
||||
end
|
||||
|
||||
actions.each do |action|
|
||||
case action[:action]
|
||||
when :create, :update, :move
|
||||
mode =
|
||||
case action[:action]
|
||||
when :update
|
||||
index.get(action[:file_path])[:mode]
|
||||
when :move
|
||||
index.get(action[:previous_path])[:mode]
|
||||
end
|
||||
mode ||= 0o100644
|
||||
|
||||
index.remove(action[:previous_path]) if action[:action] == :move
|
||||
|
||||
content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
|
||||
oid = rugged.write(content, :blob)
|
||||
|
||||
index.add(path: action[:file_path], oid: oid, mode: mode)
|
||||
when :delete
|
||||
index.remove(action[:file_path])
|
||||
end
|
||||
end
|
||||
|
||||
options = {
|
||||
tree: index.write_tree(rugged),
|
||||
message: message,
|
||||
parents: parents
|
||||
}
|
||||
options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
|
||||
|
||||
Rugged::Commit.create(rugged, options)
|
||||
end
|
||||
end
|
||||
|
||||
def get_committer_and_author(user, email: nil, name: nil)
|
||||
committer = user_to_committer(user)
|
||||
author = Gitlab::Git::committer_hash(email: email, name: name) || committer
|
||||
|
|
@ -997,6 +1043,10 @@ class Repository
|
|||
Gitlab::Popen.popen(args, path_to_repo)
|
||||
end
|
||||
|
||||
def create_ref(ref, ref_path)
|
||||
fetch_ref(path_to_repo, ref, ref_path)
|
||||
end
|
||||
|
||||
def update_branch_with_hooks(current_user, branch)
|
||||
update_autocrlf_option
|
||||
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ class Service < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def #{arg}=(value)
|
||||
self.properties ||= {}
|
||||
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
|
||||
self.properties['#{arg}'] = value
|
||||
end
|
||||
|
|
|
|||
|
|
@ -279,6 +279,11 @@ class User < ActiveRecord::Base
|
|||
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
|
||||
end
|
||||
|
||||
# Returns a user for the given SSH key.
|
||||
def find_by_ssh_key_id(key_id)
|
||||
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
|
||||
end
|
||||
|
||||
def build_user(attrs = {})
|
||||
User.new(attrs)
|
||||
end
|
||||
|
|
@ -827,6 +832,22 @@ class User < ActiveRecord::Base
|
|||
todos_pending_count(force: true)
|
||||
end
|
||||
|
||||
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
|
||||
# flow means we don't call that automatically (and can't conveniently do so).
|
||||
#
|
||||
# See:
|
||||
# <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
|
||||
#
|
||||
def increment_failed_attempts!
|
||||
self.failed_attempts ||= 0
|
||||
self.failed_attempts += 1
|
||||
if attempts_exceeded?
|
||||
lock_access! unless access_locked?
|
||||
else
|
||||
save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def projects_union(min_access_level = nil)
|
||||
|
|
|
|||
|
|
@ -56,9 +56,8 @@ class BaseService
|
|||
result
|
||||
end
|
||||
|
||||
def success
|
||||
{
|
||||
status: :success
|
||||
}
|
||||
def success(pass_back = {})
|
||||
pass_back[:status] = :success
|
||||
pass_back
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,12 +36,7 @@ module Boards
|
|||
end
|
||||
|
||||
def set_state
|
||||
params[:state] =
|
||||
case list.list_type.to_sym
|
||||
when :backlog then 'opened'
|
||||
when :done then 'closed'
|
||||
else 'all'
|
||||
end
|
||||
params[:state] = list.done? ? 'closed' : 'opened'
|
||||
end
|
||||
|
||||
def board_label_ids
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@ module Files
|
|||
create_target_branch
|
||||
end
|
||||
|
||||
if commit
|
||||
success
|
||||
result = commit
|
||||
if result
|
||||
success(result: result)
|
||||
else
|
||||
error('Something went wrong. Your changes were not committed')
|
||||
end
|
||||
|
|
@ -42,6 +43,12 @@ module Files
|
|||
@source_branch != @target_branch || @source_project != @project
|
||||
end
|
||||
|
||||
def file_has_changed?
|
||||
return false unless @last_commit_sha && last_commit
|
||||
|
||||
@last_commit_sha != last_commit.sha
|
||||
end
|
||||
|
||||
def raise_error(message)
|
||||
raise ValidationError.new(message)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -23,12 +23,6 @@ module Files
|
|||
end
|
||||
end
|
||||
|
||||
def file_has_changed?
|
||||
return false unless @last_commit_sha && last_commit
|
||||
|
||||
@last_commit_sha != last_commit.sha
|
||||
end
|
||||
|
||||
def last_commit
|
||||
@last_commit ||= Gitlab::Git::Commit.
|
||||
last_for_path(@source_project.repository, @source_branch, @file_path)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ module Members
|
|||
if member.request? && member.user != user
|
||||
notification_service.decline_access_request(member)
|
||||
end
|
||||
|
||||
member
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,17 +1,42 @@
|
|||
module Members
|
||||
class DestroyService < BaseService
|
||||
attr_accessor :member, :current_user
|
||||
include MembersHelper
|
||||
|
||||
def initialize(member, current_user)
|
||||
@member = member
|
||||
attr_accessor :source
|
||||
|
||||
ALLOWED_SCOPES = %i[members requesters all]
|
||||
|
||||
def initialize(source, current_user, params = {})
|
||||
@source = source
|
||||
@current_user = current_user
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
|
||||
raise Gitlab::Access::AccessDeniedError
|
||||
end
|
||||
def execute(scope = :members)
|
||||
raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
|
||||
|
||||
member = find_member!(scope)
|
||||
|
||||
raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
|
||||
|
||||
AuthorizedDestroyService.new(member, current_user).execute
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_member!(scope)
|
||||
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
|
||||
case scope
|
||||
when :all
|
||||
source.members.find_by(condition) ||
|
||||
source.requesters.find_by!(condition)
|
||||
else
|
||||
source.public_send(scope).find_by!(condition)
|
||||
end
|
||||
end
|
||||
|
||||
def can_destroy_member?(member)
|
||||
member && can?(current_user, action_member_permission(:destroy, member), member)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ module Projects
|
|||
def execute
|
||||
forked_from_project_id = params.delete(:forked_from_project_id)
|
||||
import_data = params.delete(:import_data)
|
||||
@skip_wiki = params.delete(:skip_wiki)
|
||||
|
||||
@project = Project.new(params)
|
||||
|
||||
# Make sure that the user is allowed to use the specified visibility level
|
||||
|
|
@ -15,6 +17,11 @@ module Projects
|
|||
return @project
|
||||
end
|
||||
|
||||
unless allowed_fork?(forked_from_project_id)
|
||||
@project.errors.add(:forked_from_project_id, 'is forbidden')
|
||||
return @project
|
||||
end
|
||||
|
||||
# Set project name from path
|
||||
if @project.name.present? && @project.path.present?
|
||||
# if both name and path set - everything is ok
|
||||
|
|
@ -71,6 +78,13 @@ module Projects
|
|||
@project.errors.add(:namespace, "is not valid")
|
||||
end
|
||||
|
||||
def allowed_fork?(source_project_id)
|
||||
return true if source_project_id.nil?
|
||||
|
||||
source_project = Project.find_by(id: source_project_id)
|
||||
current_user.can?(:fork_project, source_project)
|
||||
end
|
||||
|
||||
def allowed_namespace?(user, namespace_id)
|
||||
namespace = Namespace.find_by(id: namespace_id)
|
||||
current_user.can?(:create_projects, namespace)
|
||||
|
|
@ -80,7 +94,7 @@ module Projects
|
|||
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
|
||||
|
||||
unless @project.gitlab_project_import?
|
||||
@project.create_wiki if @project.feature_available?(:wiki, current_user)
|
||||
@project.create_wiki unless skip_wiki?
|
||||
@project.build_missing_services
|
||||
|
||||
@project.create_labels
|
||||
|
|
@ -94,6 +108,10 @@ module Projects
|
|||
end
|
||||
end
|
||||
|
||||
def skip_wiki?
|
||||
!@project.feature_available?(:wiki, current_user) || @skip_wiki
|
||||
end
|
||||
|
||||
def save_project_and_import_data(import_data)
|
||||
Project.transaction do
|
||||
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ module Projects
|
|||
end
|
||||
|
||||
new_project = CreateService.new(current_user, new_params).execute
|
||||
return new_project unless new_project.persisted?
|
||||
|
||||
builds_access_level = @project.project_feature.builds_access_level
|
||||
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
|
||||
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ module SystemNoteService
|
|||
notes = notes.where(noteable_id: noteable.id)
|
||||
end
|
||||
|
||||
notes_for_mentioner(mentioner, noteable, notes).count > 0
|
||||
notes_for_mentioner(mentioner, noteable, notes).exists?
|
||||
end
|
||||
|
||||
# Build an Array of lines detailing each commit added in a merge request
|
||||
|
|
|
|||
|
|
@ -63,6 +63,11 @@
|
|||
Reply by email
|
||||
%span.light.pull-right
|
||||
= boolean_to_icon Gitlab::IncomingEmail.enabled?
|
||||
%p
|
||||
Container Registry
|
||||
%span.light.pull-right
|
||||
= boolean_to_icon Gitlab.config.registry.enabled
|
||||
|
||||
.col-md-4
|
||||
%h4
|
||||
Components
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
%tr
|
||||
%td #{stage.capitalize} Job - #{build[:name]}
|
||||
%td
|
||||
%pre
|
||||
= simple_format build[:commands]
|
||||
%pre= build[:commands]
|
||||
|
||||
%br
|
||||
%b Tag list:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
(removed)
|
||||
|
||||
· #{time_ago_with_tooltip(todo.created_at)}
|
||||
= todo_due_date(todo)
|
||||
|
||||
.todo-body
|
||||
.todo-note
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@
|
|||
.form-group
|
||||
= f.label :projects, "Projects", class: "control-label"
|
||||
.col-sm-10
|
||||
= f.collection_select :project_ids, @group.projects, :id, :name,
|
||||
{ selected: @group.projects.map(&:id) }, multiple: true, class: 'select2'
|
||||
= f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
|
||||
{ selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
|
||||
|
||||
.col-md-6
|
||||
.form-group
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
|
||||
%div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
|
||||
%div{ class: "container-fluid" }
|
||||
.header-content
|
||||
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
|
||||
%span.sr-only Toggle navigation
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>')
|
|||
|
||||
// Toggle container-fluid class
|
||||
if ('<%= current_user.layout %>' === 'fluid') {
|
||||
$('.content-wrapper').find('.container-fluid').removeClass('container-limited')
|
||||
$('.content-wrapper .container-fluid').removeClass('container-limited')
|
||||
} else {
|
||||
$('.content-wrapper').find('.container-fluid').addClass('container-limited')
|
||||
$('.content-wrapper .container-fluid').addClass('container-limited')
|
||||
}
|
||||
|
||||
// Re-enable the "Save" button
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue