squashed merge and fixed conflicts
This commit is contained in:
parent
9be06bbbb4
commit
13e37a3ee5
403 changed files with 16753 additions and 3061 deletions
|
@ -7,7 +7,8 @@ services:
|
|||
cache:
|
||||
key: "ruby21"
|
||||
paths:
|
||||
- vendor
|
||||
- vendor/apt
|
||||
- vendor/ruby
|
||||
|
||||
variables:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
|
||||
|
@ -91,9 +92,7 @@ update-knapsack:
|
|||
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
|
||||
- export KNAPSACK_GENERATE_REPORT=true
|
||||
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
|
||||
- knapsack spinach "-r rerun"
|
||||
# retry failed tests 3 times
|
||||
- retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)'
|
||||
- knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
|
||||
artifacts:
|
||||
paths:
|
||||
- knapsack/
|
||||
|
|
|
@ -349,7 +349,7 @@ Style/MultilineArrayBraceLayout:
|
|||
|
||||
# Avoid multi-line chains of blocks.
|
||||
Style/MultilineBlockChain:
|
||||
Enabled: false
|
||||
Enabled: true
|
||||
|
||||
# Ensures newlines after multiline block do statements.
|
||||
Style/MultilineBlockLayout:
|
||||
|
|
83
CHANGELOG
83
CHANGELOG
|
@ -2,14 +2,18 @@ Please view this file on the master branch, on stable branches it's out of date.
|
|||
|
||||
v 8.9.0 (unreleased)
|
||||
- Fix Error 500 when using closes_issues API with an external issue tracker
|
||||
- Add more information into RSS feed for issues (Alexander Matyushentsev)
|
||||
- Bulk assign/unassign labels to issues.
|
||||
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
|
||||
- Fix endless redirections when accessing user OAuth applications when they are disabled
|
||||
- Allow enabling wiki page events from Webhook management UI
|
||||
- Bump rouge to 1.11.0
|
||||
- Fix issue with arrow keys not working in search autocomplete dropdown
|
||||
- Fix an issue where note polling stopped working if a window was in the
|
||||
background during a refresh.
|
||||
- Make EmailsOnPushWorker use Sidekiq mailers queue
|
||||
- Fix wiki page events' webhook to point to the wiki repository
|
||||
- Don't show tags for revert and cherry-pick operations
|
||||
- Fix issue todo not remove when leave project !4150 (Long Nguyen)
|
||||
- Allow customisable text on the 'nearly there' page after a user signs up
|
||||
- Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
|
||||
|
@ -18,11 +22,19 @@ v 8.9.0 (unreleased)
|
|||
- Added descriptions to notification settings dropdown
|
||||
- Improve note validation to prevent errors when creating invalid note via API
|
||||
- Reduce number of fog gem dependencies
|
||||
- Implement a fair usage of shared runners
|
||||
- Remove project notification settings associated with deleted projects
|
||||
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects
|
||||
- Add a metric for the number of new Redis connections created by a transaction
|
||||
- Fix Error 500 when viewing a blob with binary characters after the 1024-byte mark
|
||||
- Redesign navigation for project pages
|
||||
- Added shortcut 'y' for copying a files content hash URL #14470
|
||||
- Fix groups API to list only user's accessible projects
|
||||
- Fix horizontal scrollbar for long commit message.
|
||||
- Add Environments and Deployments
|
||||
- Redesign account and email confirmation emails
|
||||
- Don't fail builds for projects that are deleted
|
||||
- Support Docker Registry manifest v1
|
||||
- `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
|
||||
- Bump nokogiri to 1.6.8
|
||||
- Use gitlab-shell v3.0.0
|
||||
|
@ -33,14 +45,20 @@ v 8.9.0 (unreleased)
|
|||
- Add DB index on users.state
|
||||
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
|
||||
- Changed the Slack build message to use the singular duration if necessary (Aran Koning)
|
||||
- Fix race condition on merge when build succeeds
|
||||
- Links from a wiki page to other wiki pages should be rewritten as expected
|
||||
- Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos)
|
||||
- Added navigation shortcuts to the project pipelines, milestones, builds and forks page. !4393
|
||||
- Fix issues filter when ordering by milestone
|
||||
- Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3
|
||||
- Bamboo Service: Fix missing credentials & URL handling when base URL contains a path (Benjamin Schmid)
|
||||
- TeamCity Service: Fix URL handling when base URL contains a path
|
||||
- Todos will display target state if issuable target is 'Closed' or 'Merged'
|
||||
- Fix bug when sorting issues by milestone due date and filtering by two or more labels
|
||||
- Add support for using Yubikeys (U2F) for two-factor authentication
|
||||
- Link to blank group icon doesn't throw a 404 anymore
|
||||
- Remove 'main language' feature
|
||||
- Toggle whitespace button now available for compare branches diffs #17881
|
||||
- Pipelines can be canceled only when there are running builds
|
||||
- Use downcased path to container repository as this is expected path by Docker
|
||||
- Projects pending deletion will render a 404 page
|
||||
|
@ -52,34 +70,59 @@ v 8.9.0 (unreleased)
|
|||
- Use Knapsack only in CI environment
|
||||
- Cache project build count in sidebar nav
|
||||
- Add milestone expire date to the right sidebar
|
||||
- Manually mark a issue or merge request as a todo
|
||||
- Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing
|
||||
- Reduce number of queries needed to render issue labels in the sidebar
|
||||
- Improve error handling importing projects
|
||||
- Remove duplicated notification settings
|
||||
- Put project Files and Commits tabs under Code tab
|
||||
- Decouple global notification level from user model
|
||||
- Replace Colorize with Rainbow for coloring console output in Rake tasks.
|
||||
- Add workhorse controller and API helpers
|
||||
- An indicator is now displayed at the top of the comment field for confidential issues.
|
||||
- Show categorised search queries in the search autocomplete
|
||||
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
|
||||
- Improve issuables APIs performance when accessing notes !4471
|
||||
- External links now open in a new tab
|
||||
- Prevent default actions of disabled buttons and links
|
||||
- Markdown editor now correctly resets the input value on edit cancellation !4175
|
||||
- Toggling a task list item in a issue/mr description does not creates a Todo for mentions
|
||||
- Improved UX of date pickers on issue & milestone forms
|
||||
- Cache on the database if a project has an active external issue tracker.
|
||||
- Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav
|
||||
- All classes in the Banzai::ReferenceParser namespace are now instrumented
|
||||
- Remove deprecated issues_tracker and issues_tracker_id from project model
|
||||
- Allow users to create confidential issues in private projects
|
||||
- Measure CPU time for instrumented methods
|
||||
- Instrument private methods and private instance methods by default instead just public methods
|
||||
- Only show notes through JSON on confidential issues that the user has access to
|
||||
- Updated the allocations Gem to version 1.0.5
|
||||
- The background sampler now ignores classes without names
|
||||
- Update design for `Close` buttons
|
||||
- New custom icons for navigation
|
||||
- Horizontally scrolling navigation on project, group, and profile settings pages
|
||||
- Hide global side navigation by default
|
||||
- Fix project Star/Unstar project button tooltip
|
||||
- Remove tanuki logo from side navigation; center on top nav
|
||||
- Include user relationships when retrieving award_emoji
|
||||
- Various associations are now eager loaded when parsing issue references to reduce the number of queries executed
|
||||
- Set inverse_of for Project/Service association to reduce the number of queries
|
||||
|
||||
v 8.8.5 (unreleased)
|
||||
- Ensure branch cleanup regardless of whether the GitHub import process succeeds
|
||||
- Fix todos page throwing errors when you have a project pending deletion
|
||||
- Reduce number of SQL queries when rendering user references
|
||||
- Import GitHub repositories respecting the API rate limit
|
||||
- Fix importer for GitHub comments on diff
|
||||
- Disable Webhooks before proceeding with the GitHub import
|
||||
- Fix incremental trace upload API when using multi-byte UTF-8 chars in trace
|
||||
v 8.8.5
|
||||
- Import GitHub repositories respecting the API rate limit !4166
|
||||
- Fix todos page throwing errors when you have a project pending deletion !4300
|
||||
- Disable Webhooks before proceeding with the GitHub import !4470
|
||||
- Fix importer for GitHub comments on diff !4488
|
||||
- Adjust the SAML control flow to allow LDAP identities to be added to an existing SAML user !4498
|
||||
- Fix incremental trace upload API when using multi-byte UTF-8 chars in trace !4541
|
||||
- Prevent unauthorized access for projects build traces
|
||||
- Forbid scripting for wiki files
|
||||
- Only show notes through JSON on confidential issues that the user has access to
|
||||
|
||||
v 8.8.4
|
||||
- Fix LDAP-based login for users with 2FA enabled. !4493
|
||||
- Added descriptions to notification settings dropdown
|
||||
- Due date can be removed from milestones
|
||||
|
||||
v 8.8.3
|
||||
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
|
||||
|
@ -195,6 +238,9 @@ v 8.8.0
|
|||
|
||||
v 8.7.7
|
||||
- Fix import by `Any Git URL` broken if the URL contains a space
|
||||
- Prevent unauthorized access to other projects build traces
|
||||
- Forbid scripting for wiki files
|
||||
- Only show notes through JSON on confidential issues that the user has access to
|
||||
|
||||
v 8.7.6
|
||||
- Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
|
||||
|
@ -357,6 +403,11 @@ v 8.7.0
|
|||
- Add RAW build trace output and button on build page
|
||||
- Add incremental build trace update into CI API
|
||||
|
||||
v 8.6.9
|
||||
- Prevent unauthorized access to other projects build traces
|
||||
- Forbid scripting for wiki files
|
||||
- Only show notes through JSON on confidential issues that the user has access to
|
||||
|
||||
v 8.6.8
|
||||
- Prevent privilege escalation via "impersonate" feature
|
||||
- Prevent privilege escalation via notes API
|
||||
|
@ -511,6 +562,10 @@ v 8.6.0
|
|||
- Trigger a todo for mentions on commits page
|
||||
- Let project owners and admins soft delete issues and merge requests
|
||||
|
||||
v 8.5.13
|
||||
- Prevent unauthorized access to other projects build traces
|
||||
- Forbid scripting for wiki files
|
||||
|
||||
v 8.5.12
|
||||
- Prevent privilege escalation via "impersonate" feature
|
||||
- Prevent privilege escalation via notes API
|
||||
|
@ -672,6 +727,10 @@ v 8.5.0
|
|||
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
|
||||
- Add Todos
|
||||
|
||||
v 8.4.11
|
||||
- Prevent unauthorized access to other projects build traces
|
||||
- Forbid scripting for wiki files
|
||||
|
||||
v 8.4.10
|
||||
- Prevent privilege escalation via "impersonate" feature
|
||||
- Prevent privilege escalation via notes API
|
||||
|
@ -808,6 +867,10 @@ v 8.4.0
|
|||
- Add IP check against DNSBLs at account sign-up
|
||||
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
|
||||
|
||||
v 8.3.10
|
||||
- Prevent unauthorized access to other projects build traces
|
||||
- Forbid scripting for wiki files
|
||||
|
||||
v 8.3.9
|
||||
- Prevent privilege escalation via "impersonate" feature
|
||||
- Prevent privilege escalation via notes API
|
||||
|
@ -926,6 +989,10 @@ v 8.3.0
|
|||
- Expose Git's version in the admin area
|
||||
- Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye)
|
||||
|
||||
v 8.2.6
|
||||
- Prevent unauthorized access to other projects build traces
|
||||
- Forbid scripting for wiki files
|
||||
|
||||
v 8.2.5
|
||||
- Prevent privilege escalation via "impersonate" feature
|
||||
- Prevent privilege escalation via notes API
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -210,6 +210,9 @@ gem 'mousetrap-rails', '~> 1.4.6'
|
|||
# Detect and convert string character encoding
|
||||
gem 'charlock_holmes', '~> 0.7.3'
|
||||
|
||||
# Parse duration
|
||||
gem 'chronic_duration', '~> 0.10.6'
|
||||
|
||||
gem "sass-rails", '~> 5.0.0'
|
||||
gem "coffee-rails", '~> 4.1.0'
|
||||
gem "uglifier", '~> 2.7.2'
|
||||
|
@ -224,7 +227,6 @@ gem 'gon', '~> 6.0.1'
|
|||
gem 'jquery-atwho-rails', '~> 1.3.2'
|
||||
gem 'jquery-rails', '~> 4.1.0'
|
||||
gem 'jquery-ui-rails', '~> 5.0.0'
|
||||
gem 'raphael-rails', '~> 2.1.2'
|
||||
gem 'request_store', '~> 1.3.0'
|
||||
gem 'select2-rails', '~> 3.5.9'
|
||||
gem 'virtus', '~> 1.0.1'
|
||||
|
@ -245,7 +247,7 @@ end
|
|||
|
||||
group :development do
|
||||
gem "foreman"
|
||||
gem 'brakeman', '~> 3.2.0', require: false
|
||||
gem 'brakeman', '~> 3.3.0', require: false
|
||||
|
||||
gem 'letter_opener_web', '~> 1.3.0'
|
||||
gem 'quiet_assets', '~> 1.0.2'
|
||||
|
|
40
Gemfile.lock
40
Gemfile.lock
|
@ -50,7 +50,7 @@ GEM
|
|||
after_commit_queue (1.3.0)
|
||||
activerecord (>= 3.0)
|
||||
akismet (2.0.0)
|
||||
allocations (1.0.4)
|
||||
allocations (1.0.5)
|
||||
arel (6.0.3)
|
||||
asana (0.4.0)
|
||||
faraday (~> 0.9)
|
||||
|
@ -97,16 +97,7 @@ GEM
|
|||
bootstrap-sass (3.3.6)
|
||||
autoprefixer-rails (>= 5.2.1)
|
||||
sass (>= 3.3.4)
|
||||
brakeman (3.2.1)
|
||||
erubis (~> 2.6)
|
||||
haml (>= 3.0, < 5.0)
|
||||
highline (>= 1.6.20, < 2.0)
|
||||
ruby2ruby (~> 2.3.0)
|
||||
ruby_parser (~> 3.8.1)
|
||||
safe_yaml (>= 1.0)
|
||||
sass (~> 3.0)
|
||||
slim (>= 1.3.6, < 4.0)
|
||||
terminal-table (~> 1.4)
|
||||
brakeman (3.3.2)
|
||||
browser (2.0.3)
|
||||
builder (3.2.2)
|
||||
bullet (5.0.0)
|
||||
|
@ -133,6 +124,8 @@ GEM
|
|||
mime-types (>= 1.16)
|
||||
cause (0.1)
|
||||
charlock_holmes (0.7.3)
|
||||
chronic_duration (0.10.6)
|
||||
numerizer (~> 0.1.1)
|
||||
chunky_png (1.3.5)
|
||||
cliver (0.3.2)
|
||||
coderay (1.1.0)
|
||||
|
@ -284,7 +277,7 @@ GEM
|
|||
posix-spawn (~> 0.3)
|
||||
gitlab_emoji (0.3.1)
|
||||
gemojione (~> 2.2, >= 2.2.1)
|
||||
gitlab_git (10.1.0)
|
||||
gitlab_git (10.1.3)
|
||||
activesupport (~> 4.0)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
github-linguist (~> 4.7.0)
|
||||
|
@ -338,7 +331,6 @@ GEM
|
|||
hashie (3.4.3)
|
||||
health_check (1.5.1)
|
||||
rails (>= 2.3.0)
|
||||
highline (1.7.8)
|
||||
hipchat (1.5.2)
|
||||
httparty
|
||||
mimemagic
|
||||
|
@ -408,7 +400,7 @@ GEM
|
|||
mime-types (>= 1.16, < 4)
|
||||
mail_room (0.7.0)
|
||||
method_source (0.8.2)
|
||||
mime-types (2.99.1)
|
||||
mime-types (2.99.2)
|
||||
mimemagic (0.3.0)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.7.0)
|
||||
|
@ -424,6 +416,7 @@ GEM
|
|||
nokogiri (1.6.8)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
pkg-config (~> 1.1.7)
|
||||
numerizer (0.1.1)
|
||||
oauth (0.4.7)
|
||||
oauth2 (1.0.0)
|
||||
faraday (>= 0.8, < 0.10)
|
||||
|
@ -563,7 +556,6 @@ GEM
|
|||
rainbow (2.1.0)
|
||||
raindrops (0.15.0)
|
||||
rake (10.5.0)
|
||||
raphael-rails (2.1.2)
|
||||
rb-fsevent (0.9.6)
|
||||
rb-inotify (0.9.5)
|
||||
ffi (>= 0.5.0)
|
||||
|
@ -642,10 +634,7 @@ GEM
|
|||
ruby-saml (1.1.2)
|
||||
nokogiri (>= 1.5.10)
|
||||
uuid (~> 2.3)
|
||||
ruby2ruby (2.3.0)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.0)
|
||||
ruby_parser (3.8.1)
|
||||
ruby_parser (3.8.2)
|
||||
sexp_processor (~> 4.1)
|
||||
rubyntlm (0.5.2)
|
||||
rubypants (0.2.0)
|
||||
|
@ -655,7 +644,7 @@ GEM
|
|||
safe_yaml (1.0.4)
|
||||
sanitize (2.1.0)
|
||||
nokogiri (>= 1.4.4)
|
||||
sass (3.4.21)
|
||||
sass (3.4.22)
|
||||
sass-rails (5.0.4)
|
||||
railties (>= 4.0.0, < 5.0)
|
||||
sass (~> 3.1)
|
||||
|
@ -704,9 +693,6 @@ GEM
|
|||
tilt (>= 1.3, < 3)
|
||||
six (0.2.0)
|
||||
slack-notifier (1.2.1)
|
||||
slim (3.0.6)
|
||||
temple (~> 0.7.3)
|
||||
tilt (>= 1.3.3, < 2.1)
|
||||
slop (3.6.0)
|
||||
spinach (0.8.10)
|
||||
colorize
|
||||
|
@ -747,10 +733,8 @@ GEM
|
|||
railties (>= 3.2.5, < 6)
|
||||
teaspoon-jasmine (2.2.0)
|
||||
teaspoon (>= 1.0.0)
|
||||
temple (0.7.6)
|
||||
term-ansicolor (1.3.2)
|
||||
tins (~> 1.0)
|
||||
terminal-table (1.5.2)
|
||||
test_after_commit (0.4.2)
|
||||
activerecord (>= 3.2)
|
||||
thin (1.6.4)
|
||||
|
@ -759,7 +743,7 @@ GEM
|
|||
rack (~> 1.0)
|
||||
thor (0.19.1)
|
||||
thread_safe (0.3.5)
|
||||
tilt (2.0.2)
|
||||
tilt (2.0.5)
|
||||
timecop (0.8.1)
|
||||
timfel-krb5-auth (0.8.3)
|
||||
tinder (1.10.1)
|
||||
|
@ -848,7 +832,7 @@ DEPENDENCIES
|
|||
better_errors (~> 1.0.1)
|
||||
binding_of_caller (~> 0.7.2)
|
||||
bootstrap-sass (~> 3.3.0)
|
||||
brakeman (~> 3.2.0)
|
||||
brakeman (~> 3.3.0)
|
||||
browser (~> 2.0.3)
|
||||
bullet
|
||||
bundler-audit
|
||||
|
@ -857,6 +841,7 @@ DEPENDENCIES
|
|||
capybara-screenshot (~> 1.0.0)
|
||||
carrierwave (~> 0.10.0)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
chronic_duration (~> 0.10.6)
|
||||
coffee-rails (~> 4.1.0)
|
||||
connection_pool (~> 2.0)
|
||||
coveralls (~> 0.8.2)
|
||||
|
@ -952,7 +937,6 @@ DEPENDENCIES
|
|||
rails (= 4.2.6)
|
||||
rails-deprecated_sanitizer (~> 1.0.3)
|
||||
rainbow (~> 2.1.0)
|
||||
raphael-rails (~> 2.1.2)
|
||||
rblineprof
|
||||
rdoc (~> 3.6)
|
||||
recaptcha (~> 3.0)
|
||||
|
|
|
@ -42,10 +42,10 @@ class @LabelManager
|
|||
$from = @prioritizedLabels
|
||||
|
||||
if $from.find('li').length is 1
|
||||
$from.find('.empty-message').show()
|
||||
$from.find('.empty-message').removeClass('hidden')
|
||||
|
||||
if not $target.find('li').length
|
||||
$target.find('.empty-message').hide()
|
||||
$target.find('.empty-message').addClass('hidden')
|
||||
|
||||
$label.detach().appendTo($target)
|
||||
|
||||
|
@ -54,6 +54,9 @@ class @LabelManager
|
|||
|
||||
if action is 'remove'
|
||||
xhr = $.ajax url: url, type: 'DELETE'
|
||||
|
||||
# Restore empty message
|
||||
$from.find('.empty-message').removeClass('hidden') unless $from.find('li').length
|
||||
else
|
||||
xhr = @savePrioritySort($label, action)
|
||||
|
||||
|
|
|
@ -32,10 +32,6 @@
|
|||
#= require bootstrap/tooltip
|
||||
#= require bootstrap/popover
|
||||
#= require select2
|
||||
#= require raphael
|
||||
#= require g.raphael
|
||||
#= require g.bar
|
||||
#= require branch-graph
|
||||
#= require ace/ace
|
||||
#= require ace/ext-searchbox
|
||||
#= require underscore
|
||||
|
@ -125,9 +121,10 @@ window.onload = ->
|
|||
setTimeout shiftWindow, 100
|
||||
|
||||
$ ->
|
||||
gl.utils.preventDisabledButtons()
|
||||
bootstrapBreakpoint = bp.getBreakpointSize()
|
||||
|
||||
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
|
||||
$(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
|
||||
|
||||
# Click a .js-select-on-focus field, select the contents
|
||||
$(".js-select-on-focus").on "focusin", ->
|
||||
|
@ -257,3 +254,31 @@ $ ->
|
|||
gl.awardsHandler = new AwardsHandler()
|
||||
checkInitialSidebarSize()
|
||||
new Aside()
|
||||
|
||||
# Sidenav pinning
|
||||
if $(window).width() < 1440 and $.cookie('pin_nav') is 'true'
|
||||
$.cookie('pin_nav', 'false')
|
||||
$('.page-with-sidebar')
|
||||
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
|
||||
.removeClass('page-sidebar-pinned')
|
||||
$('.navbar-fixed-top').removeClass('header-pinned-nav')
|
||||
|
||||
$(document)
|
||||
.off 'click', '.js-nav-pin'
|
||||
.on 'click', '.js-nav-pin', (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
$(this).toggleClass 'is-active'
|
||||
|
||||
if $.cookie('pin_nav') is 'true'
|
||||
$.cookie 'pin_nav', 'false'
|
||||
$('.page-with-sidebar')
|
||||
.removeClass('page-sidebar-pinned')
|
||||
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
|
||||
$('.navbar-fixed-top')
|
||||
.removeClass('header-pinned-nav')
|
||||
.toggleClass('header-collapsed header-expanded')
|
||||
else
|
||||
$.cookie 'pin_nav', 'true'
|
||||
$('.page-with-sidebar').addClass('page-sidebar-pinned')
|
||||
$('.navbar-fixed-top').addClass('header-pinned-nav')
|
||||
|
|
|
@ -40,7 +40,7 @@ class @AwardsHandler
|
|||
$menu = $ '.emoji-menu'
|
||||
|
||||
if $addBtn.hasClass 'js-note-emoji'
|
||||
$addBtn.parents('.note').find('.js-awards-block').addClass 'current'
|
||||
$addBtn.closest('.note').find('.js-awards-block').addClass 'current'
|
||||
else
|
||||
$addBtn.closest('.js-awards-block').addClass 'current'
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ class @CiBuild
|
|||
.off 'resize.build'
|
||||
.on 'resize.build', @hideSidebar
|
||||
|
||||
@updateArtifactRemoveDate()
|
||||
|
||||
if $('#build-trace').length
|
||||
@getInitialBuildTrace()
|
||||
@initScrollButtonAffix()
|
||||
|
@ -103,3 +105,10 @@ class @CiBuild
|
|||
$('.js-build-sidebar')
|
||||
.removeClass 'right-sidebar-collapsed'
|
||||
.addClass 'right-sidebar-expanded'
|
||||
|
||||
updateArtifactRemoveDate: ->
|
||||
$date = $('.js-artifacts-remove')
|
||||
|
||||
if $date.length
|
||||
date = $date.text()
|
||||
$date.text $.timefor(new Date(date), ' ')
|
||||
|
|
|
@ -29,6 +29,7 @@ class Dispatcher
|
|||
new Todos()
|
||||
when 'projects:milestones:new', 'projects:milestones:edit'
|
||||
new ZenMode()
|
||||
new DueDateSelect()
|
||||
new GLForm($('.milestone-form'))
|
||||
when 'groups:milestones:new'
|
||||
new ZenMode()
|
||||
|
@ -53,9 +54,13 @@ class Dispatcher
|
|||
new Diff()
|
||||
shortcut_handler = new ShortcutsIssuable(true)
|
||||
new ZenMode()
|
||||
new MergedButtons()
|
||||
when 'projects:merge_requests:commits', 'projects:merge_requests:builds'
|
||||
new MergedButtons()
|
||||
when "projects:merge_requests:diffs"
|
||||
new Diff()
|
||||
new ZenMode()
|
||||
new MergedButtons()
|
||||
when 'projects:merge_requests:index'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
Issuable.init()
|
||||
|
@ -68,9 +73,7 @@ class Dispatcher
|
|||
new Diff()
|
||||
new ZenMode()
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
when 'projects:commits:show'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
when 'projects:activity'
|
||||
when 'projects:commits:show', 'projects:activity'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
when 'projects:show'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
|
@ -96,6 +99,7 @@ class Dispatcher
|
|||
when 'projects:blob:show', 'projects:blame:show'
|
||||
new LineHighlighter()
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
new ShortcutsBlob true
|
||||
when 'projects:labels:new', 'projects:labels:edit'
|
||||
new Labels()
|
||||
when 'projects:labels:index'
|
||||
|
@ -129,15 +133,11 @@ class Dispatcher
|
|||
new Project()
|
||||
new ProjectAvatar()
|
||||
switch path[1]
|
||||
when 'compare'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
when 'edit'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
new ProjectNew()
|
||||
when 'new'
|
||||
when 'new', 'show'
|
||||
new ProjectNew()
|
||||
when 'show'
|
||||
new ProjectShow()
|
||||
when 'wikis'
|
||||
new Wikis()
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
|
@ -146,9 +146,9 @@ class Dispatcher
|
|||
when 'snippets'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
new ZenMode() if path[2] == 'show'
|
||||
when 'labels', 'graphs'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
|
||||
when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \
|
||||
'milestones', 'project_members', 'deploy_keys', 'builds', \
|
||||
'hooks', 'services', 'protected_branches'
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
|
||||
# If we haven't installed a custom shortcut handler, install the default one
|
||||
|
|
|
@ -1,5 +1,21 @@
|
|||
class @DueDateSelect
|
||||
constructor: ->
|
||||
# Milestone edit/new form
|
||||
$datePicker = $('.datepicker')
|
||||
|
||||
if $datePicker.length
|
||||
$dueDate = $('#milestone_due_date')
|
||||
$datePicker.datepicker
|
||||
dateFormat: 'yy-mm-dd'
|
||||
onSelect: (dateText, inst) ->
|
||||
$dueDate.val(dateText)
|
||||
.datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()))
|
||||
|
||||
$('.js-clear-due-date').on 'click', (e) ->
|
||||
e.preventDefault()
|
||||
$.datepicker._clearDate($datePicker)
|
||||
|
||||
# Issuable sidebar
|
||||
$loading = $('.js-issuable-update .due_date')
|
||||
.find('.block-loading')
|
||||
.hide()
|
||||
|
@ -32,7 +48,7 @@ class @DueDateSelect
|
|||
date = new Date value.replace(new RegExp('-', 'g'), ',')
|
||||
mediumDate = $.datepicker.formatDate 'M d, yy', date
|
||||
else
|
||||
mediumDate = 'None'
|
||||
mediumDate = 'No due date'
|
||||
|
||||
data = {}
|
||||
data[abilityName] = {}
|
||||
|
@ -50,7 +66,8 @@ class @DueDateSelect
|
|||
$selectbox.hide()
|
||||
$value.css('display', '')
|
||||
|
||||
$valueContent.html(mediumDate)
|
||||
cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value'
|
||||
$valueContent.html("<span class='#{cssClass}'>#{mediumDate}</span>")
|
||||
$sidebarValue.html(mediumDate)
|
||||
|
||||
if value isnt ''
|
||||
|
|
|
@ -56,13 +56,6 @@ issuable_created = false
|
|||
Issuable.filterResults $('.filter-form')
|
||||
$('.js-label-select').trigger('update.label')
|
||||
|
||||
toggleLabelFilters: ->
|
||||
$filteredLabels = $('.filtered-labels')
|
||||
if $filteredLabels.find('.label-row').length > 0
|
||||
$filteredLabels.removeClass('hidden')
|
||||
else
|
||||
$filteredLabels.addClass('hidden')
|
||||
|
||||
filterResults: (form) =>
|
||||
formData = form.serialize()
|
||||
|
||||
|
@ -71,58 +64,16 @@ issuable_created = false
|
|||
issuesUrl = formAction
|
||||
issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
|
||||
issuesUrl += formData
|
||||
$.ajax
|
||||
type: 'GET'
|
||||
url: formAction
|
||||
data: formData
|
||||
complete: ->
|
||||
$('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
|
||||
success: (data) ->
|
||||
$('.issues-holder, .merge-requests-holder').html(data.html)
|
||||
# Change url so if user reload a page - search results are saved
|
||||
history.replaceState {page: issuesUrl}, document.title, issuesUrl
|
||||
Issuable.reload()
|
||||
Issuable.updateStateFilters()
|
||||
$filteredLabels = $('.filtered-labels')
|
||||
|
||||
if typeof Issuable.labelRow is 'function'
|
||||
$filteredLabels.html(Issuable.labelRow(data))
|
||||
|
||||
Issuable.toggleLabelFilters()
|
||||
|
||||
dataType: "json"
|
||||
|
||||
reload: ->
|
||||
if Issuable.created
|
||||
Issuable.initChecks()
|
||||
|
||||
$('#filter_issue_search').val($('#issue_search').val())
|
||||
Turbolinks.visit(issuesUrl);
|
||||
|
||||
initChecks: ->
|
||||
$('.check_all_issues').on 'click', ->
|
||||
$('.check_all_issues').off('click').on('click', ->
|
||||
$('.selected_issue').prop('checked', @checked)
|
||||
Issuable.checkChanged()
|
||||
)
|
||||
|
||||
$('.selected_issue').on 'change', Issuable.checkChanged
|
||||
|
||||
updateStateFilters: ->
|
||||
stateFilters = $('.issues-state-filters, .dropdown-menu-sort')
|
||||
newParams = {}
|
||||
paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search', 'issue_search']
|
||||
|
||||
for paramKey in paramKeys
|
||||
newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
|
||||
|
||||
if stateFilters.length
|
||||
stateFilters.find('a').each ->
|
||||
initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
|
||||
labelNameValues = gl.utils.getParameterValues('label_name[]')
|
||||
if labelNameValues
|
||||
labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
|
||||
newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
|
||||
else
|
||||
newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
|
||||
$(this).attr 'href', newUrl
|
||||
$('.selected_issue').off('change').on('change', Issuable.checkChanged)
|
||||
|
||||
checkChanged: ->
|
||||
checked_issues = $('.selected_issue:checked')
|
||||
|
|
|
@ -102,6 +102,10 @@ class @IssuableForm
|
|||
return {
|
||||
results: data
|
||||
}
|
||||
data: (query) ->
|
||||
{
|
||||
search: query
|
||||
}
|
||||
formatResult: (project) ->
|
||||
project.name_with_namespace
|
||||
formatSelection: (project) ->
|
||||
|
|
|
@ -9,6 +9,9 @@ class @IssuableBulkActions
|
|||
|
||||
@bindEvents()
|
||||
|
||||
# Fixes bulk-assign not working when navigating through pages
|
||||
Issuable.initChecks();
|
||||
|
||||
getElement: (selector) ->
|
||||
@container.find selector
|
||||
|
||||
|
@ -97,13 +100,22 @@ class @IssuableBulkActions
|
|||
$labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
|
||||
|
||||
$labels.each (k, label) ->
|
||||
labelIds.push $(label).val() if label
|
||||
labelIds.push parseInt($(label).val()) if label
|
||||
|
||||
labelIds
|
||||
|
||||
###*
|
||||
* Just an alias of @getUnmarkedIndeterminedLabels
|
||||
* @return {Array} Array of labels
|
||||
* Returns Label IDs that will be removed from issue selection
|
||||
* @return {Array} Array of labels IDs
|
||||
###
|
||||
getLabelsToRemove: ->
|
||||
@getUnmarkedIndeterminedLabels()
|
||||
result = []
|
||||
indeterminatedLabels = @getUnmarkedIndeterminedLabels()
|
||||
labelsToApply = @getLabelsToApply()
|
||||
|
||||
indeterminatedLabels.map (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
|
||||
result.push(id) if labelsToApply.indexOf(id) is -1
|
||||
|
||||
result
|
||||
|
|
|
@ -39,7 +39,7 @@ class @LabelsSelect
|
|||
</a>
|
||||
<% }); %>'
|
||||
)
|
||||
labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
|
||||
labelNoneHTMLTemplate = '<span class="no-value">None</span>'
|
||||
|
||||
if newLabelField.length
|
||||
|
||||
|
@ -145,7 +145,7 @@ class @LabelsSelect
|
|||
template = labelHTMLTemplate(data)
|
||||
labelCount = data.labels.length
|
||||
else
|
||||
template = labelNoneHTMLTemplate()
|
||||
template = labelNoneHTMLTemplate
|
||||
$value
|
||||
.removeAttr('style')
|
||||
.html(template)
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
class @LayoutNav
|
||||
$ ->
|
||||
$('.fade-left').addClass('end-scroll')
|
||||
$('.scrolling-tabs').on 'scroll', (event) ->
|
||||
$this = $(this)
|
||||
$el = $(event.target)
|
||||
currentPosition = $this.scrollLeft()
|
||||
size = bp.getBreakpointSize()
|
||||
controlBtnWidth = $('.controls').width()
|
||||
maxPosition = $this.get(0).scrollWidth - $this.parent().width()
|
||||
maxPosition += controlBtnWidth if size isnt 'xs' and $('.nav-control').length
|
||||
hideEndFade = ($scrollingTabs) ->
|
||||
$scrollingTabs.each ->
|
||||
$this = $(@)
|
||||
|
||||
$el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
|
||||
$el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
|
||||
$this
|
||||
.find('.fade-right')
|
||||
.toggleClass('end-scroll', $this.width() is $this.prop('scrollWidth'))
|
||||
|
||||
$ ->
|
||||
$('.fade-left').addClass('end-scroll')
|
||||
|
||||
hideEndFade($('.scrolling-tabs'))
|
||||
|
||||
$(window)
|
||||
.off 'resize.nav'
|
||||
.on 'resize.nav', ->
|
||||
hideEndFade($('.scrolling-tabs'))
|
||||
|
||||
$('.scrolling-tabs').on 'scroll', (event) ->
|
||||
$this = $(this)
|
||||
currentPosition = $this.scrollLeft()
|
||||
maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
|
||||
|
||||
$this.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
|
||||
$this.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
|
||||
|
|
|
@ -1,5 +1,46 @@
|
|||
((w) ->
|
||||
|
||||
w.gl or= {}
|
||||
w.gl.utils or= {}
|
||||
|
||||
w.gl.utils.isInGroupsPage = ->
|
||||
|
||||
return $('body').data('page').split(':')[0] is 'groups'
|
||||
|
||||
|
||||
w.gl.utils.isInProjectPage = ->
|
||||
|
||||
return $('body').data('page').split(':')[0] is 'projects'
|
||||
|
||||
|
||||
w.gl.utils.getProjectSlug = ->
|
||||
|
||||
return if @isInProjectPage() then $('body').data 'project' else null
|
||||
|
||||
|
||||
w.gl.utils.getGroupSlug = ->
|
||||
|
||||
return if @isInGroupsPage() then $('body').data 'group' else null
|
||||
|
||||
|
||||
|
||||
gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
|
||||
|
||||
$tooltipEl
|
||||
.tooltip 'destroy'
|
||||
.attr 'title', newTitle
|
||||
.tooltip 'fixTitle'
|
||||
|
||||
|
||||
gl.utils.preventDisabledButtons = ->
|
||||
|
||||
$('.btn').click (e) ->
|
||||
if $(this).hasClass 'disabled'
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
return false
|
||||
|
||||
|
||||
jQuery.timefor = (time, suffix, expiredLabel) ->
|
||||
|
||||
return '' unless time
|
||||
|
|
|
@ -42,9 +42,3 @@ work = ->
|
|||
|
||||
$(document).on('page:fetch', start)
|
||||
$(document).on('page:change', stop)
|
||||
|
||||
$ ->
|
||||
# Make logo clickable as part of a workaround for Safari visited
|
||||
# link behaviour (See !2690).
|
||||
$('#logo').on 'click', ->
|
||||
Turbolinks.visit('/')
|
||||
|
|
30
app/assets/javascripts/merged_buttons.js.coffee
Normal file
30
app/assets/javascripts/merged_buttons.js.coffee
Normal file
|
@ -0,0 +1,30 @@
|
|||
class @MergedButtons
|
||||
constructor: ->
|
||||
@$removeBranchWidget = $('.remove_source_branch_widget')
|
||||
@$removeBranchProgress = $('.remove_source_branch_in_progress')
|
||||
@$removeBranchFailed = $('.remove_source_branch_widget.failed')
|
||||
|
||||
@cleanEventListeners()
|
||||
@initEventListeners()
|
||||
|
||||
cleanEventListeners: ->
|
||||
$(document).off 'click', '.remove_source_branch'
|
||||
$(document).off 'ajax:success', '.remove_source_branch'
|
||||
$(document).off 'ajax:error', '.remove_source_branch'
|
||||
|
||||
initEventListeners: ->
|
||||
$(document).on 'click', '.remove_source_branch', @removeSourceBranch
|
||||
$(document).on 'ajax:success', '.remove_source_branch', @removeBranchSuccess
|
||||
$(document).on 'ajax:error', '.remove_source_branch', @removeBranchError
|
||||
|
||||
removeSourceBranch: =>
|
||||
@$removeBranchWidget.hide()
|
||||
@$removeBranchProgress.show()
|
||||
|
||||
removeBranchSuccess: ->
|
||||
location.reload()
|
||||
|
||||
removeBranchError: ->
|
||||
@$removeBranchWidget.hide()
|
||||
@$removeBranchProgress.hide()
|
||||
@$removeBranchFailed.show()
|
|
@ -24,14 +24,10 @@ class @MilestoneSelect
|
|||
|
||||
if issueUpdateURL
|
||||
milestoneLinkTemplate = _.template(
|
||||
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>">
|
||||
<span class="has-tooltip" data-container="body" title="<%= remaining %>">
|
||||
<%= _.escape(title) %>
|
||||
</span>
|
||||
</a>'
|
||||
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>" class="bold has-tooltip" data-container="body" title="<%= remaining %>"><%= _.escape(title) %></a>'
|
||||
)
|
||||
|
||||
milestoneLinkNoneTemplate = '<div class="light">None</div>'
|
||||
milestoneLinkNoneTemplate = '<span class="no-value">None</span>'
|
||||
|
||||
collapsedSidebarLabelTemplate = _.template(
|
||||
'<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left">
|
||||
|
@ -116,7 +112,7 @@ class @MilestoneSelect
|
|||
.val()
|
||||
data = {}
|
||||
data[abilityName] = {}
|
||||
data[abilityName].milestone_id = selected
|
||||
data[abilityName].milestone_id = if selected? then selected else null
|
||||
$loading
|
||||
.fadeIn()
|
||||
$dropdown.trigger('loading.gl.dropdown')
|
||||
|
|
20
app/assets/javascripts/network/application.js.coffee
Normal file
20
app/assets/javascripts/network/application.js.coffee
Normal file
|
@ -0,0 +1,20 @@
|
|||
# This is a manifest file that'll be compiled into including all the files listed below.
|
||||
# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
|
||||
# be included in the compiled file accessible from http://example.com/assets/application.js
|
||||
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
||||
# the compiled file.
|
||||
#
|
||||
#= require raphael
|
||||
#= require g.raphael
|
||||
#= require g.bar
|
||||
#= require_tree .
|
||||
|
||||
$ ->
|
||||
network_graph = new Network({
|
||||
url: $(".network-graph").attr('data-url'),
|
||||
commit_url: $(".network-graph").attr('data-commit-url'),
|
||||
ref: $(".network-graph").attr('data-ref'),
|
||||
commit_id: $(".network-graph").attr('data-commit-id')
|
||||
})
|
||||
|
||||
new ShortcutsNetwork(network_graph.branch_graph)
|
|
@ -115,12 +115,14 @@ class @Notes
|
|||
, @pollingInterval
|
||||
|
||||
refresh: =>
|
||||
return if @refreshing is true
|
||||
@refreshing = true
|
||||
if not document.hidden and document.URL.indexOf(@noteable_url) is 0
|
||||
@getContent()
|
||||
|
||||
getContent: ->
|
||||
return if @refreshing
|
||||
|
||||
@refreshing = true
|
||||
|
||||
$.ajax
|
||||
url: @notes_url
|
||||
data: "last_fetched_at=" + @last_fetched_at
|
||||
|
|
|
@ -43,6 +43,55 @@ class @Sidebar
|
|||
$('.right-sidebar')
|
||||
.hasClass('right-sidebar-collapsed'), { path: '/' })
|
||||
|
||||
$(document)
|
||||
.off 'click', '.js-issuable-todo'
|
||||
.on 'click', '.js-issuable-todo', @toggleTodo
|
||||
|
||||
toggleTodo: (e) =>
|
||||
$this = $(e.currentTarget)
|
||||
$todoLoading = $('.js-issuable-todo-loading')
|
||||
$btnText = $('.js-issuable-todo-text', $this)
|
||||
ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST'
|
||||
ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else ''
|
||||
|
||||
$.ajax(
|
||||
url: "#{$this.data('url')}#{ajaxUrlExtra}"
|
||||
type: ajaxType
|
||||
dataType: 'json'
|
||||
data:
|
||||
issuable_id: $this.data('issuable')
|
||||
issuable_type: $this.data('issuable-type')
|
||||
beforeSend: =>
|
||||
@beforeTodoSend($this, $todoLoading)
|
||||
).done (data) =>
|
||||
@todoUpdateDone(data, $this, $btnText, $todoLoading)
|
||||
|
||||
beforeTodoSend: ($btn, $todoLoading) ->
|
||||
$btn.disable()
|
||||
$todoLoading.removeClass 'hidden'
|
||||
|
||||
todoUpdateDone: (data, $btn, $btnText, $todoLoading) ->
|
||||
$todoPendingCount = $('.todos-pending-count')
|
||||
$todoPendingCount.text data.count
|
||||
|
||||
$btn.enable()
|
||||
$todoLoading.addClass 'hidden'
|
||||
|
||||
if data.count is 0
|
||||
$todoPendingCount.addClass 'hidden'
|
||||
else
|
||||
$todoPendingCount.removeClass 'hidden'
|
||||
|
||||
if data.todo?
|
||||
$btn
|
||||
.attr 'aria-label', $btn.data('mark-text')
|
||||
.attr 'data-id', data.todo.id
|
||||
$btnText.text $btn.data('mark-text')
|
||||
else
|
||||
$btn
|
||||
.attr 'aria-label', $btn.data('todo-text')
|
||||
.removeAttr 'data-id'
|
||||
$btnText.text $btn.data('todo-text')
|
||||
|
||||
sidebarDropdownLoading: (e) ->
|
||||
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
|
||||
|
@ -117,5 +166,3 @@ class @Sidebar
|
|||
|
||||
getBlock: (name) ->
|
||||
@sidebar.find(".block.#{name}")
|
||||
|
||||
|
||||
|
|
|
@ -67,8 +67,12 @@ class @SearchAutocomplete
|
|||
getData: (term, callback) ->
|
||||
_this = @
|
||||
|
||||
# Do not trigger request if input is empty
|
||||
return if @searchInput.val() is ''
|
||||
unless term
|
||||
if contents = @getCategoryContents()
|
||||
@searchInput.data('glDropdown').filter.options.callback contents
|
||||
@enableAutocomplete()
|
||||
|
||||
return
|
||||
|
||||
# Prevent multiple ajax calls
|
||||
return if @loadingSuggestions
|
||||
|
@ -122,6 +126,37 @@ class @SearchAutocomplete
|
|||
).always ->
|
||||
_this.loadingSuggestions = false
|
||||
|
||||
|
||||
getCategoryContents: ->
|
||||
|
||||
userId = gon.current_user_id
|
||||
{ utils, projectOptions, groupOptions, dashboardOptions } = gl
|
||||
|
||||
if utils.isInGroupsPage() and groupOptions
|
||||
options = groupOptions[utils.getGroupSlug()]
|
||||
|
||||
else if utils.isInProjectPage() and projectOptions
|
||||
options = projectOptions[utils.getProjectSlug()]
|
||||
|
||||
else if dashboardOptions
|
||||
options = dashboardOptions
|
||||
|
||||
{ issuesPath, mrPath, name } = options
|
||||
|
||||
items = [
|
||||
{ header: "#{name}" }
|
||||
{ text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" }
|
||||
{ text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" }
|
||||
'separator'
|
||||
{ text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" }
|
||||
{ text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" }
|
||||
]
|
||||
|
||||
items.splice 0, 1 unless name
|
||||
|
||||
return items
|
||||
|
||||
|
||||
serializeState: ->
|
||||
{
|
||||
# Search Criteria
|
||||
|
@ -209,6 +244,12 @@ class @SearchAutocomplete
|
|||
@isFocused = true
|
||||
@wrap.addClass('search-active')
|
||||
|
||||
@getData() if @getValue() is ''
|
||||
|
||||
|
||||
getValue: -> return @searchInput.val()
|
||||
|
||||
|
||||
onClearInputClick: (e) =>
|
||||
e.preventDefault()
|
||||
@searchInput.val('').focus()
|
||||
|
@ -229,6 +270,10 @@ class @SearchAutocomplete
|
|||
@locationBadgeEl.text(badgeText).show()
|
||||
@wrap.addClass('has-location-badge')
|
||||
|
||||
|
||||
hasLocationBadge: -> return @wrap.is '.has-location-badge'
|
||||
|
||||
|
||||
restoreOriginalState: ->
|
||||
inputs = Object.keys @originalState
|
||||
|
||||
|
@ -257,13 +302,14 @@ class @SearchAutocomplete
|
|||
|
||||
@getElement("##{input}").val('')
|
||||
|
||||
|
||||
removeLocationBadge: ->
|
||||
|
||||
@locationBadgeEl.hide()
|
||||
|
||||
# Reset state
|
||||
@resetSearchState()
|
||||
|
||||
@wrap.removeClass('has-location-badge')
|
||||
@disableAutocomplete()
|
||||
|
||||
|
||||
disableAutocomplete: ->
|
||||
@searchInput.addClass('disabled')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class @Shortcuts
|
||||
constructor: ->
|
||||
constructor: (skipResetBindings) ->
|
||||
@enabledHelp = []
|
||||
Mousetrap.reset()
|
||||
Mousetrap.reset() if not skipResetBindings
|
||||
Mousetrap.bind('?', @onToggleHelp)
|
||||
Mousetrap.bind('s', Shortcuts.focusSearch)
|
||||
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
|
||||
|
|
10
app/assets/javascripts/shortcuts_blob.coffee
Normal file
10
app/assets/javascripts/shortcuts_blob.coffee
Normal file
|
@ -0,0 +1,10 @@
|
|||
#= require shortcuts
|
||||
|
||||
class @ShortcutsBlob extends Shortcuts
|
||||
constructor: (skipResetBindings) ->
|
||||
super skipResetBindings
|
||||
Mousetrap.bind('y', ShortcutsBlob.copyToClipboard)
|
||||
|
||||
@copyToClipboard: ->
|
||||
clipboardButton = $('.btn-clipboard')
|
||||
clipboardButton.click() if clipboardButton
|
|
@ -3,13 +3,33 @@ expanded = 'page-sidebar-expanded'
|
|||
|
||||
toggleSidebar = ->
|
||||
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
|
||||
$('header').toggleClass("header-collapsed header-expanded")
|
||||
$('.navbar-fixed-top').toggleClass("header-collapsed header-expanded")
|
||||
|
||||
if $.cookie('pin_nav') is 'true'
|
||||
$('.navbar-fixed-top').toggleClass('header-pinned-nav')
|
||||
$('.page-with-sidebar').toggleClass('page-sidebar-pinned')
|
||||
|
||||
setTimeout ( ->
|
||||
niceScrollBars = $('.nicescroll').niceScroll();
|
||||
niceScrollBars = $('.nav-sidebar').niceScroll();
|
||||
niceScrollBars.updateScrollBar();
|
||||
), 300
|
||||
|
||||
$(document)
|
||||
.off 'click', 'body'
|
||||
.on 'click', 'body', (e) ->
|
||||
unless $.cookie('pin_nav') is 'true'
|
||||
$target = $(e.target)
|
||||
$nav = $target.closest('.sidebar-wrapper')
|
||||
pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded')
|
||||
$toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle')
|
||||
|
||||
if $nav.length is 0 and pageExpanded and $toggle.length is 0
|
||||
$('.page-with-sidebar')
|
||||
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
|
||||
|
||||
$('.navbar-fixed-top')
|
||||
.toggleClass('header-collapsed header-expanded')
|
||||
|
||||
$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
|
|
|
@ -9,9 +9,11 @@ class @Star
|
|||
$this.parent().find('.star-count').text data.star_count
|
||||
if isStarred
|
||||
$starSpan.removeClass('starred').text 'Star'
|
||||
gl.utils.updateTooltipTitle $this, 'Star project'
|
||||
$starIcon.removeClass('fa-star').addClass 'fa-star-o'
|
||||
else
|
||||
$starSpan.addClass('starred').text 'Unstar'
|
||||
gl.utils.updateTooltipTitle $this, 'Unstar project'
|
||||
$starIcon.removeClass('fa-star-o').addClass 'fa-star'
|
||||
return
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class @UsersSelect
|
|||
assignTo = (selected) ->
|
||||
data = {}
|
||||
data[abilityName] = {}
|
||||
data[abilityName].assignee_id = selected
|
||||
data[abilityName].assignee_id = if selected? then selected else null
|
||||
$loading
|
||||
.fadeIn()
|
||||
$dropdown.trigger('loading.gl.dropdown')
|
||||
|
@ -72,7 +72,7 @@ class @UsersSelect
|
|||
|
||||
assigneeTemplate = _.template(
|
||||
'<% if (username) { %>
|
||||
<a class="author_link " href="/u/<%= username %>">
|
||||
<a class="author_link bold" href="/u/<%= username %>">
|
||||
<% if( avatar ) { %>
|
||||
<img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
|
||||
<% } %>
|
||||
|
@ -82,7 +82,7 @@ class @UsersSelect
|
|||
</span>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<span class="assign-yourself">
|
||||
<span class="no-value assign-yourself">
|
||||
No assignee -
|
||||
<a href="#" class="js-assign-yourself">
|
||||
assign yourself
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
*/
|
||||
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
|
||||
.page-with-sidebar {
|
||||
|
||||
.collapse-nav a {
|
||||
.toggle-nav-collapse,
|
||||
.pin-nav-btn {
|
||||
color: $color-light;
|
||||
background: $color;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
*/
|
||||
header {
|
||||
transition-duration: .3s;
|
||||
transition: padding $sidebar-transition-duration;
|
||||
|
||||
&.navbar-empty {
|
||||
height: $header-height;
|
||||
|
@ -79,14 +79,9 @@ header {
|
|||
|
||||
&.header-collapsed {
|
||||
padding: 0 16px;
|
||||
|
||||
.side-nav-toggle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.side-nav-toggle {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
margin: 6px 0;
|
||||
|
@ -108,9 +103,7 @@ header {
|
|||
.header-content {
|
||||
position: relative;
|
||||
height: $header-height;
|
||||
padding-right: 40px;
|
||||
padding-left: 30px;
|
||||
transition-duration: .3s;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
padding-right: 0;
|
||||
|
@ -198,18 +191,6 @@ header {
|
|||
}
|
||||
}
|
||||
|
||||
.header-collapsed {
|
||||
margin-left: 0;
|
||||
|
||||
.header-content {
|
||||
|
||||
@media (min-width: $screen-sm-max) {
|
||||
padding-left: 30px;
|
||||
transition-duration: .3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tanuki-shape {
|
||||
transition: all 0.8s;
|
||||
|
||||
|
|
|
@ -159,7 +159,7 @@ ul.content-list {
|
|||
background-color: $gray-light;
|
||||
border: dotted 1px $gray-dark;
|
||||
margin: 1px 0;
|
||||
min-height: 30px;
|
||||
min-height: 52px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
|
||||
.container-fluid {
|
||||
background-color: $background-color;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
|
@ -241,6 +242,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.adjust {
|
||||
.nav-text, .nav-controls {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-nav {
|
||||
|
@ -250,7 +257,7 @@
|
|||
z-index: 11;
|
||||
background: $background-color;
|
||||
border-bottom: 1px solid $border-color;
|
||||
transition-duration: .3s;
|
||||
transition: padding $sidebar-transition-duration;
|
||||
text-align: center;
|
||||
|
||||
.container-fluid {
|
||||
|
@ -280,11 +287,10 @@
|
|||
}
|
||||
|
||||
.dropdown {
|
||||
margin-left: 7px;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
margin-left: 0;
|
||||
}
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 15px;
|
||||
z-index: 2;
|
||||
|
||||
li.active {
|
||||
font-weight: bold;
|
||||
|
@ -347,6 +353,12 @@
|
|||
.badge {
|
||||
color: $gl-icon-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
a, i {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +1,31 @@
|
|||
.page-with-sidebar {
|
||||
padding-top: $header-height;
|
||||
transition-duration: .3s;
|
||||
transition: padding $sidebar-transition-duration;
|
||||
|
||||
.sidebar-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
transition-duration: .3s;
|
||||
overflow: hidden;
|
||||
transition: width $sidebar-transition-duration;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
z-index: 1000;
|
||||
background: $background-color;
|
||||
|
||||
.nicescroll-rails-hr {
|
||||
// TODO: Figure out why nicescroll doesn't hide horizontal bar
|
||||
display: none!important;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
transition: padding $sidebar-transition-duration;
|
||||
|
||||
.container-fluid {
|
||||
background: #fff;
|
||||
|
@ -34,22 +39,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
.sidebar-user {
|
||||
padding: 15px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: $sidebar_width;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
line-height: 36px;
|
||||
transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
|
||||
|
||||
.sidebar-user {
|
||||
padding: 15px 22px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: $sidebar_width;
|
||||
overflow: hidden;
|
||||
transition-duration: .3s;
|
||||
|
||||
.username {
|
||||
margin-left: 10px;
|
||||
width: $sidebar_width - 2 * 10px;
|
||||
font-size: 16px;
|
||||
line-height: 34px;
|
||||
}
|
||||
@media (min-width: $sidebar-breakpoint) {
|
||||
bottom: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,39 +67,46 @@
|
|||
|
||||
|
||||
.nav-sidebar {
|
||||
margin-top: 22 + $header-height;
|
||||
margin-bottom: 116px;
|
||||
transition-duration: .3s;
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
bottom: 65px;
|
||||
width: $sidebar_width;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
@media (min-width: $sidebar-breakpoint) {
|
||||
bottom: 115px;
|
||||
}
|
||||
|
||||
&.navbar-collapse {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
li {
|
||||
width: $sidebar_width;
|
||||
|
||||
&.separate-item {
|
||||
padding-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 34px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
width: $sidebar_width;
|
||||
padding: 7px 15px 7px 23px;
|
||||
padding: 7px 15px 7px 12px;
|
||||
font-size: $gl-font-size;
|
||||
line-height: 24px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active, &:focus {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -109,10 +118,6 @@
|
|||
svg {
|
||||
margin-right: 13px;
|
||||
}
|
||||
|
||||
&.back-link i {
|
||||
transition-duration: .3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,37 +128,50 @@
|
|||
}
|
||||
}
|
||||
|
||||
.sidebar-subnav {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-nav a {
|
||||
.toggle-nav-collapse {
|
||||
width: $sidebar_width;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-height: 50px;
|
||||
padding: 5px 0;
|
||||
font-size: 18px;
|
||||
background: transparent;
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
transition-duration: .3s;
|
||||
outline: none;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.nav-header-btn {
|
||||
padding: 10px 5px;
|
||||
color: inherit;
|
||||
transition-duration: .3s;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $white-light;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
&.hidden-nav {
|
||||
width: 0;
|
||||
.pin-nav-btn {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 50px;
|
||||
width: $sidebar_width;
|
||||
line-height: 30px;
|
||||
|
||||
@media (min-width: $sidebar-breakpoint) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fa {
|
||||
transition: transform .15s;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.fa {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,62 +180,34 @@
|
|||
|
||||
.sidebar-wrapper {
|
||||
width: 0;
|
||||
|
||||
.nav-sidebar {
|
||||
width: 0;
|
||||
|
||||
li {
|
||||
width: auto;
|
||||
|
||||
a {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-nav a {
|
||||
width: 0;
|
||||
|
||||
i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-user {
|
||||
width: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-sidebar-expanded {
|
||||
|
||||
@media (max-width: $screen-sm-max) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
width: $sidebar_width;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
width: $sidebar_width;
|
||||
.page-sidebar-pinned {
|
||||
.content-wrapper,
|
||||
.layout-nav {
|
||||
@media (min-width: $sidebar-breakpoint) {
|
||||
padding-left: $sidebar_width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header.header-pinned-nav {
|
||||
@media (min-width: $sidebar-breakpoint) {
|
||||
padding-left: ($sidebar_width + $gl-padding);
|
||||
|
||||
.side-nav-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-sidebar li a {
|
||||
width: $sidebar_width;
|
||||
|
||||
&.back-link {
|
||||
i {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.header-content {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ $sidebar_width: 220px;
|
|||
$gutter_collapsed_width: 62px;
|
||||
$gutter_width: 290px;
|
||||
$gutter_inner_width: 258px;
|
||||
$sidebar-transition-duration: .15s;
|
||||
$sidebar-breakpoint: 1440px;
|
||||
|
||||
/*
|
||||
* UI elements
|
||||
|
|
|
@ -7,84 +7,111 @@
|
|||
margin-right: 9px;
|
||||
}
|
||||
|
||||
.lists-separator {
|
||||
margin: 10px 0;
|
||||
border-color: #ddd;
|
||||
}
|
||||
.commit-header {
|
||||
padding: 5px 10px;
|
||||
background-color: $background-color;
|
||||
border-top: 1px solid #eee;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 14px;
|
||||
|
||||
.commits-row {
|
||||
ul {
|
||||
margin: 0;
|
||||
|
||||
li.commit {
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.commits-row-date {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 5px;
|
||||
&:first-child {
|
||||
border-top-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
li.commit {
|
||||
list-style: none;
|
||||
.commit-row-title {
|
||||
line-height: 1;
|
||||
margin-bottom: 7px;
|
||||
|
||||
.commit-row-title {
|
||||
font-size: $list-font-size;
|
||||
line-height: 20px;
|
||||
margin-bottom: 2px;
|
||||
.notes_count {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn-clipboard {
|
||||
margin-top: -1px;
|
||||
.str-truncated {
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.commit-row-message {
|
||||
color: $gl-dark-link-color;
|
||||
}
|
||||
|
||||
.text-expander {
|
||||
display: inline-block;
|
||||
background: $gray-light;
|
||||
color: $gl-placeholder-color;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
border: 1px solid $border-gray-dark;
|
||||
border-radius: $border-radius-default;
|
||||
margin-left: 5px;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($gray-light, 10%);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notes_count {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
.commit-actions {
|
||||
@media (min-width: $screen-sm-min) {
|
||||
float: right;
|
||||
margin-left: $gl-padding;
|
||||
margin-top: 2px;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.btn-transparent {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
&:not(:first-child) {
|
||||
margin-left: $gl-padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commit_short_id {
|
||||
min-width: 65px;
|
||||
color: $gl-dark-link-color;
|
||||
font-family: $monospace_font;
|
||||
}
|
||||
.commit-short-id {
|
||||
font-family: $monospace_font;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.str-truncated {
|
||||
max-width: 70%;
|
||||
}
|
||||
.commit {
|
||||
padding: 10px 0;
|
||||
|
||||
.commit-row-message {
|
||||
color: $gl-dark-link-color;
|
||||
@media (min-width: $screen-sm-min) {
|
||||
padding-left: 46px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.text-expander {
|
||||
background: #eee;
|
||||
color: #555;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
&:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
a,
|
||||
button {
|
||||
color: $gl-dark-link-color;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-left: -46px;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
display: inline-block;
|
||||
max-width: 70%;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
max-width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.commit-row-description {
|
||||
font-size: 14px;
|
||||
border-left: 1px solid #eee;
|
||||
padding: 10px 15px;
|
||||
margin: 5px 0 10px 5px;
|
||||
margin: 10px 0;
|
||||
background: #f9f9f9;
|
||||
display: none;
|
||||
|
||||
|
@ -93,6 +120,7 @@ li.commit {
|
|||
background: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -102,7 +130,7 @@ li.commit {
|
|||
|
||||
.commit-row-info {
|
||||
color: $gl-gray;
|
||||
line-height: 24px;
|
||||
line-height: 1;
|
||||
|
||||
a {
|
||||
color: $gl-gray;
|
||||
|
@ -111,10 +139,6 @@ li.commit {
|
|||
.avatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.committed_ago {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&.inline-commit {
|
||||
|
|
5
app/assets/stylesheets/pages/environments.scss
Normal file
5
app/assets/stylesheets/pages/environments.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
.environments {
|
||||
.commit-title {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
|
@ -39,3 +39,20 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.groups-cover-block {
|
||||
|
||||
.container-fluid {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.access-request-button {
|
||||
@include btn-gray;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 32px;
|
||||
padding: 3px 10px;
|
||||
text-transform: none;
|
||||
background-color: $background-color;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,10 @@
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
.issuable-header-text {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.block {
|
||||
@include clearfix;
|
||||
padding: $gl-padding 0;
|
||||
|
@ -60,10 +64,6 @@
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
.issuable-count {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.gutter-toggle {
|
||||
margin-left: 20px;
|
||||
padding-left: 10px;
|
||||
|
@ -145,7 +145,6 @@
|
|||
|
||||
.assign-yourself {
|
||||
margin-top: 10px;
|
||||
font-weight: normal;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
@ -158,6 +157,10 @@
|
|||
font-weight: normal;
|
||||
}
|
||||
|
||||
.no-value {
|
||||
color: $gl-placeholder-color;
|
||||
}
|
||||
|
||||
.sidebar-collapsed-icon {
|
||||
display: none;
|
||||
}
|
||||
|
@ -248,11 +251,16 @@
|
|||
padding-bottom: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.issuable-header-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.issuable-pager {
|
||||
.issuable-header-btn {
|
||||
background: $gray-normal;
|
||||
border: 1px solid $border-gray-normal;
|
||||
|
||||
&:hover {
|
||||
background: $gray-dark;
|
||||
border: 1px solid $border-gray-dark;
|
||||
|
@ -263,7 +271,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
a:not(.issuable-pager) {
|
||||
a {
|
||||
&:hover {
|
||||
color: $md-link-color;
|
||||
text-decoration: none;
|
||||
|
@ -322,7 +330,7 @@
|
|||
margin-left: 5px;
|
||||
|
||||
a {
|
||||
color: #8c8c8c;
|
||||
color: $gl-placeholder-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -115,6 +115,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.draggable-handler {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
transition: opacity .3s;
|
||||
color: $gray-darkest;
|
||||
}
|
||||
|
||||
.prioritized-labels {
|
||||
margin-bottom: 30px;
|
||||
|
||||
|
@ -122,6 +129,13 @@
|
|||
display: none;
|
||||
color: $gray-light;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
.draggable-handler {
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.other-labels {
|
||||
|
|
|
@ -313,3 +313,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.merged-buttons {
|
||||
.btn {
|
||||
float: left;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,6 +139,12 @@ ul.notes {
|
|||
@media (min-width: $screen-sm-min) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
.inline {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-emoji-button {
|
||||
|
@ -258,7 +264,11 @@ ul.notes {
|
|||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
|
||||
.note-action-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.no-ssh-key-message, .project-limit-message {
|
||||
background-color: #f28d35;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.new_project,
|
||||
.edit-project {
|
||||
fieldset.features {
|
||||
|
@ -18,13 +20,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.project-name-holder {
|
||||
.help-inline {
|
||||
vertical-align: top;
|
||||
padding: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-home-panel {
|
||||
background: $white-light;
|
||||
text-align: left;
|
||||
|
@ -229,13 +224,20 @@
|
|||
right: 16px;
|
||||
bottom: 0;
|
||||
|
||||
.btn {
|
||||
padding: 3px 10px;
|
||||
background-color: $background-color;
|
||||
@media (max-width: $screen-lg-min) {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1304px) {
|
||||
top: 0;
|
||||
.access-request-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 61px;
|
||||
|
||||
@media (max-width: $screen-lg-min) {
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,10 +288,6 @@
|
|||
color: #555;
|
||||
}
|
||||
|
||||
.project_member_row form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.transfer-project .select2-container {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
@ -373,6 +371,7 @@ a.deploy-project-label {
|
|||
|
||||
.project-import .btn {
|
||||
float: left;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
margin: 0;
|
||||
|
||||
.commit {
|
||||
padding: 0;
|
||||
padding: 0 0 0 55px;
|
||||
|
||||
.commit-row-title {
|
||||
.commit-row-message {
|
||||
|
@ -129,4 +129,6 @@
|
|||
.tree-controls {
|
||||
float: right;
|
||||
margin-top: 11px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ class AutocompleteController < ApplicationController
|
|||
project = Project.find_by_id(params[:project_id])
|
||||
|
||||
projects = current_user.authorized_projects
|
||||
projects = projects.search(params[:search]) if params[:search].present?
|
||||
projects = projects.select do |project|
|
||||
current_user.can?(:admin_issue, project)
|
||||
end
|
||||
|
|
58
app/controllers/concerns/membership_actions.rb
Normal file
58
app/controllers/concerns/membership_actions.rb
Normal file
|
@ -0,0 +1,58 @@
|
|||
module MembershipActions
|
||||
extend ActiveSupport::Concern
|
||||
include MembersHelper
|
||||
|
||||
def request_access
|
||||
membershipable.request_access(current_user)
|
||||
|
||||
redirect_to polymorphic_path(membershipable),
|
||||
notice: 'Your request for access has been queued for review.'
|
||||
end
|
||||
|
||||
def approve_access_request
|
||||
@member = membershipable.members.request.find(params[:id])
|
||||
|
||||
return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
|
||||
|
||||
@member.accept_request
|
||||
|
||||
redirect_to polymorphic_url([membershipable, :members])
|
||||
end
|
||||
|
||||
def leave
|
||||
@member = membershipable.members.find_by(user_id: current_user)
|
||||
return render_403 unless @member
|
||||
|
||||
source_type = @member.real_source_type.humanize(capitalize: false)
|
||||
|
||||
if can?(current_user, action_member_permission(:destroy, @member), @member)
|
||||
notice =
|
||||
if @member.request?
|
||||
"Your access request to the #{source_type} has been withdrawn."
|
||||
else
|
||||
"You left the \"#{@member.source.human_name}\" #{source_type}."
|
||||
end
|
||||
@member.destroy
|
||||
|
||||
redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice
|
||||
else
|
||||
if cannot_leave?
|
||||
alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}."
|
||||
alert << " Transfer or delete the #{source_type}."
|
||||
redirect_to polymorphic_url(membershipable), alert: alert
|
||||
else
|
||||
render_403
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def membershipable
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def cannot_leave?
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
|
@ -1,11 +1,13 @@
|
|||
class Groups::GroupMembersController < Groups::ApplicationController
|
||||
include MembershipActions
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_admin_group_member!, except: [:index, :leave]
|
||||
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
|
||||
|
||||
def index
|
||||
@project = @group.projects.find(params[:project_id]) if params[:project_id]
|
||||
@members = @group.group_members
|
||||
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
|
||||
@members = @members.non_pending unless can?(current_user, :admin_group, @group)
|
||||
|
||||
if params[:search].present?
|
||||
users = @group.users.search(params[:search]).to_a
|
||||
|
@ -58,25 +60,16 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def leave
|
||||
@group_member = @group.group_members.find_by(user_id: current_user)
|
||||
|
||||
if can?(current_user, :destroy_group_member, @group_member)
|
||||
@group_member.destroy
|
||||
|
||||
redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
|
||||
else
|
||||
if @group.last_owner?(current_user)
|
||||
redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
|
||||
else
|
||||
return render_403
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def member_params
|
||||
params.require(:group_member).permit(:access_level, :user_id)
|
||||
end
|
||||
|
||||
# MembershipActions concern
|
||||
alias_method :membershipable, :group
|
||||
|
||||
def cannot_leave?
|
||||
@group.last_owner?(current_user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -42,7 +42,7 @@ class JwtController < ApplicationController
|
|||
end
|
||||
|
||||
def authenticate_user(login, password)
|
||||
user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password)
|
||||
user = Gitlab::Auth.find_with_user_password(login, password)
|
||||
Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
|
||||
user
|
||||
end
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
class Profiles::NotificationsController < Profiles::ApplicationController
|
||||
def show
|
||||
@user = current_user
|
||||
@group_notifications = current_user.notification_settings.for_groups
|
||||
@project_notifications = current_user.notification_settings.for_projects
|
||||
@user = current_user
|
||||
@group_notifications = current_user.notification_settings.for_groups
|
||||
@project_notifications = current_user.notification_settings.for_projects
|
||||
@global_notification_setting = current_user.global_notification_setting
|
||||
end
|
||||
|
||||
def update
|
||||
if current_user.update_attributes(user_params)
|
||||
if current_user.update_attributes(user_params) && update_notification_settings
|
||||
flash[:notice] = "Notification settings saved"
|
||||
else
|
||||
flash[:alert] = "Failed to save new settings"
|
||||
|
@ -16,6 +17,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController
|
|||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:notification_email, :notification_level)
|
||||
params.require(:user).permit(:notification_email)
|
||||
end
|
||||
|
||||
def global_notification_setting_params
|
||||
params.require(:global_notification_setting).permit(:level)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_notification_settings
|
||||
return true unless global_notification_setting_params
|
||||
|
||||
current_user.global_notification_setting.update_attributes(global_notification_setting_params)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
class Projects::ArtifactsController < Projects::ApplicationController
|
||||
layout 'project'
|
||||
before_action :authorize_read_build!
|
||||
before_action :authorize_update_build!, only: [:keep]
|
||||
before_action :validate_artifacts!
|
||||
|
||||
def download
|
||||
unless artifacts_file.file_storage?
|
||||
return redirect_to artifacts_file.url
|
||||
end
|
||||
|
||||
unless artifacts_file.exists?
|
||||
return render_404
|
||||
end
|
||||
|
||||
send_file artifacts_file.path, disposition: 'attachment'
|
||||
end
|
||||
|
||||
def browse
|
||||
return render_404 unless build.artifacts?
|
||||
|
||||
directory = params[:path] ? "#{params[:path]}/" : ''
|
||||
@entry = build.artifacts_metadata_entry(directory)
|
||||
|
||||
|
@ -34,8 +30,17 @@ class Projects::ArtifactsController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def keep
|
||||
build.keep_artifacts!
|
||||
redirect_to namespace_project_build_path(project.namespace, project, build)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_artifacts!
|
||||
render_404 unless build.artifacts?
|
||||
end
|
||||
|
||||
def build
|
||||
@build ||= project.builds.find_by!(id: params[:build_id])
|
||||
end
|
||||
|
|
|
@ -51,7 +51,7 @@ class Projects::BuildsController < Projects::ApplicationController
|
|||
return render_404
|
||||
end
|
||||
|
||||
build = Ci::Build.retry(@build)
|
||||
build = Ci::Build.retry(@build, current_user)
|
||||
redirect_to build_path(build)
|
||||
end
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ class Projects::CommitController < Projects::ApplicationController
|
|||
def retry_builds
|
||||
ci_builds.latest.failed.each do |build|
|
||||
if build.retryable?
|
||||
Ci::Build.retry(build)
|
||||
Ci::Build.retry(build, current_user)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
49
app/controllers/projects/environments_controller.rb
Normal file
49
app/controllers/projects/environments_controller.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
class Projects::EnvironmentsController < Projects::ApplicationController
|
||||
layout 'project'
|
||||
before_action :authorize_read_environment!
|
||||
before_action :authorize_create_environment!, only: [:new, :create]
|
||||
before_action :authorize_update_environment!, only: [:destroy]
|
||||
before_action :environment, only: [:show, :destroy]
|
||||
|
||||
def index
|
||||
@environments = project.environments
|
||||
end
|
||||
|
||||
def show
|
||||
@deployments = environment.deployments.order(id: :desc).page(params[:page])
|
||||
end
|
||||
|
||||
def new
|
||||
@environment = project.environments.new
|
||||
end
|
||||
|
||||
def create
|
||||
@environment = project.environments.create(create_params)
|
||||
|
||||
if @environment.persisted?
|
||||
redirect_to namespace_project_environment_path(project.namespace, project, @environment)
|
||||
else
|
||||
render 'new'
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @environment.destroy
|
||||
flash[:notice] = 'Environment was successfully removed.'
|
||||
else
|
||||
flash[:alert] = 'Failed to remove environment.'
|
||||
end
|
||||
|
||||
redirect_to namespace_project_environments_path(project.namespace, project)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_params
|
||||
params.require(:environment).permit(:name)
|
||||
end
|
||||
|
||||
def environment
|
||||
@environment ||= project.environments.find(params[:id])
|
||||
end
|
||||
end
|
|
@ -43,7 +43,7 @@ class Projects::GitHttpController < Projects::ApplicationController
|
|||
return if project && project.public? && upload_pack?
|
||||
|
||||
authenticate_or_request_with_http_basic do |login, password|
|
||||
auth_result = Gitlab::Auth.find(login, password, project: project, ip: request.ip)
|
||||
auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
|
||||
|
||||
if auth_result.type == :ci && upload_pack?
|
||||
@ci = true
|
||||
|
|
|
@ -204,10 +204,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
|
||||
@merge_request.update(merge_error: nil)
|
||||
|
||||
if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active?
|
||||
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
|
||||
.execute(@merge_request)
|
||||
@status = :merge_when_build_succeeds
|
||||
if params[:merge_when_build_succeeds].present?
|
||||
if @merge_request.pipeline && @merge_request.pipeline.active?
|
||||
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
|
||||
.execute(@merge_request)
|
||||
@status = :merge_when_build_succeeds
|
||||
elsif @merge_request.pipeline.success?
|
||||
# This can be triggered when a user clicks the auto merge button while
|
||||
# the tests finish at about the same time
|
||||
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
|
||||
@status = :success
|
||||
else
|
||||
@status = :failed
|
||||
end
|
||||
else
|
||||
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
|
||||
@status = :success
|
||||
|
|
|
@ -32,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def retry
|
||||
pipeline.retry_failed
|
||||
pipeline.retry_failed(current_user)
|
||||
|
||||
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
|
||||
end
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
class Projects::ProjectMembersController < Projects::ApplicationController
|
||||
include MembershipActions
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_admin_project_member!, except: [:leave, :index]
|
||||
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
|
||||
|
||||
def index
|
||||
@project_members = @project.project_members
|
||||
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
|
||||
@project_members = @project_members.non_pending unless can?(current_user, :admin_project, @project)
|
||||
|
||||
if params[:search].present?
|
||||
users = @project.users.search(params[:search]).to_a
|
||||
|
@ -14,9 +16,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
@project_members = @project_members.order('access_level DESC')
|
||||
|
||||
@group = @project.group
|
||||
|
||||
if @group
|
||||
@group_members = @group.group_members
|
||||
@group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
|
||||
@group_members = @group_members.non_pending unless can?(current_user, :admin_group, @group)
|
||||
|
||||
if params[:search].present?
|
||||
users = @group.users.search(params[:search]).to_a
|
||||
|
@ -73,26 +76,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def leave
|
||||
@project_member = @project.project_members.find_by(user_id: current_user)
|
||||
|
||||
if can?(current_user, :destroy_project_member, @project_member)
|
||||
@project_member.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
|
||||
format.js { head :ok }
|
||||
end
|
||||
else
|
||||
if current_user == @project.owner
|
||||
message = 'You can not leave your own project. Transfer or delete the project.'
|
||||
redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
|
||||
else
|
||||
render_403
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def apply_import
|
||||
source_project = Project.find(params[:source_project_id])
|
||||
|
||||
|
@ -112,4 +95,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
def member_params
|
||||
params.require(:project_member).permit(:user_id, :access_level)
|
||||
end
|
||||
|
||||
# MembershipActions concern
|
||||
alias_method :membershipable, :project
|
||||
|
||||
def cannot_leave?
|
||||
current_user == @project.owner
|
||||
end
|
||||
end
|
||||
|
|
31
app/controllers/projects/todos_controller.rb
Normal file
31
app/controllers/projects/todos_controller.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
class Projects::TodosController < Projects::ApplicationController
|
||||
def create
|
||||
todos = TodoService.new.mark_todo(issuable, current_user)
|
||||
|
||||
render json: {
|
||||
todo: todos,
|
||||
count: current_user.todos.pending.count,
|
||||
}
|
||||
end
|
||||
|
||||
def update
|
||||
current_user.todos.find_by_id(params[:id]).update(state: :done)
|
||||
|
||||
render json: {
|
||||
count: current_user.todos.pending.count,
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def issuable
|
||||
@issuable ||= begin
|
||||
case params[:issuable_type]
|
||||
when "issue"
|
||||
@project.issues.find(params[:issuable_id])
|
||||
when "merge_request"
|
||||
@project.merge_requests.find(params[:issuable_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,6 +16,9 @@ class Projects::WikisController < Projects::ApplicationController
|
|||
if @page
|
||||
render 'show'
|
||||
elsif file = @project_wiki.find_file(params[:id], params[:version_id])
|
||||
response.headers['Content-Security-Policy'] = "default-src 'none'"
|
||||
response.headers['X-Content-Security-Policy'] = "default-src 'none'"
|
||||
|
||||
if file.on_disk?
|
||||
send_file file.on_disk_path, disposition: 'inline'
|
||||
else
|
||||
|
|
|
@ -40,7 +40,7 @@ class SessionsController < Devise::SessionsController
|
|||
# Handle an "initial setup" state, where there's only one user, it's an admin,
|
||||
# and they require a password change.
|
||||
def check_initial_setup
|
||||
return unless User.count == 1
|
||||
return unless User.limit(2).count == 1 # Count as much 2 to know if we have exactly one
|
||||
|
||||
user = User.admins.last
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ class NotesFinder
|
|||
when "commit"
|
||||
project.notes.for_commit_id(target_id).non_diff_notes
|
||||
when "issue"
|
||||
project.issues.find(target_id).notes.inc_author
|
||||
project.issues.visible_to_user(current_user).find(target_id).notes.inc_author
|
||||
when "merge_request"
|
||||
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
|
||||
when "snippet", "project_snippet"
|
||||
|
|
|
@ -51,7 +51,7 @@ class SnippetsFinder
|
|||
snippets = project.snippets.fresh
|
||||
|
||||
if current_user
|
||||
if project.team.member?(current_user.id) || current_user.admin?
|
||||
if project.team.member?(current_user) || current_user.admin?
|
||||
snippets
|
||||
else
|
||||
snippets.public_and_internal
|
||||
|
|
|
@ -36,7 +36,7 @@ class TodosFinder
|
|||
private
|
||||
|
||||
def action_id?
|
||||
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i)
|
||||
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i)
|
||||
end
|
||||
|
||||
def action_id
|
||||
|
|
|
@ -116,7 +116,7 @@ module BlobHelper
|
|||
end
|
||||
|
||||
def blob_text_viewable?(blob)
|
||||
blob && blob.text? && !blob.lfs_pointer?
|
||||
blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw?
|
||||
end
|
||||
|
||||
def blob_size(blob)
|
||||
|
|
|
@ -14,4 +14,8 @@ module BranchesHelper
|
|||
|
||||
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
|
||||
end
|
||||
|
||||
def project_branches
|
||||
options_for_select(@project.repository.branch_names, @project.default_branch)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,15 @@ module ButtonHelper
|
|||
def clipboard_button(data = {})
|
||||
content_tag :button,
|
||||
icon('clipboard'),
|
||||
class: 'btn btn-clipboard',
|
||||
class: "btn",
|
||||
data: data,
|
||||
type: :button
|
||||
end
|
||||
|
||||
def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
|
||||
content_tag :button,
|
||||
icon('clipboard'),
|
||||
class: "btn #{css_class}",
|
||||
data: data,
|
||||
type: :button
|
||||
end
|
||||
|
|
|
@ -38,10 +38,10 @@ module CiStatusHelper
|
|||
icon(icon_name + ' fw')
|
||||
end
|
||||
|
||||
def render_commit_status(commit, tooltip_placement: 'auto left')
|
||||
def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '')
|
||||
project = commit.project
|
||||
path = builds_namespace_project_commit_path(project.namespace, project, commit)
|
||||
render_status_with_link('commit', commit.status, path, tooltip_placement)
|
||||
render_status_with_link('commit', commit.status, path, tooltip_placement, cssclass: cssclass)
|
||||
end
|
||||
|
||||
def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
|
||||
|
@ -57,10 +57,10 @@ module CiStatusHelper
|
|||
|
||||
private
|
||||
|
||||
def render_status_with_link(type, status, path, tooltip_placement)
|
||||
def render_status_with_link(type, status, path, tooltip_placement, cssclass: '')
|
||||
link_to ci_icon_for_status(status),
|
||||
path,
|
||||
class: "ci-status-link ci-status-icon-#{status.dasherize}",
|
||||
class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}",
|
||||
title: "#{type.titleize}: #{ci_label_for_status(status)}",
|
||||
data: { toggle: 'tooltip', placement: tooltip_placement }
|
||||
end
|
||||
|
|
|
@ -16,6 +16,16 @@ module CommitsHelper
|
|||
commit_person_link(commit, options.merge(source: :committer))
|
||||
end
|
||||
|
||||
def commit_author_avatar(commit, options = {})
|
||||
options = options.merge(source: :author)
|
||||
user = commit.send(options[:source])
|
||||
|
||||
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
|
||||
person_email = user.try(:email) || source_email
|
||||
|
||||
image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]} hidden-xs", width: options[:size], alt: "")
|
||||
end
|
||||
|
||||
def image_diff_class(diff)
|
||||
if diff.deleted_file
|
||||
"deleted"
|
||||
|
@ -102,24 +112,24 @@ module CommitsHelper
|
|||
if current_controller?(:projects, :commits)
|
||||
if @repo.blob_at(commit.id, @path)
|
||||
return link_to(
|
||||
"Browse File »",
|
||||
"Browse File",
|
||||
namespace_project_blob_path(project.namespace, project,
|
||||
tree_join(commit.id, @path)),
|
||||
class: "pull-right"
|
||||
class: "btn btn-default"
|
||||
)
|
||||
elsif @path.present?
|
||||
return link_to(
|
||||
"Browse Directory »",
|
||||
"Browse Directory",
|
||||
namespace_project_tree_path(project.namespace, project,
|
||||
tree_join(commit.id, @path)),
|
||||
class: "pull-right"
|
||||
class: "btn btn-default"
|
||||
)
|
||||
end
|
||||
end
|
||||
link_to(
|
||||
"Browse Files",
|
||||
namespace_project_tree_path(project.namespace, project, commit),
|
||||
class: "pull-right"
|
||||
class: "btn btn-default"
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -129,7 +139,7 @@ module CommitsHelper
|
|||
tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip
|
||||
|
||||
if can_collaborate_with_project?
|
||||
btn_class = "btn btn-grouped btn-close btn-#{btn_class}" unless btn_class.nil?
|
||||
btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
|
||||
link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
|
||||
elsif can?(current_user, :fork_project, @project)
|
||||
continue_params = {
|
||||
|
@ -141,7 +151,7 @@ module CommitsHelper
|
|||
namespace_key: current_user.namespace.id,
|
||||
continue: continue_params)
|
||||
|
||||
btn_class = "btn btn-grouped btn-close" unless btn_class.nil?
|
||||
btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
|
||||
|
||||
link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
|
||||
end
|
||||
|
@ -153,7 +163,7 @@ module CommitsHelper
|
|||
tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
|
||||
|
||||
if can_collaborate_with_project?
|
||||
btn_class = "btn btn-default btn-grouped btn-#{btn_class}" unless btn_class.nil?
|
||||
btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
|
||||
link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
|
||||
elsif can?(current_user, :fork_project, @project)
|
||||
continue_params = {
|
||||
|
@ -187,12 +197,10 @@ module CommitsHelper
|
|||
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
|
||||
|
||||
person_name = user.try(:name) || source_name
|
||||
person_email = user.try(:email) || source_email
|
||||
|
||||
text =
|
||||
if options[:avatar]
|
||||
avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")
|
||||
%Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
|
||||
%Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>}
|
||||
else
|
||||
person_name
|
||||
end
|
||||
|
|
|
@ -135,6 +135,11 @@ module DiffHelper
|
|||
toggle_whitespace_link(url, options)
|
||||
end
|
||||
|
||||
def diff_compare_whitespace_link(project, from, to, options)
|
||||
url = namespace_project_compare_path(project.namespace, project, from, to, params_with_whitespace)
|
||||
toggle_whitespace_link(url, options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def hide_whitespace?
|
||||
|
|
|
@ -13,10 +13,23 @@
|
|||
# merge_request_path(merge_request)
|
||||
#
|
||||
module GitlabRoutingHelper
|
||||
# Project
|
||||
def project_path(project, *args)
|
||||
namespace_project_path(project.namespace, project, *args)
|
||||
end
|
||||
|
||||
def project_url(project, *args)
|
||||
namespace_project_url(project.namespace, project, *args)
|
||||
end
|
||||
|
||||
def edit_project_path(project, *args)
|
||||
edit_namespace_project_path(project.namespace, project, *args)
|
||||
end
|
||||
|
||||
def edit_project_url(project, *args)
|
||||
edit_namespace_project_url(project.namespace, project, *args)
|
||||
end
|
||||
|
||||
def project_files_path(project, *args)
|
||||
namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
|
||||
end
|
||||
|
@ -29,6 +42,10 @@ module GitlabRoutingHelper
|
|||
namespace_project_pipelines_path(project.namespace, project, *args)
|
||||
end
|
||||
|
||||
def project_environments_path(project, *args)
|
||||
namespace_project_environments_path(project.namespace, project, *args)
|
||||
end
|
||||
|
||||
def project_builds_path(project, *args)
|
||||
namespace_project_builds_path(project.namespace, project, *args)
|
||||
end
|
||||
|
@ -41,10 +58,6 @@ module GitlabRoutingHelper
|
|||
activity_namespace_project_path(project.namespace, project, *args)
|
||||
end
|
||||
|
||||
def edit_project_path(project, *args)
|
||||
edit_namespace_project_path(project.namespace, project, *args)
|
||||
end
|
||||
|
||||
def runners_path(project, *args)
|
||||
namespace_project_runners_path(project.namespace, project, *args)
|
||||
end
|
||||
|
@ -65,14 +78,6 @@ module GitlabRoutingHelper
|
|||
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
|
||||
end
|
||||
|
||||
def project_url(project, *args)
|
||||
namespace_project_url(project.namespace, project, *args)
|
||||
end
|
||||
|
||||
def edit_project_url(project, *args)
|
||||
edit_namespace_project_url(project.namespace, project, *args)
|
||||
end
|
||||
|
||||
def issue_url(entity, *args)
|
||||
namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
|
||||
end
|
||||
|
@ -92,4 +97,56 @@ module GitlabRoutingHelper
|
|||
toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
|
||||
end
|
||||
end
|
||||
|
||||
## Members
|
||||
def project_members_url(project, *args)
|
||||
namespace_project_project_members_url(project.namespace, project)
|
||||
end
|
||||
|
||||
def project_member_path(project_member, *args)
|
||||
namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
|
||||
end
|
||||
|
||||
def request_access_project_members_path(project, *args)
|
||||
request_access_namespace_project_project_members_path(project.namespace, project)
|
||||
end
|
||||
|
||||
def leave_project_members_path(project, *args)
|
||||
leave_namespace_project_project_members_path(project.namespace, project)
|
||||
end
|
||||
|
||||
def approve_access_request_project_member_path(project_member, *args)
|
||||
approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
|
||||
end
|
||||
|
||||
def resend_invite_project_member_path(project_member, *args)
|
||||
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
|
||||
end
|
||||
|
||||
# Groups
|
||||
|
||||
## Members
|
||||
def group_members_url(group, *args)
|
||||
group_group_members_url(group, *args)
|
||||
end
|
||||
|
||||
def group_member_path(group_member, *args)
|
||||
group_group_member_path(group_member.source, group_member)
|
||||
end
|
||||
|
||||
def request_access_group_members_path(group, *args)
|
||||
request_access_group_group_members_path(group)
|
||||
end
|
||||
|
||||
def leave_group_members_path(group, *args)
|
||||
leave_group_group_members_path(group)
|
||||
end
|
||||
|
||||
def approve_access_request_group_member_path(group_member, *args)
|
||||
approve_access_request_group_group_member_path(group_member.source, group_member)
|
||||
end
|
||||
|
||||
def resend_invite_group_member_path(group_member, *args)
|
||||
resend_invite_group_group_member_path(group_member.source, group_member)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,24 +1,4 @@
|
|||
module GroupsHelper
|
||||
def remove_user_from_group_message(group, member)
|
||||
if member.user
|
||||
"Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
|
||||
else
|
||||
"Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
|
||||
end
|
||||
end
|
||||
|
||||
def leave_group_message(group)
|
||||
"Are you sure you want to leave \"#{group}\" group?"
|
||||
end
|
||||
|
||||
def should_user_see_group_roles?(user, group)
|
||||
if user
|
||||
user.is_admin? || group.members.exists?(user_id: user.id)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def can_change_group_visibility_level?(group)
|
||||
can?(current_user, :change_visibility_level, group)
|
||||
end
|
||||
|
|
|
@ -67,6 +67,12 @@ module IssuablesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def has_todo(issuable)
|
||||
unless current_user.nil?
|
||||
current_user.todos.find_by(target_id: issuable.id, state: :pending)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sidebar_gutter_collapsed?
|
||||
|
|
39
app/helpers/members_helper.rb
Normal file
39
app/helpers/members_helper.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
module MembersHelper
|
||||
# Returns a `<action>_<source>_member` association, e.g.:
|
||||
# - admin_project_member, update_project_member, destroy_project_member
|
||||
# - admin_group_member, update_group_member, destroy_group_member
|
||||
def action_member_permission(action, member)
|
||||
"#{action}_#{member.type.underscore}".to_sym
|
||||
end
|
||||
|
||||
def remove_member_message(member, user: nil)
|
||||
user = current_user if defined?(current_user)
|
||||
|
||||
text = 'Are you sure you want to '
|
||||
action =
|
||||
if member.request?
|
||||
if member.user == user
|
||||
'withdraw your access request for'
|
||||
else
|
||||
"deny #{member.user.name}'s request to join"
|
||||
end
|
||||
elsif member.invite?
|
||||
"revoke the invitation for #{member.invite_email} to join"
|
||||
else
|
||||
"remove #{member.user.name} from"
|
||||
end
|
||||
|
||||
text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
|
||||
end
|
||||
|
||||
def remove_member_title(member)
|
||||
text = " from #{member.real_source_type.humanize(capitalize: false)}"
|
||||
|
||||
text.prepend(member.request? ? 'Deny access request' : 'Remove user')
|
||||
end
|
||||
|
||||
def leave_confirmation_message(member_source)
|
||||
"Are you sure you want to leave the " \
|
||||
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
|
||||
end
|
||||
end
|
|
@ -12,10 +12,10 @@ module NavHelper
|
|||
end
|
||||
|
||||
def page_sidebar_class
|
||||
if nav_menu_collapsed?
|
||||
"page-sidebar-collapsed"
|
||||
if pinned_nav?
|
||||
"page-sidebar-expanded page-sidebar-pinned"
|
||||
else
|
||||
"page-sidebar-expanded"
|
||||
"page-sidebar-collapsed"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -36,7 +36,15 @@ module NavHelper
|
|||
end
|
||||
|
||||
def nav_header_class
|
||||
class_name = " with-horizontal-nav" if defined?(nav) && nav
|
||||
class_name = ''
|
||||
class_name << " with-horizontal-nav" if defined?(nav) && nav
|
||||
|
||||
if pinned_nav?
|
||||
class_name << " header-expanded header-pinned-nav"
|
||||
else
|
||||
class_name << " header-collapsed"
|
||||
end
|
||||
|
||||
class_name
|
||||
end
|
||||
|
||||
|
@ -47,4 +55,8 @@ module NavHelper
|
|||
def nav_control_class
|
||||
"nav-control" if current_user
|
||||
end
|
||||
|
||||
def pinned_nav?
|
||||
cookies[:pin_nav] == 'true'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -61,4 +61,23 @@ module NotificationsHelper
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def notification_level_radio_buttons
|
||||
html = ""
|
||||
|
||||
NotificationSetting.levels.each_key do |level|
|
||||
level = level.to_sym
|
||||
next if level == :global
|
||||
|
||||
html << content_tag(:div, class: "radio") do
|
||||
content_tag(:label, { value: level }) do
|
||||
radio_button_tag(:"global_notification_setting[level]", level, @global_notification_setting.level.to_sym == level) +
|
||||
content_tag(:div, level.to_s.capitalize, class: "level-title") +
|
||||
content_tag(:p, notification_description(level))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
html.html_safe
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
module ProjectsHelper
|
||||
def remove_from_project_team_message(project, member)
|
||||
if member.user
|
||||
"You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
|
||||
else
|
||||
"You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
|
||||
end
|
||||
end
|
||||
|
||||
def link_to_project(project)
|
||||
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
|
||||
title = content_tag(:span, project.name, class: 'project-name')
|
||||
|
@ -49,7 +41,7 @@ module ProjectsHelper
|
|||
author_html = author_html.html_safe
|
||||
|
||||
if opts[:name]
|
||||
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
|
||||
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
|
||||
else
|
||||
title = opts[:title].sub(":name", sanitize(author.name))
|
||||
link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe
|
||||
|
@ -115,14 +107,6 @@ module ProjectsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def user_max_access_in_project(user_id, project)
|
||||
level = project.team.max_member_access(user_id)
|
||||
|
||||
if level
|
||||
Gitlab::Access.options_with_owner.key(level)
|
||||
end
|
||||
end
|
||||
|
||||
def license_short_name(project)
|
||||
return 'LICENSE' if project.repository.license_key.nil?
|
||||
|
||||
|
@ -156,6 +140,10 @@ module ProjectsHelper
|
|||
nav_tabs << :container_registry
|
||||
end
|
||||
|
||||
if can?(current_user, :read_environment, project)
|
||||
nav_tabs << :environments
|
||||
end
|
||||
|
||||
if can?(current_user, :admin_project, project)
|
||||
nav_tabs << :settings
|
||||
end
|
||||
|
@ -286,10 +274,6 @@ module ProjectsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def leave_project_message(project)
|
||||
"Are you sure you want to leave \"#{project.name}\" project?"
|
||||
end
|
||||
|
||||
def new_readme_path
|
||||
ref = @repository.root_ref if @repository
|
||||
ref ||= 'master'
|
||||
|
|
|
@ -20,7 +20,6 @@ module TimeHelper
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
def date_from_to(from, to)
|
||||
"#{from.to_s(:short)} - #{to.to_s(:short)}"
|
||||
end
|
||||
|
|
|
@ -12,6 +12,7 @@ module TodosHelper
|
|||
when Todo::ASSIGNED then 'assigned you'
|
||||
when Todo::MENTIONED then 'mentioned you on'
|
||||
when Todo::BUILD_FAILED then 'The build failed for your'
|
||||
when Todo::MARKED then 'marked this as a Todo for'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
module Emails
|
||||
module Groups
|
||||
def group_access_granted_email(group_member_id)
|
||||
@group_member = GroupMember.find(group_member_id)
|
||||
@group = @group_member.group
|
||||
|
||||
@target_url = group_url(@group)
|
||||
@current_user = @group_member.user
|
||||
|
||||
mail(to: @group_member.user.notification_email,
|
||||
subject: subject("Access to group was granted"))
|
||||
end
|
||||
|
||||
def group_member_invited_email(group_member_id, token)
|
||||
@group_member = GroupMember.find group_member_id
|
||||
@group = @group_member.group
|
||||
@token = token
|
||||
|
||||
@target_url = group_url(@group)
|
||||
@current_user = @group_member.user
|
||||
|
||||
mail(to: @group_member.invite_email,
|
||||
subject: "Invitation to join group #{@group.name}")
|
||||
end
|
||||
|
||||
def group_invite_accepted_email(group_member_id)
|
||||
@group_member = GroupMember.find group_member_id
|
||||
return if @group_member.created_by.nil?
|
||||
|
||||
@group = @group_member.group
|
||||
|
||||
@target_url = group_url(@group)
|
||||
@current_user = @group_member.created_by
|
||||
|
||||
mail(to: @group_member.created_by.notification_email,
|
||||
subject: subject("Invitation accepted"))
|
||||
end
|
||||
|
||||
def group_invite_declined_email(group_id, invite_email, access_level, created_by_id)
|
||||
return if created_by_id.nil?
|
||||
|
||||
@group = Group.find(group_id)
|
||||
@current_user = @created_by = User.find(created_by_id)
|
||||
@access_level = access_level
|
||||
@invite_email = invite_email
|
||||
|
||||
@target_url = group_url(@group)
|
||||
mail(to: @created_by.notification_email,
|
||||
subject: subject("Invitation declined"))
|
||||
end
|
||||
end
|
||||
end
|
81
app/mailers/emails/members.rb
Normal file
81
app/mailers/emails/members.rb
Normal file
|
@ -0,0 +1,81 @@
|
|||
module Emails
|
||||
module Members
|
||||
extend ActiveSupport::Concern
|
||||
include MembersHelper
|
||||
|
||||
included do
|
||||
helper_method :member_source, :member
|
||||
end
|
||||
|
||||
def member_access_requested_email(member_source_type, member_id)
|
||||
@member_source_type = member_source_type
|
||||
@member_id = member_id
|
||||
|
||||
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
|
||||
|
||||
mail(to: admins,
|
||||
subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
|
||||
end
|
||||
|
||||
def member_access_granted_email(member_source_type, member_id)
|
||||
@member_source_type = member_source_type
|
||||
@member_id = member_id
|
||||
|
||||
mail(to: member.user.notification_email,
|
||||
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
|
||||
end
|
||||
|
||||
def member_access_denied_email(member_source_type, source_id, user_id)
|
||||
@member_source_type = member_source_type
|
||||
@member_source = member_source_class.find(source_id)
|
||||
requester = User.find(user_id)
|
||||
|
||||
mail(to: requester.notification_email,
|
||||
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
|
||||
end
|
||||
|
||||
def member_invited_email(member_source_type, member_id, token)
|
||||
@member_source_type = member_source_type
|
||||
@member_id = member_id
|
||||
@token = token
|
||||
|
||||
mail(to: member.invite_email,
|
||||
subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
|
||||
end
|
||||
|
||||
def member_invite_accepted_email(member_source_type, member_id)
|
||||
@member_source_type = member_source_type
|
||||
@member_id = member_id
|
||||
return unless member.created_by
|
||||
|
||||
mail(to: member.created_by.notification_email,
|
||||
subject: subject('Invitation accepted'))
|
||||
end
|
||||
|
||||
def member_invite_declined_email(member_source_type, source_id, invite_email, created_by_id)
|
||||
return unless created_by_id
|
||||
|
||||
@member_source_type = member_source_type
|
||||
@member_source = member_source_class.find(source_id)
|
||||
@invite_email = invite_email
|
||||
inviter = User.find(created_by_id)
|
||||
|
||||
mail(to: inviter.notification_email,
|
||||
subject: subject('Invitation declined'))
|
||||
end
|
||||
|
||||
def member
|
||||
@member ||= Member.find(@member_id)
|
||||
end
|
||||
|
||||
def member_source
|
||||
@member_source ||= member.source
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def member_source_class
|
||||
@member_source_type.classify.constantize
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,55 +1,5 @@
|
|||
module Emails
|
||||
module Projects
|
||||
def project_access_granted_email(project_member_id)
|
||||
@project_member = ProjectMember.find project_member_id
|
||||
@project = @project_member.project
|
||||
|
||||
@target_url = namespace_project_url(@project.namespace, @project)
|
||||
@current_user = @project_member.user
|
||||
|
||||
mail(to: @project_member.user.notification_email,
|
||||
subject: subject("Access to project was granted"))
|
||||
end
|
||||
|
||||
def project_member_invited_email(project_member_id, token)
|
||||
@project_member = ProjectMember.find project_member_id
|
||||
@project = @project_member.project
|
||||
@token = token
|
||||
|
||||
@target_url = namespace_project_url(@project.namespace, @project)
|
||||
@current_user = @project_member.user
|
||||
|
||||
mail(to: @project_member.invite_email,
|
||||
subject: "Invitation to join project #{@project.name_with_namespace}")
|
||||
end
|
||||
|
||||
def project_invite_accepted_email(project_member_id)
|
||||
@project_member = ProjectMember.find project_member_id
|
||||
return if @project_member.created_by.nil?
|
||||
|
||||
@project = @project_member.project
|
||||
|
||||
@target_url = namespace_project_url(@project.namespace, @project)
|
||||
@current_user = @project_member.created_by
|
||||
|
||||
mail(to: @project_member.created_by.notification_email,
|
||||
subject: subject("Invitation accepted"))
|
||||
end
|
||||
|
||||
def project_invite_declined_email(project_id, invite_email, access_level, created_by_id)
|
||||
return if created_by_id.nil?
|
||||
|
||||
@project = Project.find(project_id)
|
||||
@current_user = @created_by = User.find(created_by_id)
|
||||
@access_level = access_level
|
||||
@invite_email = invite_email
|
||||
|
||||
@target_url = namespace_project_url(@project.namespace, @project)
|
||||
|
||||
mail(to: @created_by.notification_email,
|
||||
subject: subject("Invitation declined"))
|
||||
end
|
||||
|
||||
def project_was_moved_email(project_id, user_id, old_path_with_namespace)
|
||||
@current_user = @user = User.find user_id
|
||||
@project = Project.find project_id
|
||||
|
|
|
@ -6,13 +6,15 @@ class Notify < BaseMailer
|
|||
include Emails::Notes
|
||||
include Emails::Projects
|
||||
include Emails::Profile
|
||||
include Emails::Groups
|
||||
include Emails::Builds
|
||||
include Emails::Members
|
||||
|
||||
add_template_helper MergeRequestsHelper
|
||||
add_template_helper DiffHelper
|
||||
add_template_helper BlobHelper
|
||||
add_template_helper EmailsHelper
|
||||
add_template_helper MembersHelper
|
||||
add_template_helper GitlabRoutingHelper
|
||||
|
||||
def test_email(recipient_email, subject, body)
|
||||
mail(to: recipient_email,
|
||||
|
|
|
@ -9,7 +9,6 @@ class Ability
|
|||
when CommitStatus then commit_status_abilities(user, subject)
|
||||
when Project then project_abilities(user, subject)
|
||||
when Issue then issue_abilities(user, subject)
|
||||
when ExternalIssue then external_issue_abilities(user, subject)
|
||||
when Note then note_abilities(user, subject)
|
||||
when ProjectSnippet then project_snippet_abilities(user, subject)
|
||||
when PersonalSnippet then personal_snippet_abilities(user, subject)
|
||||
|
@ -19,6 +18,7 @@ class Ability
|
|||
when GroupMember then group_member_abilities(user, subject)
|
||||
when ProjectMember then project_member_abilities(user, subject)
|
||||
when User then user_abilities
|
||||
when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project)
|
||||
else []
|
||||
end.concat(global_abilities(user))
|
||||
end
|
||||
|
@ -187,6 +187,8 @@ class Ability
|
|||
project_report_rules
|
||||
elsif team.guest?(user)
|
||||
project_guest_rules
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -228,6 +230,8 @@ class Ability
|
|||
:read_build,
|
||||
:read_container_image,
|
||||
:read_pipeline,
|
||||
:read_environment,
|
||||
:read_deployment
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -246,6 +250,8 @@ class Ability
|
|||
:push_code,
|
||||
:create_container_image,
|
||||
:update_container_image,
|
||||
:create_environment,
|
||||
:create_deployment
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -263,6 +269,8 @@ class Ability
|
|||
@project_master_rules ||= project_dev_rules + [
|
||||
:push_code_to_protected_branches,
|
||||
:update_project_snippet,
|
||||
:update_environment,
|
||||
:update_deployment,
|
||||
:admin_milestone,
|
||||
:admin_project_snippet,
|
||||
:admin_project_member,
|
||||
|
@ -273,7 +281,9 @@ class Ability
|
|||
:admin_commit_status,
|
||||
:admin_build,
|
||||
:admin_container_image,
|
||||
:admin_pipeline
|
||||
:admin_pipeline,
|
||||
:admin_environment,
|
||||
:admin_deployment
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -317,6 +327,8 @@ class Ability
|
|||
unless project.builds_enabled
|
||||
rules += named_abilities('build')
|
||||
rules += named_abilities('pipeline')
|
||||
rules += named_abilities('environment')
|
||||
rules += named_abilities('deployment')
|
||||
end
|
||||
|
||||
unless project.container_registry_enabled
|
||||
|
@ -511,10 +523,6 @@ class Ability
|
|||
end
|
||||
end
|
||||
|
||||
def external_issue_abilities(user, subject)
|
||||
project_abilities(user, subject.project)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def restricted_public_level?
|
||||
|
@ -533,7 +541,7 @@ class Ability
|
|||
def filter_confidential_issues_abilities(user, issue, rules)
|
||||
return rules if user.admin? || !issue.confidential?
|
||||
|
||||
unless issue.author == user || issue.assignee == user || issue.project.team.member?(user.id)
|
||||
unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER)
|
||||
rules.delete(:admin_issue)
|
||||
rules.delete(:read_issue)
|
||||
rules.delete(:update_issue)
|
||||
|
|
|
@ -24,7 +24,7 @@ class Blob < SimpleDelegator
|
|||
end
|
||||
|
||||
def only_display_raw?
|
||||
size && size > 5.megabytes
|
||||
size && truncated?
|
||||
end
|
||||
|
||||
def svg?
|
||||
|
|
|
@ -11,6 +11,8 @@ module Ci
|
|||
|
||||
scope :unstarted, ->() { where(runner_id: nil) }
|
||||
scope :ignore_failures, ->() { where(allow_failure: false) }
|
||||
scope :with_artifacts, ->() { where.not(artifacts_file: nil) }
|
||||
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
|
||||
|
||||
mount_uploader :artifacts_file, ArtifactUploader
|
||||
mount_uploader :artifacts_metadata, ArtifactUploader
|
||||
|
@ -38,7 +40,7 @@ module Ci
|
|||
new_build.save
|
||||
end
|
||||
|
||||
def retry(build)
|
||||
def retry(build, user = nil)
|
||||
new_build = Ci::Build.new(status: 'pending')
|
||||
new_build.ref = build.ref
|
||||
new_build.tag = build.tag
|
||||
|
@ -52,6 +54,7 @@ module Ci
|
|||
new_build.stage = build.stage
|
||||
new_build.stage_idx = build.stage_idx
|
||||
new_build.trigger_request = build.trigger_request
|
||||
new_build.user = user
|
||||
new_build.save
|
||||
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
|
||||
new_build
|
||||
|
@ -73,6 +76,17 @@ module Ci
|
|||
build.update_coverage
|
||||
build.execute_hooks
|
||||
end
|
||||
|
||||
after_transition any => [:success] do |build|
|
||||
if build.environment.present?
|
||||
service = CreateDeploymentService.new(build.project, build.user,
|
||||
environment: build.environment,
|
||||
sha: build.sha,
|
||||
ref: build.ref,
|
||||
tag: build.tag)
|
||||
service.execute(build)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def retryable?
|
||||
|
@ -83,10 +97,6 @@ module Ci
|
|||
!self.pipeline.statuses.latest.include?(self)
|
||||
end
|
||||
|
||||
def retry
|
||||
Ci::Build.retry(self)
|
||||
end
|
||||
|
||||
def depends_on_builds
|
||||
# Get builds of the same type
|
||||
latest_builds = self.pipeline.builds.latest
|
||||
|
@ -317,7 +327,7 @@ module Ci
|
|||
end
|
||||
|
||||
def artifacts?
|
||||
artifacts_file.exists?
|
||||
!artifacts_expired? && artifacts_file.exists?
|
||||
end
|
||||
|
||||
def artifacts_metadata?
|
||||
|
@ -328,11 +338,15 @@ module Ci
|
|||
Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry
|
||||
end
|
||||
|
||||
def erase_artifacts!
|
||||
remove_artifacts_file!
|
||||
remove_artifacts_metadata!
|
||||
end
|
||||
|
||||
def erase(opts = {})
|
||||
return false unless erasable?
|
||||
|
||||
remove_artifacts_file!
|
||||
remove_artifacts_metadata!
|
||||
erase_artifacts!
|
||||
erase_trace!
|
||||
update_erased!(opts[:erased_by])
|
||||
end
|
||||
|
@ -345,6 +359,25 @@ module Ci
|
|||
!self.erased_at.nil?
|
||||
end
|
||||
|
||||
def artifacts_expired?
|
||||
artifacts_expire_at && artifacts_expire_at < Time.now
|
||||
end
|
||||
|
||||
def artifacts_expire_in
|
||||
artifacts_expire_at - Time.now if artifacts_expire_at
|
||||
end
|
||||
|
||||
def artifacts_expire_in=(value)
|
||||
self.artifacts_expire_at =
|
||||
if value
|
||||
Time.now + ChronicDuration.parse(value)
|
||||
end
|
||||
end
|
||||
|
||||
def keep_artifacts!
|
||||
self.update(artifacts_expire_at: nil)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def erase_trace!
|
||||
|
@ -352,7 +385,7 @@ module Ci
|
|||
end
|
||||
|
||||
def update_erased!(user = nil)
|
||||
self.update(erased_by: user, erased_at: Time.now)
|
||||
self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
|
||||
end
|
||||
|
||||
def yaml_variables
|
||||
|
|
|
@ -76,8 +76,10 @@ module Ci
|
|||
builds.running_or_pending.each(&:cancel)
|
||||
end
|
||||
|
||||
def retry_failed
|
||||
builds.latest.failed.select(&:retryable?).each(&:retry)
|
||||
def retry_failed(user)
|
||||
builds.latest.failed.select(&:retryable?).each do |build|
|
||||
Ci::Build.retry(build, user)
|
||||
end
|
||||
end
|
||||
|
||||
def latest?
|
||||
|
@ -161,6 +163,10 @@ module Ci
|
|||
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
|
||||
end
|
||||
|
||||
def environments
|
||||
builds.where.not(environment: nil).success.pluck(:environment).uniq
|
||||
end
|
||||
|
||||
def notes
|
||||
Note.for_commit_id(sha)
|
||||
end
|
||||
|
|
16
app/models/concerns/access_requestable.rb
Normal file
16
app/models/concerns/access_requestable.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# == AccessRequestable concern
|
||||
#
|
||||
# Contains functionality related to objects that can receive request for access.
|
||||
#
|
||||
# Used by Project, and Group.
|
||||
#
|
||||
module AccessRequestable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def request_access(user)
|
||||
members.create(
|
||||
access_level: Gitlab::Access::DEVELOPER,
|
||||
user: user,
|
||||
requested_at: Time.now.utc)
|
||||
end
|
||||
end
|
|
@ -5,7 +5,7 @@ module Awardable
|
|||
has_many :award_emoji, as: :awardable, dependent: :destroy
|
||||
|
||||
if self < Participable
|
||||
participant :award_emoji
|
||||
participant :award_emoji_with_associations
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -34,8 +34,12 @@ module Awardable
|
|||
end
|
||||
end
|
||||
|
||||
def award_emoji_with_associations
|
||||
award_emoji.includes(:user)
|
||||
end
|
||||
|
||||
def grouped_awards(with_thumbs: true)
|
||||
awards = award_emoji.group_by(&:name)
|
||||
awards = award_emoji_with_associations.group_by(&:name)
|
||||
|
||||
if with_thumbs
|
||||
awards[AwardEmoji::UPVOTE_NAME] ||= []
|
||||
|
|
29
app/models/deployment.rb
Normal file
29
app/models/deployment.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
class Deployment < ActiveRecord::Base
|
||||
include InternalId
|
||||
|
||||
belongs_to :project, required: true, validate: true
|
||||
belongs_to :environment, required: true, validate: true
|
||||
belongs_to :user
|
||||
belongs_to :deployable, polymorphic: true
|
||||
|
||||
validates :sha, presence: true
|
||||
validates :ref, presence: true
|
||||
|
||||
delegate :name, to: :environment, prefix: true
|
||||
|
||||
def commit
|
||||
project.commit(sha)
|
||||
end
|
||||
|
||||
def commit_title
|
||||
commit.try(:title)
|
||||
end
|
||||
|
||||
def short_sha
|
||||
Commit.truncate_sha(sha)
|
||||
end
|
||||
|
||||
def last?
|
||||
self == environment.last_deployment
|
||||
end
|
||||
end
|
16
app/models/environment.rb
Normal file
16
app/models/environment.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class Environment < ActiveRecord::Base
|
||||
belongs_to :project, required: true, validate: true
|
||||
|
||||
has_many :deployments
|
||||
|
||||
validates :name,
|
||||
presence: true,
|
||||
uniqueness: { scope: :project_id },
|
||||
length: { within: 0..255 },
|
||||
format: { with: Gitlab::Regex.environment_name_regex,
|
||||
message: Gitlab::Regex.environment_name_regex_message }
|
||||
|
||||
def last_deployment
|
||||
deployments.last
|
||||
end
|
||||
end
|
|
@ -3,11 +3,18 @@ require 'carrierwave/orm/activerecord'
|
|||
class Group < Namespace
|
||||
include Gitlab::ConfigHelper
|
||||
include Gitlab::VisibilityLevel
|
||||
include AccessRequestable
|
||||
include Referable
|
||||
|
||||
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
|
||||
alias_method :members, :group_members
|
||||
has_many :users, through: :group_members
|
||||
has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members
|
||||
|
||||
has_many :owners,
|
||||
-> { where(members: { access_level: Gitlab::Access::OWNER }) },
|
||||
through: :group_members,
|
||||
source: :user
|
||||
|
||||
has_many :project_group_links, dependent: :destroy
|
||||
has_many :shared_projects, through: :project_group_links, source: :project
|
||||
has_many :notification_settings, dependent: :destroy, as: :source
|
||||
|
@ -58,6 +65,10 @@ class Group < Namespace
|
|||
"#{self.class.reference_prefix}#{name}"
|
||||
end
|
||||
|
||||
def web_url
|
||||
Gitlab::Routing.url_helpers.group_url(self)
|
||||
end
|
||||
|
||||
def human_name
|
||||
name
|
||||
end
|
||||
|
@ -83,10 +94,6 @@ class Group < Namespace
|
|||
end
|
||||
end
|
||||
|
||||
def owners
|
||||
@owners ||= group_members.owners.includes(:user).map(&:user)
|
||||
end
|
||||
|
||||
def add_users(user_ids, access_level, current_user = nil)
|
||||
user_ids.each do |user_id|
|
||||
Member.add_user(self.group_members, user_id, access_level, current_user)
|
||||
|
|
|
@ -51,10 +51,18 @@ class Issue < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.visible_to_user(user)
|
||||
return where(confidential: false) if user.blank?
|
||||
return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
|
||||
return all if user.admin?
|
||||
|
||||
where('issues.confidential = false OR (issues.confidential = true AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects.select(:id))
|
||||
where('
|
||||
issues.confidential IS NULL
|
||||
OR issues.confidential IS FALSE
|
||||
OR (issues.confidential = TRUE
|
||||
AND (issues.author_id = :user_id
|
||||
OR issues.assignee_id = :user_id
|
||||
OR issues.project_id IN(:project_ids)))',
|
||||
user_id: user.id,
|
||||
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
|
||||
end
|
||||
|
||||
def self.reference_prefix
|
||||
|
|
|
@ -26,20 +26,28 @@ class Member < ActiveRecord::Base
|
|||
allow_nil: true
|
||||
}
|
||||
|
||||
scope :invite, -> { where(user_id: nil) }
|
||||
scope :non_invite, -> { where("user_id IS NOT NULL") }
|
||||
scope :invite, -> { where.not(invite_token: nil) }
|
||||
scope :non_invite, -> { where(invite_token: nil) }
|
||||
scope :request, -> { where.not(requested_at: nil) }
|
||||
scope :non_request, -> { where(requested_at: nil) }
|
||||
scope :non_pending, -> { non_request.non_invite }
|
||||
|
||||
scope :guests, -> { where(access_level: GUEST) }
|
||||
scope :reporters, -> { where(access_level: REPORTER) }
|
||||
scope :developers, -> { where(access_level: DEVELOPER) }
|
||||
scope :masters, -> { where(access_level: MASTER) }
|
||||
scope :owners, -> { where(access_level: OWNER) }
|
||||
scope :owners_and_masters, -> { where(access_level: [OWNER, MASTER]) }
|
||||
|
||||
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
|
||||
|
||||
after_create :send_invite, if: :invite?
|
||||
after_create :create_notification_setting, unless: :invite?
|
||||
after_create :post_create_hook, unless: :invite?
|
||||
after_update :post_update_hook, unless: :invite?
|
||||
after_destroy :post_destroy_hook, unless: :invite?
|
||||
after_create :send_request, if: :request?
|
||||
after_create :create_notification_setting, unless: :pending?
|
||||
after_create :post_create_hook, unless: :pending?
|
||||
after_update :post_update_hook, unless: :pending?
|
||||
after_destroy :post_destroy_hook, unless: :pending?
|
||||
after_destroy :post_decline_request, if: :request?
|
||||
|
||||
delegate :name, :username, :email, to: :user, prefix: true
|
||||
|
||||
|
@ -96,10 +104,31 @@ class Member < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def real_source_type
|
||||
source_type
|
||||
end
|
||||
|
||||
def invite?
|
||||
self.invite_token.present?
|
||||
end
|
||||
|
||||
def request?
|
||||
requested_at.present?
|
||||
end
|
||||
|
||||
def pending?
|
||||
invite? || request?
|
||||
end
|
||||
|
||||
def accept_request
|
||||
return false unless request?
|
||||
|
||||
updated = self.update(requested_at: nil)
|
||||
after_accept_request if updated
|
||||
|
||||
updated
|
||||
end
|
||||
|
||||
def accept_invite!(new_user)
|
||||
return false unless invite?
|
||||
|
||||
|
@ -157,6 +186,10 @@ class Member < ActiveRecord::Base
|
|||
# override in subclass
|
||||
end
|
||||
|
||||
def send_request
|
||||
# override in subclass
|
||||
end
|
||||
|
||||
def post_create_hook
|
||||
system_hook_service.execute_hooks_for(self, :create)
|
||||
end
|
||||
|
@ -177,6 +210,14 @@ class Member < ActiveRecord::Base
|
|||
# override in subclass
|
||||
end
|
||||
|
||||
def after_accept_request
|
||||
post_create_hook
|
||||
end
|
||||
|
||||
def post_decline_request
|
||||
# override in subclass
|
||||
end
|
||||
|
||||
def system_hook_service
|
||||
SystemHooksService.new
|
||||
end
|
||||
|
|
|
@ -8,9 +8,6 @@ class GroupMember < Member
|
|||
validates_format_of :source_type, with: /\ANamespace\z/
|
||||
default_scope { where(source_type: SOURCE_TYPE) }
|
||||
|
||||
scope :with_group, ->(group) { where(source_id: group.id) }
|
||||
scope :with_user, ->(user) { where(user_id: user.id) }
|
||||
|
||||
def self.access_level_roles
|
||||
Gitlab::Access.options_with_owner
|
||||
end
|
||||
|
@ -23,6 +20,11 @@ class GroupMember < Member
|
|||
access_level
|
||||
end
|
||||
|
||||
# Because source_type is `Namespace`...
|
||||
def real_source_type
|
||||
'Group'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_invite
|
||||
|
@ -31,6 +33,12 @@ class GroupMember < Member
|
|||
super
|
||||
end
|
||||
|
||||
def send_request
|
||||
notification_service.new_group_access_request(self)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def post_create_hook
|
||||
notification_service.new_group_member(self)
|
||||
|
||||
|
@ -56,4 +64,10 @@ class GroupMember < Member
|
|||
|
||||
super
|
||||
end
|
||||
|
||||
def post_decline_request
|
||||
notification_service.decline_group_access_request(self)
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,8 +11,6 @@ class ProjectMember < Member
|
|||
default_scope { where(source_type: SOURCE_TYPE) }
|
||||
|
||||
scope :in_project, ->(project) { where(source_id: project.id) }
|
||||
scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
|
||||
scope :with_user, ->(user) { where(user_id: user.id) }
|
||||
|
||||
before_destroy :delete_member_todos
|
||||
|
||||
|
@ -84,7 +82,7 @@ class ProjectMember < Member
|
|||
Gitlab::Access.sym_options
|
||||
end
|
||||
|
||||
def access_roles
|
||||
def access_level_roles
|
||||
Gitlab::Access.options
|
||||
end
|
||||
end
|
||||
|
@ -113,6 +111,12 @@ class ProjectMember < Member
|
|||
super
|
||||
end
|
||||
|
||||
def send_request
|
||||
notification_service.new_project_access_request(self)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def post_create_hook
|
||||
unless owner?
|
||||
event_service.join_project(self.project, self.user)
|
||||
|
@ -148,6 +152,12 @@ class ProjectMember < Member
|
|||
super
|
||||
end
|
||||
|
||||
def post_decline_request
|
||||
notification_service.decline_project_access_request(self)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def event_service
|
||||
EventCreateService.new
|
||||
end
|
||||
|
|
|
@ -88,22 +88,9 @@ class Note < ActiveRecord::Base
|
|||
table = arel_table
|
||||
pattern = "%#{query}%"
|
||||
|
||||
found_notes = joins('LEFT JOIN issues ON issues.id = noteable_id').
|
||||
where(table[:note].matches(pattern))
|
||||
|
||||
if as_user
|
||||
found_notes.where('
|
||||
issues.confidential IS NULL
|
||||
OR issues.confidential IS FALSE
|
||||
OR (issues.confidential IS TRUE
|
||||
AND (issues.author_id = :user_id
|
||||
OR issues.assignee_id = :user_id
|
||||
OR issues.project_id IN(:project_ids)))',
|
||||
user_id: as_user.id,
|
||||
project_ids: as_user.authorized_projects.select(:id))
|
||||
else
|
||||
found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
|
||||
end
|
||||
Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
|
||||
where(table[:note].matches(pattern)).
|
||||
merge(Issue.visible_to_user(as_user))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -200,6 +187,10 @@ class Note < ActiveRecord::Base
|
|||
award_emoji_supported? && contains_emoji_only?
|
||||
end
|
||||
|
||||
def emoji_awardable?
|
||||
!system?
|
||||
end
|
||||
|
||||
def clear_blank_line_code!
|
||||
self.line_code = nil if self.line_code.blank?
|
||||
end
|
||||
|
|
|
@ -7,7 +7,6 @@ class NotificationSetting < ActiveRecord::Base
|
|||
belongs_to :source, polymorphic: true
|
||||
|
||||
validates :user, presence: true
|
||||
validates :source, presence: true
|
||||
validates :level, presence: true
|
||||
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
|
||||
message: "already exists in source",
|
||||
|
|
|
@ -5,6 +5,7 @@ class Project < ActiveRecord::Base
|
|||
include Gitlab::ShellAdapter
|
||||
include Gitlab::VisibilityLevel
|
||||
include Gitlab::CurrentSettings
|
||||
include AccessRequestable
|
||||
include Referable
|
||||
include Sortable
|
||||
include AfterCommitQueue
|
||||
|
@ -80,7 +81,7 @@ class Project < ActiveRecord::Base
|
|||
has_one :jira_service, dependent: :destroy
|
||||
has_one :redmine_service, dependent: :destroy
|
||||
has_one :custom_issue_tracker_service, dependent: :destroy
|
||||
has_one :gitlab_issue_tracker_service, dependent: :destroy
|
||||
has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
|
||||
has_one :external_wiki_service, dependent: :destroy
|
||||
|
||||
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
|
||||
|
@ -102,8 +103,9 @@ class Project < ActiveRecord::Base
|
|||
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
|
||||
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
|
||||
has_many :protected_branches, dependent: :destroy
|
||||
has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
|
||||
has_many :users, through: :project_members
|
||||
has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
|
||||
alias_method :members, :project_members
|
||||
has_many :users, -> { where(members: { requested_at: nil }) }, through: :project_members
|
||||
has_many :deploy_keys_projects, dependent: :destroy
|
||||
has_many :deploy_keys, through: :deploy_keys_projects
|
||||
has_many :users_star_projects, dependent: :destroy
|
||||
|
@ -125,6 +127,8 @@ class Project < ActiveRecord::Base
|
|||
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
|
||||
has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
|
||||
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
|
||||
has_many :environments, dependent: :destroy
|
||||
has_many :deployments, dependent: :destroy
|
||||
|
||||
accepts_nested_attributes_for :variables, allow_destroy: true
|
||||
|
||||
|
@ -146,7 +150,6 @@ class Project < ActiveRecord::Base
|
|||
message: Gitlab::Regex.project_path_regex_message }
|
||||
validates :issues_enabled, :merge_requests_enabled,
|
||||
:wiki_enabled, inclusion: { in: [true, false] }
|
||||
validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true
|
||||
validates :namespace, presence: true
|
||||
validates_uniqueness_of :name, scope: :namespace_id
|
||||
validates_uniqueness_of :path, scope: :namespace_id
|
||||
|
@ -589,10 +592,6 @@ class Project < ActiveRecord::Base
|
|||
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
|
||||
end
|
||||
|
||||
def can_have_issues_tracker_id?
|
||||
self.issues_enabled && !self.default_issues_tracker?
|
||||
end
|
||||
|
||||
def build_missing_services
|
||||
services_templates = Service.where(template: true)
|
||||
|
||||
|
@ -685,16 +684,6 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def project_member_by_name_or_email(name = nil, email = nil)
|
||||
user = users.find_by('name like ? or email like ?', name, email)
|
||||
project_members.where(user: user) if user
|
||||
end
|
||||
|
||||
# Get Team Member record by user id
|
||||
def project_member_by_id(user_id)
|
||||
project_members.find_by(user_id: user_id)
|
||||
end
|
||||
|
||||
def name_with_namespace
|
||||
@name_with_namespace ||= begin
|
||||
if namespace
|
||||
|
@ -704,6 +693,7 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
end
|
||||
alias_method :human_name, :name_with_namespace
|
||||
|
||||
def path_with_namespace
|
||||
if namespace
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue