Imported Upstream version 8.9.0+dfsg

This commit is contained in:
Praveen Arimbrathodiyil 2016-06-22 15:30:34 +05:30
parent bcb40d0491
commit 0f42b0dbae
500 changed files with 23391 additions and 2691 deletions

View file

@ -1,19 +1,29 @@
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased) v 8.9.0 (unreleased)
- Fix builds API response not including commit data
- Fix error when CI job variables key specified but not defined
- Fix pipeline status when there are no builds in pipeline
- Fix Error 500 when using closes_issues API with an external issue tracker - Fix Error 500 when using closes_issues API with an external issue tracker
- Add more information into RSS feed for issues (Alexander Matyushentsev) - Add more information into RSS feed for issues (Alexander Matyushentsev)
- Bulk assign/unassign labels to issues. - Bulk assign/unassign labels to issues.
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters) - Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
- Show Star and Fork buttons on mobile.
- Performance improvements on RelativeLinkFilter
- Fix endless redirections when accessing user OAuth applications when they are disabled - Fix endless redirections when accessing user OAuth applications when they are disabled
- Allow enabling wiki page events from Webhook management UI - Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0 - Bump rouge to 1.11.0
- Fix issue with arrow keys not working in search autocomplete dropdown - 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 - Fix an issue where note polling stopped working if a window was in the
background during a refresh. background during a refresh.
- Pre-processing Markdown now only happens when needed
- Make EmailsOnPushWorker use Sidekiq mailers queue - Make EmailsOnPushWorker use Sidekiq mailers queue
- Redesign all Devise emails. !4297
- Don't show 'Leave Project' to group members
- Fix wiki page events' webhook to point to the wiki repository - Fix wiki page events' webhook to point to the wiki repository
- Add a border around images to differentiate them from the background.
- Don't show tags for revert and cherry-pick operations - Don't show tags for revert and cherry-pick operations
- Show image ID on registry page
- Fix issue todo not remove when leave project !4150 (Long Nguyen) - Fix issue todo not remove when leave project !4150 (Long Nguyen)
- Allow customisable text on the 'nearly there' page after a user signs up - 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 - Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
@ -22,12 +32,18 @@ v 8.9.0 (unreleased)
- Added descriptions to notification settings dropdown - Added descriptions to notification settings dropdown
- Improve note validation to prevent errors when creating invalid note via API - Improve note validation to prevent errors when creating invalid note via API
- Reduce number of fog gem dependencies - Reduce number of fog gem dependencies
- Add number of merge requests for a given milestone to the milestones view.
- Implement a fair usage of shared runners
- Remove project notification settings associated with deleted projects - Remove project notification settings associated with deleted projects
- Fix 404 page when viewing TODOs that contain milestones or labels in different 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 - 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 - Fix Error 500 when viewing a blob with binary characters after the 1024-byte mark
- Redesign navigation for project pages - Redesign navigation for project pages
- Fix images in sign-up confirmation email
- Added shortcut 'y' for copying a files content hash URL #14470
- Fix groups API to list only user's accessible projects - Fix groups API to list only user's accessible projects
- Fix horizontal scrollbar for long commit message.
- GitLab Performance Monitoring now tracks the total method execution time and call count per method
- Add Environments and Deployments - Add Environments and Deployments
- Redesign account and email confirmation emails - Redesign account and email confirmation emails
- Don't fail builds for projects that are deleted - Don't fail builds for projects that are deleted
@ -35,30 +51,47 @@ v 8.9.0 (unreleased)
- `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix - `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
- Bump nokogiri to 1.6.8 - Bump nokogiri to 1.6.8
- Use gitlab-shell v3.0.0 - Use gitlab-shell v3.0.0
- Fixed alignment of download dropdown in merge requests
- Upgrade to jQuery 2 - Upgrade to jQuery 2
- Adds selected branch name to the dropdown toggle
- Add API endpoint for Sidekiq Metrics !4653
- Refactoring Award Emoji with API support for Issues and MergeRequests
- Use Knapsack to evenly distribute tests across multiple nodes - Use Knapsack to evenly distribute tests across multiple nodes
- Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
- Don't allow MRs to be merged when commits were added since the last review / page load - Don't allow MRs to be merged when commits were added since the last review / page load
- Add DB index on users.state - Add DB index on users.state
- Limit email on push diff size to 30 files / 150 KB
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - 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) - Changed the Slack build message to use the singular duration if necessary (Aran Koning)
- Links from a wiki page to other wiki pages should be rewritten as expected - 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) - 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 - Fix issues filter when ordering by milestone
- Disable SAML account unlink feature
- Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3 - 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) - 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 - TeamCity Service: Fix URL handling when base URL contains a path
- Todos will display target state if issuable target is 'Closed' or 'Merged' - Todos will display target state if issuable target is 'Closed' or 'Merged'
- Validate only and except regexp
- Fix bug when sorting issues by milestone due date and filtering by two or more labels - Fix bug when sorting issues by milestone due date and filtering by two or more labels
- POST to API /projects/:id/runners/:runner_id would give 409 if the runner was already enabled for this project
- Add support for using Yubikeys (U2F) for two-factor authentication - Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore - Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature - Remove 'main language' feature
- Toggle whitespace button now available for compare branches diffs #17881
- Pipelines can be canceled only when there are running builds - Pipelines can be canceled only when there are running builds
- Allow authentication using personal access tokens
- Use downcased path to container repository as this is expected path by Docker - Use downcased path to container repository as this is expected path by Docker
- Allow to use CI token to fetch LFS objects
- Custom notification settings
- Projects pending deletion will render a 404 page - Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails - Measure queue duration between gitlab-workhorse and Rails
- Added Gfm autocomplete for labels
- Added edit note 'up' shortcut documentation to the help panel and docs screenshot #18114
- Make Omniauth providers specs to not modify global configuration - Make Omniauth providers specs to not modify global configuration
- Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir)
- Make authentication service for Container Registry to be compatible with < Docker 1.11 - Make authentication service for Container Registry to be compatible with < Docker 1.11
- Make it possible to lock a runner from being enabled for other projects
- Add Application Setting to configure Container Registry token expire delay (default 5min) - Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav - Cache assigned issue and merge request counts in sidebar nav
- Use Knapsack only in CI environment - Use Knapsack only in CI environment
@ -74,8 +107,11 @@ v 8.9.0 (unreleased)
- Replace Colorize with Rainbow for coloring console output in Rake tasks. - Replace Colorize with Rainbow for coloring console output in Rake tasks.
- Add workhorse controller and API helpers - Add workhorse controller and API helpers
- An indicator is now displayed at the top of the comment field for confidential issues. - 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 - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
- Dropdown for `.gitlab-ci.yml` templates
- Improve issuables APIs performance when accessing notes !4471 - Improve issuables APIs performance when accessing notes !4471
- Add sorting dropdown to tags page !4423
- External links now open in a new tab - External links now open in a new tab
- Prevent default actions of disabled buttons and links - Prevent default actions of disabled buttons and links
- Markdown editor now correctly resets the input value on edit cancellation !4175 - Markdown editor now correctly resets the input value on edit cancellation !4175
@ -83,6 +119,7 @@ v 8.9.0 (unreleased)
- Improved UX of date pickers on issue & milestone forms - Improved UX of date pickers on issue & milestone forms
- Cache on the database if a project has an active external issue tracker. - 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 - Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav
- GitLab project import and export functionality
- All classes in the Banzai::ReferenceParser namespace are now instrumented - All classes in the Banzai::ReferenceParser namespace are now instrumented
- Remove deprecated issues_tracker and issues_tracker_id from project model - Remove deprecated issues_tracker and issues_tracker_id from project model
- Allow users to create confidential issues in private projects - Allow users to create confidential issues in private projects
@ -108,9 +145,37 @@ v 8.8.5 (unreleased)
- Fix importer for GitHub comments on diff - Fix importer for GitHub comments on diff
- Disable Webhooks before proceeding with the GitHub import - Disable Webhooks before proceeding with the GitHub import
- Fix incremental trace upload API when using multi-byte UTF-8 chars in trace - Fix incremental trace upload API when using multi-byte UTF-8 chars in trace
- Set inverse_of for Project/Service association to reduce the number of queries
- Update tanuki logo highlight/loading colors
- Remove explicit Gitlab::Metrics.action assignments, are already automatic.
- Use Git cached counters for branches and tags on project page
- Cache participable participants in an instance variable.
- Filter parameters for request_uri value on instrumented transactions.
- Remove duplicated keys add UNIQUE index to keys fingerprint column
- ExtractsPath get ref_names from repository cache, if not there access git.
- Cache user todo counts from TodoService
- Ensure Todos counters doesn't count Todos for projects pending delete
- Add tooltip to pin/unpin navbar
- Add left/right arrows horizontal navigation
- Add new sub nav style to Wiki and Graphs sub navigation
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
- Banzai::Filter::UploadLinkFilter use XPath instead CSS expressions
- Banzai::Filter::ExternalLinkFilter use XPath instead CSS expressions
v 8.8.4 v 8.8.4
- Fix LDAP-based login for users with 2FA enabled. !4493 - 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 v 8.8.3
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
@ -226,6 +291,9 @@ v 8.8.0
v 8.7.7 v 8.7.7
- Fix import by `Any Git URL` broken if the URL contains a space - 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 v 8.7.6
- Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
@ -388,6 +456,11 @@ v 8.7.0
- Add RAW build trace output and button on build page - Add RAW build trace output and button on build page
- Add incremental build trace update into CI API - 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 v 8.6.8
- Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API - Prevent privilege escalation via notes API
@ -542,6 +615,10 @@ v 8.6.0
- Trigger a todo for mentions on commits page - Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests - 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 v 8.5.12
- Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API - Prevent privilege escalation via notes API
@ -703,6 +780,10 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos - Add Todos
v 8.4.11
- Prevent unauthorized access to other projects build traces
- Forbid scripting for wiki files
v 8.4.10 v 8.4.10
- Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API - Prevent privilege escalation via notes API
@ -839,6 +920,10 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up - Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching - 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 v 8.3.9
- Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API - Prevent privilege escalation via notes API
@ -957,6 +1042,10 @@ v 8.3.0
- Expose Git's version in the admin area - Expose Git's version in the admin area
- Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye) - 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 v 8.2.5
- Prevent privilege escalation via "impersonate" feature - Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API - Prevent privilege escalation via notes API

View file

@ -48,11 +48,11 @@ gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1' gem 'u2f', '~> 0.2.1'
# Browser detection # Browser detection
gem "browser", '~> 2.0.3' gem "browser", '~> 2.2'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
gem "gitlab_git", '~> 10.0' gem "gitlab_git", '~> 10.2'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
@ -221,13 +221,12 @@ gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0' gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.2' gem 'font-awesome-rails', '~> 4.6.1'
gem 'gitlab_emoji', '~> 0.3.0' gem 'gitlab_emoji', '~> 0.3.0'
gem 'gon', '~> 6.0.1' gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0' gem 'jquery-rails', '~> 4.1.0'
gem 'jquery-ui-rails', '~> 5.0.0' gem 'jquery-ui-rails', '~> 5.0.0'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.3.0' gem 'request_store', '~> 1.3.0'
gem 'select2-rails', '~> 3.5.9' gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1' gem 'virtus', '~> 1.0.1'
@ -331,7 +330,7 @@ gem "newrelic_rpm", '~> 3.14'
gem 'octokit', '~> 4.3.0' gem 'octokit', '~> 4.3.0'
gem "mail_room", "~> 0.7" gem "mail_room", "~> 0.8"
gem 'email_reply_parser', '~> 0.5.8' gem 'email_reply_parser', '~> 0.5.8'

View file

@ -98,7 +98,7 @@ GEM
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
brakeman (3.3.2) brakeman (3.3.2)
browser (2.0.3) browser (2.2.0)
builder (3.2.2) builder (3.2.2)
bullet (5.0.0) bullet (5.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -246,7 +246,7 @@ GEM
fog-xml (0.1.2) fog-xml (0.1.2)
fog-core fog-core
nokogiri (~> 1.5, >= 1.5.11) nokogiri (~> 1.5, >= 1.5.11)
font-awesome-rails (4.5.0.1) font-awesome-rails (4.6.1.0)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
foreman (0.78.0) foreman (0.78.0)
thor (~> 0.19.1) thor (~> 0.19.1)
@ -277,7 +277,7 @@ GEM
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab_emoji (0.3.1) gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1) gemojione (~> 2.2, >= 2.2.1)
gitlab_git (10.1.3) gitlab_git (10.2.0)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
@ -398,7 +398,7 @@ GEM
systemu (~> 2.6.2) systemu (~> 2.6.2)
mail (2.6.4) mail (2.6.4)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.7.0) mail_room (0.8.0)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.2) mime-types (2.99.2)
mimemagic (0.3.0) mimemagic (0.3.0)
@ -556,7 +556,6 @@ GEM
rainbow (2.1.0) rainbow (2.1.0)
raindrops (0.15.0) raindrops (0.15.0)
rake (10.5.0) rake (10.5.0)
raphael-rails (2.1.2)
rb-fsevent (0.9.6) rb-fsevent (0.9.6)
rb-inotify (0.9.5) rb-inotify (0.9.5)
ffi (>= 0.5.0) ffi (>= 0.5.0)
@ -834,7 +833,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
brakeman (~> 3.3.0) brakeman (~> 3.3.0)
browser (~> 2.0.3) browser (~> 2.2)
bullet bullet
bundler-audit bundler-audit
byebug byebug
@ -867,7 +866,7 @@ DEPENDENCIES
fog-google (~> 0.3) fog-google (~> 0.3)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
font-awesome-rails (~> 4.2) font-awesome-rails (~> 4.6.1)
foreman foreman
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
@ -875,7 +874,7 @@ DEPENDENCIES
github-markup (~> 1.3.1) github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.3.0) gitlab_emoji (~> 0.3.0)
gitlab_git (~> 10.0) gitlab_git (~> 10.2)
gitlab_meta (= 7.0) gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0) gollum-lib (~> 4.1.0)
@ -900,7 +899,7 @@ DEPENDENCIES
license_finder license_finder
licensee (~> 8.0.0) licensee (~> 8.0.0)
loofah (~> 2.0.3) loofah (~> 2.0.3)
mail_room (~> 0.7) mail_room (~> 0.8)
method_source (~> 0.8) method_source (~> 0.8)
minitest (~> 5.7.0) minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6) mousetrap-rails (~> 1.4.6)
@ -938,7 +937,6 @@ DEPENDENCIES
rails (= 4.2.6) rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0) rainbow (~> 2.1.0)
raphael-rails (~> 2.1.2)
rblineprof rblineprof
rdoc (~> 3.6) rdoc (~> 3.6)
recaptcha (~> 3.0) recaptcha (~> 3.0)

View file

@ -1 +1 @@
8.9.0-rc4 8.9.0

View file

@ -27,6 +27,11 @@ class @LabelManager
$btn = $(e.currentTarget) $btn = $(e.currentTarget)
$label = $("##{$btn.data('domId')}") $label = $("##{$btn.data('domId')}")
action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add' action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add'
# Make sure tooltip will hide
$tooltip = $ "##{$btn.find('.has-tooltip:visible').attr('aria-describedby')}"
$tooltip.tooltip 'destroy'
_this.toggleLabelPriority($label, action) _this.toggleLabelPriority($label, action)
toggleLabelPriority: ($label, action, persistState = true) -> toggleLabelPriority: ($label, action, persistState = true) ->
@ -42,10 +47,10 @@ class @LabelManager
$from = @prioritizedLabels $from = @prioritizedLabels
if $from.find('li').length is 1 if $from.find('li').length is 1
$from.find('.empty-message').show() $from.find('.empty-message').removeClass('hidden')
if not $target.find('li').length if not $target.find('li').length
$target.find('.empty-message').hide() $target.find('.empty-message').addClass('hidden')
$label.detach().appendTo($target) $label.detach().appendTo($target)
@ -54,6 +59,9 @@ class @LabelManager
if action is 'remove' if action is 'remove'
xhr = $.ajax url: url, type: 'DELETE' xhr = $.ajax url: url, type: 'DELETE'
# Restore empty message
$from.find('.empty-message').removeClass('hidden') unless $from.find('li').length
else else
xhr = @savePrioritySort($label, action) xhr = @savePrioritySort($label, action)

View file

@ -7,6 +7,7 @@
labelsPath: "/api/:version/projects/:id/labels" labelsPath: "/api/:version/projects/:id/labels"
licensePath: "/api/:version/licenses/:key" licensePath: "/api/:version/licenses/:key"
gitignorePath: "/api/:version/gitignores/:key" gitignorePath: "/api/:version/gitignores/:key"
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key"
group: (group_id, callback) -> group: (group_id, callback) ->
url = Api.buildUrl(Api.groupPath) url = Api.buildUrl(Api.groupPath)
@ -110,6 +111,12 @@
$.get url, (gitignore) -> $.get url, (gitignore) ->
callback(gitignore) callback(gitignore)
gitlabCiYml: (key, callback) ->
url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key)
$.get url, (file) ->
callback(file)
buildUrl: (url) -> buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root? url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version) return url.replace(':version', gon.api_version)

View file

@ -32,10 +32,6 @@
#= require bootstrap/tooltip #= require bootstrap/tooltip
#= require bootstrap/popover #= require bootstrap/popover
#= require select2 #= require select2
#= require raphael
#= require g.raphael
#= require g.bar
#= require branch-graph
#= require ace/ace #= require ace/ace
#= require ace/ext-searchbox #= require ace/ext-searchbox
#= require underscore #= require underscore
@ -128,7 +124,7 @@ $ ->
gl.utils.preventDisabledButtons() gl.utils.preventDisabledButtons()
bootstrapBreakpoint = bp.getBreakpointSize() 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 # Click a .js-select-on-focus field, select the contents
$(".js-select-on-focus").on "focusin", -> $(".js-select-on-focus").on "focusin", ->
@ -258,3 +254,47 @@ $ ->
gl.awardsHandler = new AwardsHandler() gl.awardsHandler = new AwardsHandler()
checkInitialSidebarSize() checkInitialSidebarSize()
new Aside() new Aside()
# Sidenav pinning
if $(window).width() < 1440 and $.cookie('pin_nav') is 'true'
$.cookie('pin_nav', 'false', { path: '/' })
$('.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()
$pinBtn = $(e.currentTarget)
$page = $ '.page-with-sidebar'
$topNav = $ '.navbar-fixed-top'
$tooltip = $ "##{$pinBtn.attr('aria-describedby')}"
doPinNav = not $page.is('.page-sidebar-pinned')
tooltipText = 'Pin navigation'
$(this).toggleClass 'is-active'
if doPinNav
$page.addClass('page-sidebar-pinned')
$topNav.addClass('header-pinned-nav')
else
$tooltip.remove() # Remove it immediately when collapsing the sidebar
$page.removeClass('page-sidebar-pinned')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
$topNav.removeClass('header-pinned-nav')
.toggleClass('header-collapsed header-expanded')
# Save settings
$.cookie 'pin_nav', doPinNav, { path: '/' }
if $.cookie('pin_nav') is 'true' or doPinNav
tooltipText = 'Unpin navigation'
# Update tooltip text immediately
$tooltip.find('.tooltip-inner').text(tooltipText)
# Persist tooltip title
$pinBtn.attr('title', tooltipText).tooltip('fixTitle')

View file

@ -40,7 +40,7 @@ class @AwardsHandler
$menu = $ '.emoji-menu' $menu = $ '.emoji-menu'
if $addBtn.hasClass 'js-note-emoji' 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 else
$addBtn.closest('.js-awards-block').addClass 'current' $addBtn.closest('.js-awards-block').addClass 'current'

View file

@ -0,0 +1,23 @@
#= require blob/template_selector
class @BlobCiYamlSelector extends TemplateSelector
requestFile: (query) ->
Api.gitlabCiYml query.name, @requestFileSuccess.bind(@)
class @BlobCiYamlSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitlab-ci-yml-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobCiYamlSelector(
pattern: /(.gitlab-ci.yml)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
dropdown: $dropdown,
editor: @editor
)

View file

@ -1,58 +1,5 @@
class @BlobGitignoreSelector #= require blob/template_selector
constructor: (opts) ->
{
@dropdown
@editor
@$wrapper = @dropdown.closest('.gitignore-selector')
@$filenameInput = $('#file_name')
@data = @dropdown.data('filenames')
} = opts
@dropdown.glDropdown( class @BlobGitignoreSelector extends TemplateSelector
data: @data, requestFile: (query) ->
filterable: true, Api.gitignoreText query.name, @requestFileSuccess.bind(@)
selectable: true,
search:
fields: ['name']
clicked: @onClick
text: (gitignore) ->
gitignore.name
)
@toggleGitignoreSelector()
@bindEvents()
bindEvents: ->
@$filenameInput
.on 'keyup blur', (e) =>
@toggleGitignoreSelector()
toggleGitignoreSelector: ->
filename = @$filenameInput.val() or $('.editor-file-name').text().trim()
@$wrapper.toggleClass 'hidden', filename isnt '.gitignore'
onClick: (item, el, e) =>
e.preventDefault()
@requestIgnoreFile(item.name)
requestIgnoreFile: (name) ->
Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@)
requestIgnoreFileSuccess: (gitignore) ->
@editor.setValue(gitignore.content, 1)
@editor.focus()
class @BlobGitignoreSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitignore-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobGitignoreSelector(
dropdown: $dropdown,
editor: @editor
)

View file

@ -0,0 +1,17 @@
class @BlobGitignoreSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitignore-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobGitignoreSelector(
pattern: /(.gitignore)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
dropdown: $dropdown,
editor: @editor
)

View file

@ -1,30 +1,9 @@
class @BlobLicenseSelector #= require blob/template_selector
licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i
constructor: (editor) -> class @BlobLicenseSelector extends TemplateSelector
@$licenseSelector = $('.js-license-selector') requestFile: (query) ->
$fileNameInput = $('#file_name')
initialFileNameValue = if $fileNameInput.length
$fileNameInput.val()
else if $('.editor-file-name').length
$('.editor-file-name').text().trim()
@toggleLicenseSelector(initialFileNameValue)
if $fileNameInput
$fileNameInput.on 'keyup blur', (e) =>
@toggleLicenseSelector($(e.target).val())
$('select.license-select').on 'change', (e) ->
data = data =
project: $(this).data('project') project: @dropdown.data('project')
fullname: $(this).data('fullname') fullname: @dropdown.data('fullname')
Api.licenseText $(this).val(), data, (license) ->
editor.setValue(license.content, -1)
toggleLicenseSelector: (fileName) => Api.licenseText query.id, data, @requestFileSuccess.bind(@)
if @licenseRegex.test(fileName)
@$licenseSelector.show()
else
@$licenseSelector.hide()

View file

@ -0,0 +1,17 @@
class @BlobLicenseSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-license-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobLicenseSelector(
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
editor: @editor
)

View file

@ -12,8 +12,10 @@ class @EditBlob
$("#file-content").val(@editor.getValue()) $("#file-content").val(@editor.getValue())
@initModePanesAndLinks() @initModePanesAndLinks()
new BlobLicenseSelector(@editor)
new BlobGitignoreSelectors(editor: @editor) new BlobLicenseSelectors { @editor }
new BlobGitignoreSelectors { @editor }
new BlobCiYamlSelectors { @editor }
initModePanesAndLinks: -> initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane") @$editModePanes = $(".js-edit-mode-pane")

View file

@ -0,0 +1,56 @@
class @TemplateSelector
constructor: (opts = {}) ->
{
@dropdown,
@data,
@pattern,
@wrapper,
@editor,
@fileEndpoint,
@$input = $('#file_name')
} = opts
@buildDropdown()
@bindEvents()
@onFilenameUpdate()
buildDropdown: ->
@dropdown.glDropdown(
data: @data,
filterable: true,
selectable: true,
search:
fields: ['name']
clicked: @onClick
text: (item) ->
item.name
)
bindEvents: ->
@$input.on('keyup blur', (e) =>
@onFilenameUpdate()
)
onFilenameUpdate: ->
return unless @$input.length
filenameMatches = @pattern.test(@$input.val().trim())
if not filenameMatches
@wrapper.addClass('hidden')
return
@wrapper.removeClass('hidden')
onClick: (item, el, e) =>
e.preventDefault()
@requestFile(item)
requestFile: (item) ->
# To be implemented on the extending class
# e.g.
# Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess: (file) ->
@editor.setValue(file.content, 1)
@editor.focus()

View file

@ -29,6 +29,7 @@ class Dispatcher
new Todos() new Todos()
when 'projects:milestones:new', 'projects:milestones:edit' when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode() new ZenMode()
new DueDateSelect()
new GLForm($('.milestone-form')) new GLForm($('.milestone-form'))
when 'groups:milestones:new' when 'groups:milestones:new'
new ZenMode() new ZenMode()
@ -72,13 +73,12 @@ class Dispatcher
new Diff() new Diff()
new ZenMode() new ZenMode()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'projects:commits:show' when 'projects:commits:show', 'projects:activity'
shortcut_handler = new ShortcutsNavigation()
when 'projects:activity'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'projects:show' when 'projects:show'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new NotificationsForm()
new TreeView() if $('#tree-slider').length new TreeView() if $('#tree-slider').length
when 'groups:activity' when 'groups:activity'
new Activities() new Activities()
@ -100,6 +100,7 @@ class Dispatcher
when 'projects:blob:show', 'projects:blame:show' when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter() new LineHighlighter()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ShortcutsBlob true
when 'projects:labels:new', 'projects:labels:edit' when 'projects:labels:new', 'projects:labels:edit'
new Labels() new Labels()
when 'projects:labels:index' when 'projects:labels:index'
@ -129,19 +130,21 @@ class Dispatcher
shortcut_handler = new ShortcutsDashboardNavigation() shortcut_handler = new ShortcutsDashboardNavigation()
when 'profiles' when 'profiles'
new Profile() new Profile()
new NotificationsForm()
new NotificationsDropdown()
when 'projects' when 'projects'
new Project() new Project()
new ProjectAvatar() new ProjectAvatar()
switch path[1] switch path[1]
when 'compare'
shortcut_handler = new ShortcutsNavigation()
when 'edit' when 'edit'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ProjectNew() new ProjectNew()
when 'new' when 'new'
new ProjectNew() new ProjectNew()
when 'show' when 'show'
new ProjectNew()
new ProjectShow() new ProjectShow()
new NotificationsDropdown()
when 'wikis' when 'wikis'
new Wikis() new Wikis()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
@ -150,9 +153,9 @@ class Dispatcher
when 'snippets' when 'snippets'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ZenMode() if path[2] == 'show' new ZenMode() if path[2] == 'show'
when 'labels', 'graphs' when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \
shortcut_handler = new ShortcutsNavigation() 'milestones', 'project_members', 'deploy_keys', 'builds', \
when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
# If we haven't installed a custom shortcut handler, install the default one # If we haven't installed a custom shortcut handler, install the default one

View file

@ -1,5 +1,21 @@
class @DueDateSelect class @DueDateSelect
constructor: -> 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') $loading = $('.js-issuable-update .due_date')
.find('.block-loading') .find('.block-loading')
.hide() .hide()
@ -32,7 +48,7 @@ class @DueDateSelect
date = new Date value.replace(new RegExp('-', 'g'), ',') date = new Date value.replace(new RegExp('-', 'g'), ',')
mediumDate = $.datepicker.formatDate 'M d, yy', date mediumDate = $.datepicker.formatDate 'M d, yy', date
else else
mediumDate = 'None' mediumDate = 'No due date'
data = {} data = {}
data[abilityName] = {} data[abilityName] = {}
@ -50,7 +66,8 @@ class @DueDateSelect
$selectbox.hide() $selectbox.hide()
$value.css('display', '') $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) $sidebarValue.html(mediumDate)
if value isnt '' if value isnt ''

View file

@ -15,6 +15,9 @@ GitLab.GfmAutoComplete =
Members: Members:
template: '<li>${username} <small>${title}</small></li>' template: '<li>${username} <small>${title}</small></li>'
Labels:
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
# Issues and MergeRequests # Issues and MergeRequests
Issues: Issues:
template: '<li><small>${id}</small> ${title}</li>' template: '<li><small>${id}</small> ${title}</li>'
@ -176,6 +179,25 @@ GitLab.GfmAutoComplete =
title: sanitize(m.title) title: sanitize(m.title)
search: "#{m.iid} #{m.title}" search: "#{m.iid} #{m.title}"
@input.atwho
at: '~'
alias: 'labels'
searchKey: 'search'
displayTpl: @Labels.template
insertTpl: '${atwho-at}${title}'
callbacks:
beforeSave: (merges) ->
sanitizeLabelTitle = (title)->
if /\w+\s+\w+/g.test(title)
"\"#{sanitize(title)}\""
else
sanitize(title)
$.map merges, (m) ->
title: sanitizeLabelTitle(m.title)
color: m.color
search: "#{m.title}"
destroyAtWho: -> destroyAtWho: ->
@input.atwho('destroy') @input.atwho('destroy')
@ -195,6 +217,8 @@ GitLab.GfmAutoComplete =
@input.atwho 'load', 'mergerequests', data.mergerequests @input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis # load emojis
@input.atwho 'load', ':', data.emojis @input.atwho 'load', ':', data.emojis
# load labels
@input.atwho 'load', '~', data.labels
# This trigger at.js again # This trigger at.js again
# otherwise we would be stuck with loading until the user types # otherwise we would be stuck with loading until the user types

View file

@ -58,7 +58,7 @@ class GitLabDropdownFilter
filter: (search_text) -> filter: (search_text) ->
data = @options.data() data = @options.data()
if data? if data? and not @options.filterByText
results = data results = data
if search_text isnt '' if search_text isnt ''
@ -102,6 +102,7 @@ class GitLabDropdownFilter
$el = $(@) $el = $(@)
matches = fuzzaldrinPlus.match($el.text().trim(), search_text) matches = fuzzaldrinPlus.match($el.text().trim(), search_text)
unless $el.is('.dropdown-header')
if matches.length if matches.length
$el.show() $el.show()
else else
@ -191,6 +192,7 @@ class GitLabDropdown
if @options.filterable if @options.filterable
@filter = new GitLabDropdownFilter @filterInput, @filter = new GitLabDropdownFilter @filterInput,
filterInputBlur: @filterInputBlur filterInputBlur: @filterInputBlur
filterByText: @options.filterByText
remote: @options.filterRemote remote: @options.filterRemote
query: @options.data query: @options.data
keys: searchFields keys: searchFields
@ -302,6 +304,9 @@ class GitLabDropdown
if @options.setIndeterminateIds if @options.setIndeterminateIds
@options.setIndeterminateIds.call(@) @options.setIndeterminateIds.call(@)
if @options.setActiveIds
@options.setActiveIds.call(@)
# Makes indeterminate items effective # Makes indeterminate items effective
if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@parseData @fullData @parseData @fullData

View file

@ -39,7 +39,7 @@ class @LabelsSelect
</a> </a>
<% }); %>' <% }); %>'
) )
labelNoneHTMLTemplate = _.template('<div class="light">None</div>') labelNoneHTMLTemplate = '<span class="no-value">None</span>'
if newLabelField.length if newLabelField.length
@ -145,7 +145,7 @@ class @LabelsSelect
template = labelHTMLTemplate(data) template = labelHTMLTemplate(data)
labelCount = data.labels.length labelCount = data.labels.length
else else
template = labelNoneHTMLTemplate() template = labelNoneHTMLTemplate
$value $value
.removeAttr('style') .removeAttr('style')
.html(template) .html(template)
@ -210,9 +210,21 @@ class @LabelsSelect
if $dropdown.hasClass('js-filter-bulk-update') if $dropdown.hasClass('js-filter-bulk-update')
indeterminate = instance.indeterminateIds indeterminate = instance.indeterminateIds
active = instance.activeIds
if indeterminate.indexOf(label.id) isnt -1 if indeterminate.indexOf(label.id) isnt -1
selectedClass.push 'is-indeterminate' selectedClass.push 'is-indeterminate'
if active.indexOf(label.id) isnt -1
# Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf 'is-indeterminate'
selectedClass.splice i, 1 unless i is -1
selectedClass.push 'is-active'
# Add input manually
instance.addInput @fieldName, label.id
if $form.find("input[type='hidden']\ if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\ [name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length [value='#{this.id(label)}']").length
@ -328,6 +340,10 @@ class @LabelsSelect
setIndeterminateIds: -> setIndeterminateIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@indeterminateIds = _this.getIndeterminateIds() @indeterminateIds = _this.getIndeterminateIds()
setActiveIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@activeIds = _this.getActiveIds()
) )
@bindEvents() @bindEvents()
@ -352,3 +368,12 @@ class @LabelsSelect
label_ids.push $("#issue_#{issue_id}").data('labels') label_ids.push $("#issue_#{issue_id}").data('labels')
_.flatten(label_ids) _.flatten(label_ids)
getActiveIds: ->
label_ids = []
$('.selected_issue:checked').each (i, el) ->
issue_id = $(el).data('id')
label_ids.push $("#issue_#{issue_id}").data('labels')
_.intersection.apply _, label_ids

View file

@ -1,7 +1,45 @@
((w) -> ((w) ->
window.gl or= {} w.gl or= {}
window.gl.utils 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) -> jQuery.timefor = (time, suffix, expiredLabel) ->
@ -24,20 +62,4 @@
return timefor return timefor
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
) window ) window

View file

@ -88,7 +88,7 @@ class @MergeRequestTabs
scrollToElement: (container) -> scrollToElement: (container) ->
if window.location.hash if window.location.hash
navBarHeight = $('.navbar-gitlab').outerHeight() navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
$el = $("#{container} #{window.location.hash}:not(.match)") $el = $("#{container} #{window.location.hash}:not(.match)")
$.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length

View file

@ -24,14 +24,10 @@ class @MilestoneSelect
if issueUpdateURL if issueUpdateURL
milestoneLinkTemplate = _.template( milestoneLinkTemplate = _.template(
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"> '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>" class="bold has-tooltip" data-container="body" title="<%= remaining %>"><%= _.escape(title) %></a>'
<span class="has-tooltip" data-container="body" title="<%= remaining %>">
<%= _.escape(title) %>
</span>
</a>'
) )
milestoneLinkNoneTemplate = '<div class="light">None</div>' milestoneLinkNoneTemplate = '<span class="no-value">None</span>'
collapsedSidebarLabelTemplate = _.template( collapsedSidebarLabelTemplate = _.template(
'<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left"> '<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left">

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

View file

@ -102,12 +102,15 @@ class @Notes
keydownNoteText: (e) -> keydownNoteText: (e) ->
$this = $(this) $this = $(this)
if $this.val() is '' and e.which is 38 #aka the up key if $this.val() is '' and e.which is 38 and not isMetaKey e
myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last") myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last")
if myLastNote.length if myLastNote.length
myLastNoteEditBtn = myLastNote.find('.js-note-edit') myLastNoteEditBtn = myLastNote.find('.js-note-edit')
myLastNoteEditBtn.trigger('click', [true, myLastNote]) myLastNoteEditBtn.trigger('click', [true, myLastNote])
isMetaKey = (e) ->
(e.metaKey or e.ctrlKey or e.altKey or e.shiftKey)
initRefresh: -> initRefresh: ->
clearInterval(Notes.interval) clearInterval(Notes.interval)
Notes.interval = setInterval => Notes.interval = setInterval =>

View file

@ -0,0 +1,25 @@
class @NotificationsDropdown
constructor: ->
$(document)
.off 'click', '.update-notification'
.on 'click', '.update-notification', (e) ->
e.preventDefault()
return if $(this).is('.is-active') and $(this).data('notification-level') is 'custom'
notificationLevel = $(@).data 'notification-level'
label = $(@).data 'notification-title'
form = $(this).parents('.notification-form:first')
form.find('.js-notification-loading').toggleClass 'fa-bell fa-spin fa-spinner'
form.find('#notification_setting_level').val(notificationLevel)
form.submit()
$(document)
.off 'ajax:success', '.notification-form'
.on 'ajax:success', '.notification-form', (e, data) ->
if data.saved
$(e.currentTarget)
.closest('.notification-dropdown')
.replaceWith(data.html)
else
new Flash('Failed to save new settings', 'alert')

View file

@ -0,0 +1,49 @@
class @NotificationsForm
constructor: ->
@removeEventListeners()
@initEventListeners()
removeEventListeners: ->
$(document).off 'change', '.js-custom-notification-event'
initEventListeners: ->
$(document).on 'change', '.js-custom-notification-event', @toggleCheckbox
toggleCheckbox: (e) =>
$checkbox = $(e.currentTarget)
$parent = $checkbox.closest('.checkbox')
@saveEvent($checkbox, $parent)
showCheckboxLoadingSpinner: ($parent) ->
$parent
.addClass 'is-loading'
.find '.custom-notification-event-loading'
.removeClass 'fa-check'
.addClass 'fa-spin fa-spinner'
.removeClass 'is-done'
saveEvent: ($checkbox, $parent) ->
form = $parent.parents('form:first')
$.ajax(
url: form.attr('action')
method: form.attr('method')
dataType: 'json'
data: form.serialize()
beforeSend: =>
@showCheckboxLoadingSpinner($parent)
).done (data) ->
$checkbox.enable()
if data.saved
$parent
.find '.custom-notification-event-loading'
.toggleClass 'fa-spin fa-spinner fa-check is-done'
setTimeout(->
$parent
.removeClass 'is-loading'
.find '.custom-notification-event-loading'
.toggleClass 'fa-spin fa-spinner fa-check is-done'
, 2000)

View file

@ -8,6 +8,10 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> $('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit() $(this).parents('form').submit()
# Automatically submit email form when it changes
$('#user_notification_email').on 'change', ->
$(this).parents('form').submit()
$('.update-username').on 'ajax:before', -> $('.update-username').on 'ajax:before', ->
$('.loading-username').show() $('.loading-username').show()
$(this).find('.update-success').hide() $(this).find('.update-success').hide()

View file

@ -19,6 +19,7 @@ class @Project
$('.clone').text(url) $('.clone').text(url)
# Ref switcher # Ref switcher
@initRefSwitcher()
$('.project-refs-select').on 'change', -> $('.project-refs-select').on 'change', ->
$(@).parents('form').submit() $(@).parents('form').submit()
@ -34,23 +35,6 @@ class @Project
$(@).parents('.no-password-message').remove() $(@).parents('.no-password-message').remove()
e.preventDefault() e.preventDefault()
$('.update-notification').on 'click', (e) ->
e.preventDefault()
notification_level = $(@).data 'notification-level'
label = $(@).data 'notification-title'
$('#notification_setting_level').val(notification_level)
$('#notification-form').submit()
$('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
$(@).parents('ul').find('li.active').removeClass 'active'
$(@).parent().addClass 'active'
$('#notification-form').on 'ajax:success', (e, data) ->
if data.saved
new Flash("Notification settings saved", "notice")
else
new Flash("Failed to save new settings", "alert")
@projectSelectDropdown() @projectSelectDropdown()
projectSelectDropdown: -> projectSelectDropdown: ->
@ -66,3 +50,39 @@ class @Project
changeProject: (url) -> changeProject: (url) ->
window.location = url window.location = url
initRefSwitcher: ->
$('.js-project-refs-dropdown').each ->
$dropdown = $(@)
selected = $dropdown.data('selected')
$dropdown.glDropdown(
data: (term, callback) ->
$.ajax(
url: $dropdown.data('refs-url')
data:
ref: $dropdown.data('ref')
).done (refs) ->
callback(refs)
selectable: true
filterable: true
filterByText: true
fieldName: 'ref'
renderRow: (ref) ->
if ref.header?
"<li class='dropdown-header'>#{ref.header}</li>"
else
isActiveClass = if ref is selected then 'is-active' else ''
"<li>
<a href='#' data-ref='#{escape(ref)}' class='#{isActiveClass}'>
#{ref}
</a>
</li>"
id: (obj, $el) ->
$el.data('ref')
toggleLabel: (obj, $el) ->
$el.text().trim()
clicked: (e) ->
$dropdown.closest('form').submit()
)

View file

@ -51,15 +51,19 @@ class @Sidebar
$this = $(e.currentTarget) $this = $(e.currentTarget)
$todoLoading = $('.js-issuable-todo-loading') $todoLoading = $('.js-issuable-todo-loading')
$btnText = $('.js-issuable-todo-text', $this) $btnText = $('.js-issuable-todo-text', $this)
ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST' ajaxType = if $this.attr('data-delete-path') then 'DELETE' else 'POST'
ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else ''
if $this.attr('data-delete-path')
url = "#{$this.attr('data-delete-path')}"
else
url = "#{$this.data('url')}"
$.ajax( $.ajax(
url: "#{$this.data('url')}#{ajaxUrlExtra}" url: url
type: ajaxType type: ajaxType
dataType: 'json' dataType: 'json'
data: data:
issuable_id: $this.data('issuable') issuable_id: $this.data('issuable-id')
issuable_type: $this.data('issuable-type') issuable_type: $this.data('issuable-type')
beforeSend: => beforeSend: =>
@beforeTodoSend($this, $todoLoading) @beforeTodoSend($this, $todoLoading)
@ -82,15 +86,15 @@ class @Sidebar
else else
$todoPendingCount.removeClass 'hidden' $todoPendingCount.removeClass 'hidden'
if data.todo? if data.delete_path?
$btn $btn
.attr 'aria-label', $btn.data('mark-text') .attr 'aria-label', $btn.data('mark-text')
.attr 'data-id', data.todo.id .attr 'data-delete-path', data.delete_path
$btnText.text $btn.data('mark-text') $btnText.text $btn.data('mark-text')
else else
$btn $btn
.attr 'aria-label', $btn.data('todo-text') .attr 'aria-label', $btn.data('todo-text')
.removeAttr 'data-id' .removeAttr 'data-delete-path'
$btnText.text $btn.data('todo-text') $btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) -> sidebarDropdownLoading: (e) ->

View file

@ -67,8 +67,12 @@ class @SearchAutocomplete
getData: (term, callback) -> getData: (term, callback) ->
_this = @ _this = @
# Do not trigger request if input is empty unless term
return if @searchInput.val() is '' if contents = @getCategoryContents()
@searchInput.data('glDropdown').filter.options.callback contents
@enableAutocomplete()
return
# Prevent multiple ajax calls # Prevent multiple ajax calls
return if @loadingSuggestions return if @loadingSuggestions
@ -122,6 +126,37 @@ class @SearchAutocomplete
).always -> ).always ->
_this.loadingSuggestions = false _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: -> serializeState: ->
{ {
# Search Criteria # Search Criteria
@ -209,6 +244,12 @@ class @SearchAutocomplete
@isFocused = true @isFocused = true
@wrap.addClass('search-active') @wrap.addClass('search-active')
@getData() if @getValue() is ''
getValue: -> return @searchInput.val()
onClearInputClick: (e) => onClearInputClick: (e) =>
e.preventDefault() e.preventDefault()
@searchInput.val('').focus() @searchInput.val('').focus()
@ -229,6 +270,10 @@ class @SearchAutocomplete
@locationBadgeEl.text(badgeText).show() @locationBadgeEl.text(badgeText).show()
@wrap.addClass('has-location-badge') @wrap.addClass('has-location-badge')
hasLocationBadge: -> return @wrap.is '.has-location-badge'
restoreOriginalState: -> restoreOriginalState: ->
inputs = Object.keys @originalState inputs = Object.keys @originalState
@ -257,13 +302,14 @@ class @SearchAutocomplete
@getElement("##{input}").val('') @getElement("##{input}").val('')
removeLocationBadge: -> removeLocationBadge: ->
@locationBadgeEl.hide() @locationBadgeEl.hide()
# Reset state
@resetSearchState() @resetSearchState()
@wrap.removeClass('has-location-badge') @wrap.removeClass('has-location-badge')
@disableAutocomplete()
disableAutocomplete: -> disableAutocomplete: ->
@searchInput.addClass('disabled') @searchInput.addClass('disabled')

View file

@ -1,7 +1,7 @@
class @Shortcuts class @Shortcuts
constructor: -> constructor: (skipResetBindings) ->
@enabledHelp = [] @enabledHelp = []
Mousetrap.reset() Mousetrap.reset() if not skipResetBindings
Mousetrap.bind('?', @onToggleHelp) Mousetrap.bind('?', @onToggleHelp)
Mousetrap.bind('s', Shortcuts.focusSearch) Mousetrap.bind('s', Shortcuts.focusSearch)
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview) Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)

View 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

View file

@ -3,13 +3,33 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = -> toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('.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 ( -> setTimeout ( ->
niceScrollBars = $('.nicescroll').niceScroll(); niceScrollBars = $('.nav-sidebar').niceScroll();
niceScrollBars.updateScrollBar(); niceScrollBars.updateScrollBar();
), 300 ), 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) -> $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) ->
e.preventDefault() e.preventDefault()

View file

@ -6,12 +6,6 @@ class @Calendar
@daySizeWithSpace = @daySize + (@daySpace * 2) @daySizeWithSpace = @daySize + (@daySpace * 2)
@monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] @monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
@months = [] @months = []
@highestValue = 0
# Get the highest value from the timestampes
_.each timestamps, (count) =>
if count > @highestValue
@highestValue = count
# Loop through the timestamps to create a group of objects # Loop through the timestamps to create a group of objects
# The group of objects will be grouped based on the day of the week they are # The group of objects will be grouped based on the day of the week they are
@ -39,8 +33,8 @@ class @Calendar
i++ i++
# Init color functions # Init color functions
@color = @initColor()
@colorKey = @initColorKey() @colorKey = @initColorKey()
@color = @initColor()
# Init the svg element # Init the svg element
@renderSvg(group) @renderSvg(group)
@ -104,7 +98,7 @@ class @Calendar
.attr 'class', 'user-contrib-cell js-tooltip' .attr 'class', 'user-contrib-cell js-tooltip'
.attr 'fill', (stamp) => .attr 'fill', (stamp) =>
if stamp.count isnt 0 if stamp.count isnt 0
@color(stamp.count) @color(Math.min(stamp.count, 40))
else else
'#ededed' '#ededed'
.attr 'data-container', 'body' .attr 'data-container', 'body'
@ -164,10 +158,11 @@ class @Calendar
color color
initColor: -> initColor: ->
colorRange = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)]
d3.scale d3.scale
.linear() .threshold()
.range(['#acd5f2', '#254e77']) .domain([0, 10, 20, 30])
.domain([0, @highestValue]) .range(colorRange)
initColorKey: -> initColorKey: ->
d3.scale d3.scale

View file

@ -72,7 +72,7 @@ class @UsersSelect
assigneeTemplate = _.template( assigneeTemplate = _.template(
'<% if (username) { %> '<% if (username) { %>
<a class="author_link " href="/u/<%= username %>"> <a class="author_link bold" href="/u/<%= username %>">
<% if( avatar ) { %> <% if( avatar ) { %>
<img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>"> <img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
<% } %> <% } %>
@ -82,7 +82,7 @@ class @UsersSelect
</span> </span>
</a> </a>
<% } else { %> <% } else { %>
<span class="assign-yourself"> <span class="no-value assign-yourself">
No assignee - No assignee -
<a href="#" class="js-assign-yourself"> <a href="#" class="js-assign-yourself">
assign yourself assign yourself

View file

@ -37,3 +37,4 @@
@import "framework/timeline.scss"; @import "framework/timeline.scss";
@import "framework/typography.scss"; @import "framework/typography.scss";
@import "framework/zen.scss"; @import "framework/zen.scss";
@import "framework/blank";

View file

@ -0,0 +1,23 @@
.blank-state {
padding-top: 20px;
padding-bottom: 20px;
text-align: center;
}
.blank-state-no-icon {
padding-top: 40px;
padding-bottom: 40px;
}
.blank-state-title {
margin-top: 0;
margin-bottom: 5px;
font-size: 19px;
font-weight: normal;
}
.blank-state-text {
margin-top: 0;
margin-bottom: $gl-padding;
font-size: 15px;
}

View file

@ -91,6 +91,26 @@
background-color: $white-light; background-color: $white-light;
border-top: none; border-top: none;
} }
&.top-block .container-fluid {
background-color: inherit;
}
}
.sub-header-block {
background-color: $white-light;
border-bottom: 1px solid $white-dark;
padding: 11px 0;
margin-bottom: 11px;
.oneline {
line-height: 35px;
}
&.no-bottom-space {
border-bottom: 0;
margin-bottom: 0;
}
} }
.cover-block { .cover-block {

View file

@ -461,10 +461,12 @@
} }
} }
.ui-state-active, .ui-datepicker-calendar {
.ui-state-hover { .ui-state-hover,
color: $md-link-color; .ui-state-active {
background-color: $calendar-hover-bg; color: #fff;
border: 0;
}
} }
.ui-datepicker-prev, .ui-datepicker-prev,

View file

@ -8,8 +8,8 @@
*/ */
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar { .page-with-sidebar {
.toggle-nav-collapse,
.collapse-nav a { .pin-nav-btn {
color: $color-light; color: $color-light;
background: $color; background: $color;

View file

@ -2,8 +2,19 @@
* Application Header * Application Header
* *
*/ */
@mixin tanuki-logo-colors($path-color) {
fill: $path-color;
transition: all 0.8s;
&:hover,
&.highlight {
fill: lighten($path-color, 25%);
transition: all 0.1s;
}
}
header { header {
transition-duration: .3s; transition: padding $sidebar-transition-duration;
&.navbar-empty { &.navbar-empty {
height: $header-height; height: $header-height;
@ -79,14 +90,9 @@ header {
&.header-collapsed { &.header-collapsed {
padding: 0 16px; padding: 0 16px;
.side-nav-toggle {
display: block;
}
} }
.side-nav-toggle { .side-nav-toggle {
display: none;
position: absolute; position: absolute;
left: -10px; left: -10px;
margin: 6px 0; margin: 6px 0;
@ -108,9 +114,7 @@ header {
.header-content { .header-content {
position: relative; position: relative;
height: $header-height; height: $header-height;
padding-right: 40px;
padding-left: 30px; padding-left: 30px;
transition-duration: .3s;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-right: 0; padding-right: 0;
@ -198,25 +202,24 @@ header {
} }
} }
.header-collapsed { #tanuki-logo {
margin-left: 0;
.header-content { #tanuki-left-ear,
#tanuki-right-ear,
@media (min-width: $screen-sm-max) { #tanuki-nose {
padding-left: 30px; @include tanuki-logo-colors($tanuki-red);
transition-duration: .3s;
} }
}
}
.tanuki-shape { #tanuki-left-eye,
transition: all 0.8s; #tanuki-right-eye {
@include tanuki-logo-colors($tanuki-orange);
&:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
} }
#tanuki-left-cheek,
#tanuki-right-cheek {
@include tanuki-logo-colors($tanuki-yellow);
}
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {

View file

@ -159,7 +159,7 @@ ul.content-list {
background-color: $gray-light; background-color: $gray-light;
border: dotted 1px $gray-dark; border: dotted 1px $gray-dark;
margin: 1px 0; margin: 1px 0;
min-height: 30px; min-height: 52px;
} }
} }
} }

View file

@ -97,5 +97,7 @@
white-space: pre-wrap; white-space: pre-wrap;
word-break: keep-all; word-break: keep-all;
} }
@include bulleted-list;
} }
} }

View file

@ -110,3 +110,17 @@
font-size: 16px; font-size: 16px;
line-height: 24px; line-height: 24px;
} }
@mixin bulleted-list {
> ul {
list-style-type: disc;
ul {
list-style-type: circle;
ul {
list-style-type: square;
}
}
}
}

View file

@ -52,6 +52,19 @@
.git-clone-holder { .git-clone-holder {
display: none; display: none;
} }
// Display Star and Fork buttons without counters on mobile.
.project-action-buttons {
display: block;
.count-buttons .btn {
margin: 0 10px;
}
.count-buttons .count-with-arrow {
display: none;
}
}
} }
.project-stats { .project-stats {

View file

@ -18,6 +18,13 @@
opacity: 0; opacity: 0;
transition-duration: .3s; transition-duration: .3s;
} }
.fa {
position: relative;
top: 3px;
font-size: 13px;
color: $btn-placeholder-gray;
}
} }
@mixin scrolling-links() { @mixin scrolling-links() {
@ -104,10 +111,6 @@
width: 50%; width: 50%;
line-height: 28px; line-height: 28px;
&.wiki-page {
padding: 16px 10px 11px;
}
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-min) {
width: 100%; width: 100%;
@ -136,7 +139,7 @@
} }
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-max) { @media (max-width: $screen-xs-max) {
width: 100%; width: 100%;
} }
} }
@ -220,6 +223,7 @@
form { form {
display: block; display: block;
height: auto; height: auto;
margin-bottom: 14px;
input { input {
width: 100%; width: 100%;
@ -242,6 +246,12 @@
} }
} }
} }
&.adjust {
.nav-text, .nav-controls {
width: auto;
}
}
} }
.layout-nav { .layout-nav {
@ -251,7 +261,7 @@
z-index: 11; z-index: 11;
background: $background-color; background: $background-color;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
transition-duration: .3s; transition: padding $sidebar-transition-duration;
text-align: center; text-align: center;
.container-fluid { .container-fluid {
@ -313,11 +323,19 @@
.fade-right { .fade-right {
@include fade(left, rgba(250, 250, 250, 0.4), $background-color); @include fade(left, rgba(250, 250, 250, 0.4), $background-color);
right: 0; right: 0;
.fa {
right: -7px;
}
} }
.fade-left { .fade-left {
@include fade(right, rgba(250, 250, 250, 0.4), $background-color); @include fade(right, rgba(250, 250, 250, 0.4), $background-color);
left: 0; left: 0;
.fa {
left: -7px;
}
} }
li { li {
@ -347,6 +365,12 @@
.badge { .badge {
color: $gl-icon-color; color: $gl-icon-color;
} }
&:hover {
a, i {
color: $black;
}
}
} }
} }

View file

@ -9,6 +9,10 @@
margin-top: -2px; margin-top: -2px;
float: right; float: right;
} }
.dropdown-menu-toggle {
line-height: 20px;
}
} }
.panel-body { .panel-body {

View file

@ -165,11 +165,6 @@
background-size: 16px 16px !important; background-size: 16px 16px !important;
} }
/** Branch/tag selector **/
.project-refs-form .select2-container {
width: 160px !important;
}
.select2-results .select2-no-results, .select2-results .select2-no-results,
.select2-results .select2-searching, .select2-results .select2-searching,
.select2-results .select2-ajax-error, .select2-results .select2-ajax-error,

View file

@ -1,26 +1,31 @@
.page-with-sidebar { .page-with-sidebar {
padding-top: $header-height; padding-top: $header-height;
transition-duration: .3s; transition: padding $sidebar-transition-duration;
.sidebar-wrapper { .sidebar-wrapper {
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
overflow-y: auto;
overflow-x: hidden;
left: 0; left: 0;
height: 100%; height: 100%;
transition-duration: .3s; overflow: hidden;
transition: width $sidebar-transition-duration;
} }
} }
.sidebar-wrapper { .sidebar-wrapper {
z-index: 1000; z-index: 1000;
background: $background-color; background: $background-color;
.nicescroll-rails-hr {
// TODO: Figure out why nicescroll doesn't hide horizontal bar
display: none!important;
}
} }
.content-wrapper { .content-wrapper {
width: 100%; width: 100%;
transition: padding $sidebar-transition-duration;
.container-fluid { .container-fluid {
background: #fff; background: #fff;
@ -34,50 +39,39 @@
} }
} }
.sidebar-wrapper { .sidebar-user {
padding: 15px;
.sidebar-user { position: absolute;
padding: 15px 22px; left: 0;
position: fixed;
bottom: 0; bottom: 0;
width: $sidebar_width; width: $sidebar_width;
overflow: hidden; overflow: hidden;
transition-duration: .3s;
.username {
margin-left: 10px;
width: $sidebar_width - 2 * 10px;
font-size: 16px; font-size: 16px;
line-height: 34px; line-height: 36px;
} transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
@media (min-width: $sidebar-breakpoint) {
bottom: 50px;
} }
} }
.tanuki-shape {
transition: all 0.8s;
&:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
}
}
.nav-sidebar { .nav-sidebar {
margin-top: 22 + $header-height; position: absolute;
margin-bottom: 116px; top: 50px;
transition-duration: .3s; bottom: 65px;
list-style: none; width: $sidebar_width;
overflow: hidden; overflow-y: auto;
overflow-x: hidden;
@media (min-width: $sidebar-breakpoint) {
bottom: 115px;
}
&.navbar-collapse { &.navbar-collapse {
padding: 0 !important; padding: 0 !important;
} }
li { li {
width: $sidebar_width;
&.separate-item { &.separate-item {
padding-top: 10px; padding-top: 10px;
margin-top: 10px; margin-top: 10px;
@ -90,8 +84,7 @@
} }
a { a {
width: $sidebar_width; padding: 7px 15px 7px 12px;
padding: 7px 15px 7px 23px;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: 24px; line-height: 24px;
display: block; display: block;
@ -99,11 +92,9 @@
font-weight: normal; font-weight: normal;
outline: none; outline: none;
&:hover { &:hover,
text-decoration: none; &:active,
} &:focus {
&:active, &:focus {
text-decoration: none; text-decoration: none;
} }
@ -115,10 +106,6 @@
svg { svg {
margin-right: 13px; margin-right: 13px;
} }
&.back-link i {
transition-duration: .3s;
}
} }
} }
@ -129,37 +116,50 @@
} }
} }
.sidebar-subnav { .toggle-nav-collapse {
margin-left: 0;
padding-left: 0;
li {
list-style: none;
}
}
.collapse-nav a {
width: $sidebar_width; width: $sidebar_width;
position: fixed; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
min-height: 50px;
padding: 5px 0; padding: 5px 0;
font-size: 18px; font-size: 18px;
background: transparent; line-height: 30px;
height: 50px; }
text-align: center;
line-height: 40px;
transition-duration: .3s;
outline: none;
&:hover { .nav-header-btn {
padding: 10px 5px;
color: inherit;
transition-duration: .3s;
&:hover,
&:focus {
color: $white-light;
text-decoration: none; text-decoration: none;
} }
} }
.sidebar-wrapper { .pin-nav-btn {
&.hidden-nav { display: none;
width: 0; 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);
}
} }
} }
@ -168,62 +168,34 @@
.sidebar-wrapper { .sidebar-wrapper {
width: 0; 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 { .page-sidebar-expanded {
@media (max-width: $screen-sm-max) {
padding-left: 0;
}
.sidebar-wrapper { .sidebar-wrapper {
width: $sidebar_width; width: $sidebar_width;
.nav-sidebar {
width: $sidebar_width;
} }
}
.nav-sidebar li a { .page-sidebar-pinned {
width: $sidebar_width; .content-wrapper,
.layout-nav {
&.back-link { @media (min-width: $sidebar-breakpoint) {
i { padding-left: $sidebar_width;
opacity: 0;
} }
} }
}
header.header-pinned-nav {
@media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
.side-nav-toggle {
display: none;
}
.header-content {
padding-left: 0;
} }
} }
} }

View file

@ -6,6 +6,8 @@ $sidebar_width: 220px;
$gutter_collapsed_width: 62px; $gutter_collapsed_width: 62px;
$gutter_width: 290px; $gutter_width: 290px;
$gutter_inner_width: 258px; $gutter_inner_width: 258px;
$sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1440px;
/* /*
* UI elements * UI elements
@ -154,6 +156,11 @@ $warning-message-border: #f0e2bb;
/* header */ /* header */
$light-grey-header: #faf9f9; $light-grey-header: #faf9f9;
/* tanuki logo colors */
$tanuki-red: #e24329;
$tanuki-orange: #fc6d26;
$tanuki-yellow: #fca326;
/* /*
* State colors: * State colors:
*/ */
@ -261,5 +268,10 @@ $calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1); $calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9; $calendar-unselectable-bg: #faf9f9;
/*
* Personal Access Tokens
*/
$personal-access-tokens-disabled-label-color: #bbb;
$ci-output-bg: #1d1f21; $ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6; $ci-text-color: #c5c8c6;

View file

@ -38,6 +38,10 @@ table {
margin: 0 auto; margin: 0 auto;
text-align: left; text-align: left;
width: 600px; width: 600px;
& > td {
text-align: center;
}
} }
&#body { &#body {

View file

@ -7,84 +7,119 @@
margin-right: 9px; margin-right: 9px;
} }
.lists-separator { .commit-header {
margin: 10px 0; padding: 5px 10px;
border-color: #ddd; background-color: $background-color;
} border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
font-size: 14px;
.commits-row { &:first-child {
ul { border-top-width: 0;
margin: 0;
li.commit {
padding: 8px 0;
}
}
.commits-row-date {
font-size: 15px;
line-height: 20px;
margin-bottom: 5px;
} }
} }
li.commit { .commit-row-title {
list-style: none; line-height: 1;
margin-bottom: 7px;
.commit-row-title {
font-size: $list-font-size;
line-height: 20px;
margin-bottom: 2px;
.btn-clipboard {
margin-top: -1px;
}
.notes_count { .notes_count {
float: right; float: right;
margin-right: 10px; margin-right: 10px;
} }
.commit_short_id {
min-width: 65px;
color: $gl-dark-link-color;
font-family: $monospace_font;
}
.str-truncated { .str-truncated {
max-width: 70%; max-width: 70%;
} }
.commit-row-message { .commit-row-message {
color: $gl-dark-link-color; color: $gl-dark-link-color;
&:hover {
text-decoration: underline;
}
} }
.text-expander { .text-expander {
background: #eee; display: inline-block;
color: #555; background: $gray-light;
color: $gl-placeholder-color;
padding: 0 5px; padding: 0 5px;
cursor: pointer; cursor: pointer;
margin-left: 4px; border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
&:hover { &:hover {
background-color: #ddd; background-color: darken($gray-light, 10%);
text-decoration: none;
} }
} }
}
.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 {
font-family: $monospace_font;
font-weight: 600;
}
.commit {
padding: 10px 0;
position: relative;
@media (min-width: $screen-sm-min) {
padding-left: 20px;
.commit-info-block {
padding-left: 44px;
}
}
&:not(:last-child) {
border-bottom: 1px solid #eee;
}
a,
button {
color: $gl-dark-link-color;
vertical-align: baseline;
}
.avatar {
position: absolute;
top: 10px;
left: 16px;
} }
.item-title { .item-title {
display: inline-block; display: inline-block;
@media (min-width: $screen-sm-min) {
max-width: 70%; max-width: 70%;
} }
}
.commit-row-description { .commit-row-description {
font-size: 14px; font-size: 14px;
border-left: 1px solid #eee; border-left: 1px solid #eee;
padding: 10px 15px; padding: 10px 15px;
margin: 5px 0 10px 5px; margin: 10px 0;
background: #f9f9f9; background: #f9f9f9;
display: none; display: none;
@ -93,6 +128,7 @@ li.commit {
background: inherit; background: inherit;
padding: 0; padding: 0;
margin: 0; margin: 0;
white-space: pre-wrap;
} }
a { a {
@ -102,7 +138,7 @@ li.commit {
.commit-row-info { .commit-row-info {
color: $gl-gray; color: $gl-gray;
line-height: 24px; line-height: 1;
a { a {
color: $gl-gray; color: $gl-gray;
@ -111,10 +147,6 @@ li.commit {
.avatar { .avatar {
margin-right: 8px; margin-right: 8px;
} }
.committed_ago {
display: inline-block;
}
} }
&.inline-commit { &.inline-commit {

View file

@ -4,6 +4,11 @@
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
border-radius: 3px; border-radius: 3px;
.commit-short-id {
font-family: $regular_font;
font-weight: 400;
}
.diff-header { .diff-header {
position: relative; position: relative;
background: $background-color; background: $background-color;

View file

@ -60,14 +60,14 @@
.encoding-selector, .encoding-selector,
.license-selector, .license-selector,
.gitignore-selector { .gitignore-selector,
.gitlab-ci-yml-selector {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
font-family: $regular_font; font-family: $regular_font;
} }
.gitignore-selector { .gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
.dropdown { .dropdown {
line-height: 21px; line-height: 21px;
} }
@ -77,4 +77,10 @@
width: 220px; width: 220px;
} }
} }
.gitlab-ci-yml-selector {
.dropdown-menu-toggle {
width: 250px;
}
}
} }

View file

@ -136,9 +136,10 @@
.event-last-push { .event-last-push {
overflow: auto; overflow: auto;
width: 100%; width: 100%;
.event-last-push-text { .event-last-push-text {
@include str-truncated(100%); @include str-truncated(100%);
padding: 5px 0; padding: 4px 0;
font-size: 13px; font-size: 13px;
float: left; float: left;
margin-right: -150px; margin-right: -150px;

View file

@ -57,4 +57,11 @@
.documentation { .documentation {
padding: 7px; padding: 7px;
// Border around images in the help pages.
img:not(.emoji) {
border: 1px solid $table-border-gray;
padding: 5px;
margin: 5px;
}
} }

View file

@ -4,6 +4,13 @@
margin-right: 1px; margin-right: 1px;
} }
} }
// Border around images in issue and MR descriptions.
.description img:not(.emoji) {
border: 1px solid $table-border-gray;
padding: 5px;
margin: 5px;
}
} }
.issuable-filter-count { .issuable-filter-count {
@ -145,7 +152,6 @@
.assign-yourself { .assign-yourself {
margin-top: 10px; margin-top: 10px;
font-weight: normal;
display: block; display: block;
} }
} }
@ -158,6 +164,10 @@
font-weight: normal; font-weight: normal;
} }
.no-value {
color: $gl-placeholder-color;
}
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
display: none; display: none;
} }
@ -248,11 +258,16 @@
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 10px; margin-bottom: 10px;
} }
.issuable-header-btn {
display: none;
}
} }
.issuable-header-btn { .issuable-header-btn {
background: $gray-normal; background: $gray-normal;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
&:hover { &:hover {
background: $gray-dark; background: $gray-dark;
border: 1px solid $border-gray-dark; border: 1px solid $border-gray-dark;
@ -322,7 +337,7 @@
margin-left: 5px; margin-left: 5px;
a { a {
color: #8c8c8c; color: $gl-placeholder-color;
} }
} }

View file

@ -50,11 +50,10 @@
.label-row { .label-row {
.label-name { .label-name {
display: block; display: inline-block;
margin-bottom: 10px; margin-bottom: 10px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: inline-block;
width: 200px; width: 200px;
margin-bottom: 0; margin-bottom: 0;
} }
@ -63,6 +62,7 @@
.label-description { .label-description {
display: block; display: block;
margin-bottom: 10px; margin-bottom: 10px;
margin-left: 50px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: inline-block; display: inline-block;
@ -115,6 +115,13 @@
} }
} }
.draggable-handler {
display: inline-block;
opacity: 0;
transition: opacity .3s;
color: $gray-darkest;
}
.prioritized-labels { .prioritized-labels {
margin-bottom: 30px; margin-bottom: 30px;
@ -122,6 +129,13 @@
display: none; display: none;
color: $gray-light; color: $gray-light;
} }
li:hover {
.draggable-handler {
display: inline-block;
opacity: 1;
}
}
} }
.other-labels { .other-labels {

View file

@ -119,7 +119,12 @@
margin-bottom: 0; margin-bottom: 0;
} }
@media (max-width: $screen-sm-max) { .btn-grouped {
margin-left: 0;
margin-right: 7px;
}
@media (max-width: $screen-xs-max) {
h4 { h4 {
font-size: 15px; font-size: 15px;
} }
@ -131,10 +136,14 @@
.btn, .btn,
.btn-group, .btn-group,
.accept-action { .accept-action {
width: 100%;
margin-bottom: 4px; margin-bottom: 4px;
} }
.accept-action {
width: 100%;
text-align: center;
}
.accept-control { .accept-control {
width: 100%; width: 100%;
text-align: center; text-align: center;
@ -244,6 +253,10 @@
.panel-footer { .panel-footer {
padding: 5px 10px; padding: 5px 10px;
.btn {
min-width: auto;
}
} }
.commit { .commit {
@ -252,9 +265,7 @@
} }
.avatar { .avatar {
width: 20px; margin-left: 0;
height: 20px;
margin-right: 5px;
} }
.commit-row-info { .commit-row-info {
@ -282,7 +293,7 @@
margin-bottom: 0; margin-bottom: 0;
} }
@media (min-width: $screen-sm-min) { @media (min-width: $screen-xs-min) {
float: left; float: left;
width: 50%; width: 50%;
margin-bottom: 0; margin-bottom: 0;

View file

@ -219,3 +219,16 @@
float: left; float: left;
} }
} }
.note-form-actions {
@media (max-width: $screen-xs-max) {
.btn {
float: none;
width: 100%;
&:not(:last-child) {
margin-bottom: 10px;
}
}
}
}

View file

@ -84,24 +84,14 @@ ul.notes {
word-wrap: break-word; word-wrap: break-word;
@include md-typography; @include md-typography;
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
// On diffs code should wrap nicely and not overflow // On diffs code should wrap nicely and not overflow
code { code {
white-space: pre-wrap; white-space: pre-wrap;
} }
// Reset ul style types since we're nested inside a ul already
& > ul {
list-style-type: disc;
ul {
list-style-type: circle;
ul {
list-style-type: square;
}
}
}
ul.task-list { ul.task-list {
ul:not(.task-list) { ul:not(.task-list) {
padding-left: 1.3em; padding-left: 1.3em;
@ -117,6 +107,13 @@ ul.notes {
code { code {
word-break: keep-all; word-break: keep-all;
} }
// Border around images in issue and MR comments.
img:not(.emoji) {
border: 1px solid $table-border-gray;
padding: 5px;
margin: 5px 0;
}
} }
} }
@ -139,6 +136,12 @@ ul.notes {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-right: 0; padding-right: 0;
} }
@media (max-width: $screen-xs-min) {
.inline {
display: block;
}
}
} }
.note-emoji-button { .note-emoji-button {
@ -259,6 +262,10 @@ ul.notes {
right: 0; right: 0;
top: 0; top: 0;
.note-action-button {
margin-left: 10px;
}
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
position: relative; position: relative;
} }

View file

@ -192,6 +192,25 @@
} }
} }
.personal-access-tokens-never-expires-label {
color: $personal-access-tokens-disabled-label-color;
}
.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
text-align: center;
}
.created-personal-access-token-container {
#created-personal-access-token {
width: 90%;
display: inline;
}
.btn-clipboard {
margin-left: 5px;
}
}
.user-profile { .user-profile {
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.cover-block { .cover-block {

View file

@ -5,10 +5,12 @@
font-weight: normal; font-weight: normal;
} }
} }
.no-ssh-key-message, .project-limit-message { .no-ssh-key-message, .project-limit-message {
background-color: #f28d35; background-color: #f28d35;
margin-bottom: 0; margin-bottom: 0;
} }
.new_project, .new_project,
.edit-project { .edit-project {
fieldset.features { fieldset.features {
@ -18,13 +20,6 @@
} }
} }
.project-name-holder {
.help-inline {
vertical-align: top;
padding: 7px;
}
}
.project-home-panel { .project-home-panel {
background: $white-light; background: $white-light;
text-align: left; text-align: left;
@ -33,7 +28,7 @@
.container-fluid { .container-fluid {
position: relative; position: relative;
@media (min-width: $screen-md-max) { @media (min-width: $screen-lg-min) {
.row { .row {
display: flex; display: flex;
-ms-flex-align: center; -ms-flex-align: center;
@ -106,7 +101,8 @@
.notifications-btn { .notifications-btn {
.fa-bell { .fa-bell,
.fa-spinner {
margin-right: 6px; margin-right: 6px;
} }
@ -133,11 +129,6 @@
} }
} }
.btn-group:not(:first-child):not(:last-child) > .btn {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
form { form {
margin-left: 10px; margin-left: 10px;
} }
@ -229,7 +220,7 @@
right: 16px; right: 16px;
bottom: 0; bottom: 0;
@media (max-width: $screen-lg-min) { @media (max-width: $screen-md-max) {
top: 0; top: 0;
} }
@ -238,7 +229,7 @@
right: 0; right: 0;
bottom: 61px; bottom: 61px;
@media (max-width: $screen-lg-min) { @media (max-width: $screen-md-max) {
position: relative; position: relative;
bottom: 0; bottom: 0;
margin-right: 10px; margin-right: 10px;
@ -376,13 +367,14 @@ a.deploy-project-label {
.project-import .btn { .project-import .btn {
float: left; float: left;
margin-bottom: 10px;
margin-right: 10px; margin-right: 10px;
} }
.project-stats { .project-stats {
margin-top: $gl-padding; margin-top: $gl-padding;
margin-bottom: 0; margin-bottom: 0;
padding: 16px 0; padding: 0;
background-color: $white-light; background-color: $white-light;
font-size: 0; font-size: 0;
@ -391,13 +383,14 @@ a.deploy-project-label {
} }
.nav li { .nav li {
display: inline; display: inline-block;
margin: 16px 0;
margin-right: 16px;
} }
.nav > li > a { .nav > li > a {
background-color: transparent; background-color: transparent;
margin-right: 12px; padding: 5px 10px;
padding: 0 10px;
font-size: 15px; font-size: 15px;
color: $notes-light-color; color: $notes-light-color;
} }
@ -411,12 +404,17 @@ a.deploy-project-label {
font-size: 17px; font-size: 17px;
} }
li.missing a { li.missing {
color: #5a6069; border: 1px dashed $border-gray-light;
border: 1px dashed #dce0e5; border-radius: $border-radius-default;
a {
color: $notes-light-color;
display: block;
}
&:hover { &:hover {
background-color: #f0f2f5; background-color: $gray-normal;
} }
} }
@ -503,7 +501,8 @@ pre.light-well {
.activity-filter-block { .activity-filter-block {
.controls { .controls {
padding-bottom: 10px; padding-bottom: 7px;
margin-top: 8px;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
} }
@ -607,3 +606,26 @@ pre.light-well {
} }
} }
} }
.custom-notifications-form {
.is-loading {
.custom-notification-event-loading {
display: inline-block;
}
}
}
.custom-notification-event-loading {
display: none;
margin-left: 5px;
&.is-done {
color: $gl-text-green;
}
}
.project-refs-form {
.dropdown-menu {
width: 300px;
}
}

View file

@ -14,18 +14,28 @@
font-size: 10px; font-size: 10px;
} }
#contributors-master {
@include make-md-column(12);
svg {
width: 100%;
}
}
#contributors { #contributors {
.contributors-list { .contributors-list {
margin: 0 0 10px; margin: 0 0 10px;
list-style: none; list-style: none;
padding: 0; padding: 0;
float: left;
svg {
width: 100%;
}
} }
.person { .person {
&:nth-child(even) { @include make-md-column(6);
float: right;
}
float: left;
margin-top: 10px; margin-top: 10px;
} }

View file

@ -101,7 +101,7 @@
margin: 0; margin: 0;
.commit { .commit {
padding: 0; padding: 0 0 0 55px;
.commit-row-title { .commit-row-title {
.commit-row-message { .commit-row-message {
@ -129,4 +129,6 @@
.tree-controls { .tree-controls {
float: right; float: right;
margin-top: 11px; margin-top: 11px;
position: relative;
z-index: 2;
} }

View file

@ -1,15 +1,14 @@
class Admin::RunnerProjectsController < Admin::ApplicationController class Admin::RunnerProjectsController < Admin::ApplicationController
before_action :project, only: [:create] before_action :project, only: [:create]
def index
@runner_projects = project.runner_projects.all
@runner_project = project.runner_projects.new
end
def create def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id]) @runner = Ci::Runner.find(params[:runner_project][:runner_id])
if @runner.assign_to(@project, current_user) return head(403) if @runner.is_shared? || @runner.locked?
runner_project = @runner.assign_to(@project, current_user)
if runner_project.persisted?
redirect_to admin_runner_path(@runner) redirect_to admin_runner_path(@runner)
else else
redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project' redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project'

View file

@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
include PageLayoutHelper include PageLayoutHelper
include WorkhorseHelper include WorkhorseHelper
before_action :authenticate_user_from_token! before_action :authenticate_user_from_private_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :reject_blocked! before_action :reject_blocked!
@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings helper_method :abilities, :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled? helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception| rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception) log_exception(exception)
@ -64,17 +64,10 @@ class ApplicationController < ActionController::Base
end end
end end
# From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example # This filter handles both private tokens and personal access tokens
# https://gist.github.com/josevalim/fb706b1e933ef01e4fb6 def authenticate_user_from_private_token!
def authenticate_user_from_token! token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
user_token = if params[:authenticity_token].presence user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
params[:authenticity_token].presence
elsif params[:private_token].presence
params[:private_token].presence
elsif request.headers['PRIVATE-TOKEN'].present?
request.headers['PRIVATE-TOKEN']
end
user = user_token && User.find_by_authentication_token(user_token.to_s)
if user if user
# Notice we are passing store false, so the user is not # Notice we are passing store false, so the user is not
@ -326,6 +319,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git') current_application_settings.import_sources.include?('git')
end end
def gitlab_project_import_enabled?
current_application_settings.import_sources.include?('gitlab_project')
end
def two_factor_authentication_required? def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication current_application_settings.require_two_factor_authentication
end end

View file

@ -1,44 +1,39 @@
class Dashboard::TodosController < Dashboard::ApplicationController class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy, :destroy_all] include TodosHelper
before_action :find_todos, only: [:index, :destroy_all]
def index def index
@todos = @todos.page(params[:page]) @todos = @todos.page(params[:page])
end end
def destroy def destroy
todo.done TodoService.new.mark_todos_as_done([todo], current_user)
todo_notice = 'Todo was successfully marked as done.'
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: todo_notice } format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
format.js { head :ok } format.js { head :ok }
format.json do format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
render json: { count: @todos.size, done_count: current_user.todos.done.count }
end
end end
end end
def destroy_all def destroy_all
@todos.each(&:done) TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok } format.js { head :ok }
format.json do format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
find_todos
render json: { count: @todos.size, done_count: current_user.todos.done.count }
end
end end
end end
private private
def todo def todo
@todo ||= current_user.todos.find(params[:id]) @todo ||= find_todos.find(params[:id])
end end
def find_todos def find_todos
@todos = TodosFinder.new(current_user, params).execute @todos ||= TodosFinder.new(current_user, params).execute
end end
end end

View file

@ -1,16 +0,0 @@
class Groups::NotificationSettingsController < Groups::ApplicationController
before_action :authenticate_user!
def update
notification_setting = current_user.notification_settings_for(group)
saved = notification_setting.update_attributes(notification_setting_params)
render json: { saved: saved }
end
private
def notification_setting_params
params.require(:notification_setting).permit(:level)
end
end

View file

@ -0,0 +1,48 @@
class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
def new
@namespace_id = project_params[:namespace_id]
@namespace_name = Namespace.find(project_params[:namespace_id]).name
@path = project_params[:path]
end
def create
unless file_is_valid?
return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
end
@project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
current_user,
File.expand_path(project_params[:file].path),
project_params[:path]).execute
if @project.saved?
redirect_to(
project_path(@project),
notice: "Project '#{@project.name}' is being imported."
)
else
redirect_to(
new_import_gitlab_project_path,
alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}"
)
end
end
private
def file_is_valid?
project_params[:file] && project_params[:file].respond_to?(:read)
end
def verify_gitlab_project_import_enabled
render_404 unless gitlab_project_import_enabled?
end
def project_params
params.permit(
:path, :namespace_id, :file
)
end
end

View file

@ -0,0 +1,36 @@
class NotificationSettingsController < ApplicationController
before_action :authenticate_user!
def create
project = Project.find(params[:project][:id])
return render_404 unless can?(current_user, :read_project, project)
@notification_setting = current_user.notification_settings_for(project)
@saved = @notification_setting.update_attributes(notification_setting_params)
render_response
end
def update
@notification_setting = current_user.notification_settings.find(params[:id])
@saved = @notification_setting.update_attributes(notification_setting_params)
render_response
end
private
def render_response
render json: {
html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting),
saved: @saved
}
end
def notification_setting_params
allowed_fields = NotificationSetting::EMAIL_EVENTS.dup
allowed_fields << :level
params.require(:notification_setting).permit(allowed_fields)
end
end

View file

@ -5,7 +5,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
def unlink def unlink
provider = params[:provider] provider = params[:provider]
current_user.identities.find_by(provider: provider).destroy current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml'
redirect_to profile_account_path redirect_to profile_account_path
end end
end end

View file

@ -1,13 +1,13 @@
class Profiles::NotificationsController < Profiles::ApplicationController class Profiles::NotificationsController < Profiles::ApplicationController
def show def show
@user = current_user @user = current_user
@group_notifications = current_user.notification_settings.for_groups @group_notifications = current_user.notification_settings.for_groups.order(:id)
@project_notifications = current_user.notification_settings.for_projects @project_notifications = current_user.notification_settings.for_projects.order(:id)
@global_notification_setting = current_user.global_notification_setting @global_notification_setting = current_user.global_notification_setting
end end
def update def update
if current_user.update_attributes(user_params) && update_notification_settings if current_user.update_attributes(user_params)
flash[:notice] = "Notification settings saved" flash[:notice] = "Notification settings saved"
else else
flash[:alert] = "Failed to save new settings" flash[:alert] = "Failed to save new settings"
@ -19,16 +19,4 @@ class Profiles::NotificationsController < Profiles::ApplicationController
def user_params def user_params
params.require(:user).permit(:notification_email) params.require(:user).permit(:notification_email)
end 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 end

View file

@ -0,0 +1,42 @@
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
before_action :load_personal_access_tokens, only: :index
def index
@personal_access_token = current_user.personal_access_tokens.build
end
def create
@personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
if @personal_access_token.save
flash[:personal_access_token] = @personal_access_token.token
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
else
load_personal_access_tokens
render :index
end
end
def revoke
@personal_access_token = current_user.personal_access_tokens.find(params[:id])
if @personal_access_token.revoke!
flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
else
flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}."
end
redirect_to profile_personal_access_tokens_path
end
private
def personal_access_token_params
params.require(:personal_access_token).permit(:name, :expires_at)
end
def load_personal_access_tokens
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
end
end

View file

@ -74,7 +74,7 @@ class Projects::ApplicationController < ApplicationController
end end
def require_branch_head def require_branch_head
unless @repository.branch_names.include?(@ref) unless @repository.branch_exists?(@ref)
redirect_to( redirect_to(
namespace_project_tree_path(@project.namespace, @project, @ref), namespace_project_tree_path(@project.namespace, @project, @ref),
notice: "This action is not allowed unless you are on a branch" notice: "This action is not allowed unless you are on a branch"

View file

@ -1,16 +0,0 @@
class Projects::NotificationSettingsController < Projects::ApplicationController
before_action :authenticate_user!
def update
notification_setting = current_user.notification_settings_for(project)
saved = notification_setting.update_attributes(notification_setting_params)
render json: { saved: saved }
end
private
def notification_setting_params
params.require(:notification_setting).permit(:level)
end
end

View file

@ -54,6 +54,6 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def commit def commit
@commit ||= @pipeline.commit_data @commit ||= @pipeline.commit
end end
end end

View file

@ -6,11 +6,13 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
def create def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id]) @runner = Ci::Runner.find(params[:runner_project][:runner_id])
return head(403) if @runner.is_shared? || @runner.locked?
return head(403) unless current_user.ci_authorized_runners.include?(@runner) return head(403) unless current_user.ci_authorized_runners.include?(@runner)
path = runners_path(project) path = runners_path(project)
runner_project = @runner.assign_to(project, current_user)
if @runner.assign_to(project, current_user) if runner_project.persisted?
redirect_to path redirect_to path
else else
redirect_to path, alert: 'Failed adding runner to project' redirect_to path, alert: 'Failed adding runner to project'

View file

@ -5,10 +5,9 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
def index def index
@runners = project.runners.ordered @project_runners = project.runners.ordered
@specific_runners = current_user.ci_authorized_runners. @assignable_runners = current_user.ci_authorized_runners.
where.not(id: project.runners). assignable_for(project).ordered.page(params[:page]).per(20)
ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active @shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all) @shared_runners_count = @shared_runners.count(:all)
end end

View file

@ -6,8 +6,10 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy] before_action :authorize_admin_project!, only: [:destroy]
def index def index
sorted = VersionSorter.rsort(@repository.tag_names) @sort = params[:sort] || 'name'
@tags = Kaminari.paginate_array(sorted).page(params[:page]) @tags = @repository.tags_sorted_by(@sort)
@tags = Kaminari.paginate_array(@tags).page(params[:page])
@releases = project.releases.where(tag: @tags) @releases = project.releases.where(tag: @tags)
end end

View file

@ -1,18 +1,12 @@
class Projects::TodosController < Projects::ApplicationController class Projects::TodosController < Projects::ApplicationController
before_action :authenticate_user!, only: [:create]
def create def create
todos = TodoService.new.mark_todo(issuable, current_user) todo = TodoService.new.mark_todo(issuable, current_user)
render json: { render json: {
todo: todos, count: current_user.todos_pending_count,
count: current_user.todos.pending.count, delete_path: dashboard_todo_path(todo)
}
end
def update
current_user.todos.find_by_id(params[:id]).update(state: :done)
render json: {
count: current_user.todos.pending.count,
} }
end end
@ -22,7 +16,13 @@ class Projects::TodosController < Projects::ApplicationController
@issuable ||= begin @issuable ||= begin
case params[:issuable_type] case params[:issuable_type]
when "issue" when "issue"
@project.issues.find(params[:issuable_id]) issue = @project.issues.find(params[:issuable_id])
if can?(current_user, :read_issue, issue)
issue
else
render_404
end
when "merge_request" when "merge_request"
@project.merge_requests.find(params[:issuable_id]) @project.merge_requests.find(params[:issuable_id])
end end

View file

@ -1,13 +1,13 @@
class ProjectsController < Projects::ApplicationController class ProjectsController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
before_action :authenticate_user!, except: [:show, :activity] before_action :authenticate_user!, except: [:show, :activity, :refs]
before_action :project, except: [:new, :create] before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create] before_action :repository, except: [:new, :create]
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists? before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# Authorize # Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping] before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
before_action :event_filter, only: [:show, :activity] before_action :event_filter, only: [:show, :activity]
layout :determine_layout layout :determine_layout
@ -143,6 +143,7 @@ class ProjectsController < Projects::ApplicationController
issues: autocomplete.issues, issues: autocomplete.issues,
milestones: autocomplete.milestones, milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests, mergerequests: autocomplete.merge_requests,
labels: autocomplete.labels,
members: participants members: participants
} }
@ -185,6 +186,48 @@ class ProjectsController < Projects::ApplicationController
) )
end end
def export
@project.add_export_job(current_user: current_user)
redirect_to(
edit_project_path(@project),
notice: "Project export started. A download link will be sent by email."
)
end
def download_export
export_project_path = @project.export_project_path
if export_project_path
send_file export_project_path, disposition: 'attachment'
else
redirect_to(
edit_project_path(@project),
alert: "Project export link has expired. Please generate a new export from your project settings."
)
end
end
def remove_export
if @project.remove_exports
flash[:notice] = "Project export has been deleted."
else
flash[:alert] = "Project export could not be deleted."
end
redirect_to(edit_project_path(@project))
end
def generate_new_export
if @project.remove_exports
export
else
redirect_to(
edit_project_path(@project),
alert: "Project export could not be deleted."
)
end
end
def toggle_star def toggle_star
current_user.toggle_star(@project) current_user.toggle_star(@project)
@project.reload @project.reload
@ -208,6 +251,24 @@ class ProjectsController < Projects::ApplicationController
} }
end end
def refs
options = {
'Branches' => @repository.branch_names,
}
unless @repository.tag_count.zero?
options['Tags'] = VersionSorter.rsort(@repository.tag_names)
end
# If reference is commit id - we should add it to branch/tag selectbox
ref = Addressable::URI.unescape(params[:ref])
if ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/
options['Commits'] = [ref]
end
render json: options.to_json
end
private private
def determine_layout def determine_layout
@ -242,8 +303,14 @@ class ProjectsController < Projects::ApplicationController
project.repository_exists? && !project.empty_repo? project.repository_exists? && !project.empty_repo?
end end
# Override get_id from ExtractsPath, which returns the branch and file path # Override extract_ref from ExtractsPath, which returns the branch and file path
# for the blob/tree, which in this case is just the root of the default branch. # for the blob/tree, which in this case is just the root of the default branch.
# This way we avoid to access the repository.ref_names.
def extract_ref(_id)
[get_id, '']
end
# Override get_id from ExtractsPath in this case is just the root of the default branch.
def get_id def get_id
project.repository.root_ref project.repository.root_ref
end end

View file

@ -123,7 +123,7 @@ class TodosFinder
end end
def by_state(items) def by_state(items)
case params[:state] case params[:state].to_s
when 'done' when 'done'
items.done items.done
else else

View file

@ -101,22 +101,6 @@ module ApplicationHelper
'Never' 'Never'
end end
def grouped_options_refs
repository = @project.repository
options = [
['Branches', repository.branch_names],
['Tags', VersionSorter.rsort(repository.tag_names)]
]
# If reference is commit id - we should add it to branch/tag selectbox
if @ref && !options.flatten.include?(@ref) && @ref =~ /\A[0-9a-zA-Z]{6,52}\z/
options << ['Commit', [@ref]]
end
grouped_options_for_select(options, @ref || @project.default_branch)
end
# Define whenever show last push event # Define whenever show last push event
# with suggestion to create MR # with suggestion to create MR
def show_last_push_widget?(event) def show_last_push_widget?(event)
@ -132,7 +116,7 @@ module ApplicationHelper
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any? return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
# Skip if user removed branch right after that # Skip if user removed branch right after that
return false unless project.repository.branch_names.include?(event.branch_name) return false unless project.repository.branch_exists?(event.branch_name)
true true
end end

View file

@ -180,18 +180,22 @@ module BlobHelper
licenses = Licensee::License.all licenses = Licensee::License.all
@licenses_for_select = { @licenses_for_select = {
Popular: licenses.select(&:featured).map { |license| [license.name, license.key] }, Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } },
Other: licenses.reject(&:featured).map { |license| [license.name, license.key] } Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } }
} }
end end
def gitignore_names def gitignore_names
return @gitignore_names if defined?(@gitignore_names) @gitignore_names ||=
Gitlab::Template::Gitignore.categories.keys.map do |k|
[k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
end.to_h
end
@gitignore_names = { def gitlab_ci_ymls
Global: Gitlab::Gitignore.global.map { |gitignore| { name: gitignore.name } }, @gitlab_ci_ymls ||=
# Note that the key here doesn't cover it really Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
Languages: Gitlab::Gitignore.languages_frameworks.map{ |gitignore| { name: gitignore.name } } [k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
} end.to_h
end end
end end

View file

@ -10,7 +10,7 @@ module BranchesHelper
end end
def can_push_branch?(project, branch_name) def can_push_branch?(project, branch_name)
return false unless project.repository.branch_names.include?(branch_name) return false unless project.repository.branch_exists?(branch_name)
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
end end

View file

@ -17,7 +17,25 @@ module ButtonHelper
def clipboard_button(data = {}) def clipboard_button(data = {})
content_tag :button, content_tag :button,
icon('clipboard'), icon('clipboard'),
class: 'btn btn-clipboard', class: "btn btn-clipboard",
data: data,
type: :button
end
# Output a "Copy to Clipboard" button with a custom CSS class
#
# data - Data attributes passed to `content_tag`
# css_class - Class passed to the `content_tag`
#
# Examples:
#
# # Define the target element
# clipboard_button_with_class({clipboard_target: "div#foo"}, css_class: "btn-clipboard")
# # => "<button class='btn btn-clipboard' data-clipboard-target='div#foo'>...</button>"
def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
content_tag :button,
icon('clipboard'),
class: "btn #{css_class}",
data: data, data: data,
type: :button type: :button
end end

View file

@ -38,10 +38,10 @@ module CiStatusHelper
icon(icon_name + ' fw') icon(icon_name + ' fw')
end end
def render_commit_status(commit, tooltip_placement: 'auto left') def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '')
project = commit.project project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit) 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 end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left') def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
@ -57,10 +57,10 @@ module CiStatusHelper
private 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), link_to ci_icon_for_status(status),
path, 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)}", title: "#{type.titleize}: #{ci_label_for_status(status)}",
data: { toggle: 'tooltip', placement: tooltip_placement } data: { toggle: 'tooltip', placement: tooltip_placement }
end end

View file

@ -16,6 +16,16 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer)) commit_person_link(commit, options.merge(source: :committer))
end 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) def image_diff_class(diff)
if diff.deleted_file if diff.deleted_file
"deleted" "deleted"
@ -102,24 +112,24 @@ module CommitsHelper
if current_controller?(:projects, :commits) if current_controller?(:projects, :commits)
if @repo.blob_at(commit.id, @path) if @repo.blob_at(commit.id, @path)
return link_to( return link_to(
"Browse File »", "Browse File",
namespace_project_blob_path(project.namespace, project, namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)), tree_join(commit.id, @path)),
class: "pull-right" class: "btn btn-default"
) )
elsif @path.present? elsif @path.present?
return link_to( return link_to(
"Browse Directory »", "Browse Directory",
namespace_project_tree_path(project.namespace, project, namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)), tree_join(commit.id, @path)),
class: "pull-right" class: "btn btn-default"
) )
end end
end end
link_to( link_to(
"Browse Files", "Browse Files",
namespace_project_tree_path(project.namespace, project, commit), namespace_project_tree_path(project.namespace, project, commit),
class: "pull-right" class: "btn btn-default"
) )
end end
@ -187,12 +197,10 @@ module CommitsHelper
source_email = clean(commit.send "#{options[:source]}_email".to_sym) source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_name = user.try(:name) || source_name person_name = user.try(:name) || source_name
person_email = user.try(:email) || source_email
text = text =
if options[:avatar] 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{<span class="commit-#{options[:source]}-name">#{person_name}</span>}
%Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
else else
person_name person_name
end end

View file

@ -135,6 +135,11 @@ module DiffHelper
toggle_whitespace_link(url, options) toggle_whitespace_link(url, options)
end 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 private
def hide_whitespace? def hide_whitespace?

View file

@ -50,8 +50,6 @@ module GitlabMarkdownHelper
context[:project] ||= @project context[:project] ||= @project
text = Banzai.pre_process(text, context)
html = Banzai.render(text, context) html = Banzai.render(text, context)
context.merge!( context.merge!(

View file

@ -67,9 +67,9 @@ module IssuablesHelper
end end
end end
def has_todo(issuable) def issuable_todo(issuable)
unless current_user.nil? if current_user
current_user.todos.find_by(target_id: issuable.id, state: :pending) current_user.todos.find_by(target: issuable, state: :pending)
end end
end end

View file

@ -6,10 +6,10 @@ module MembersHelper
"#{action}_#{member.type.underscore}".to_sym "#{action}_#{member.type.underscore}".to_sym
end end
def can_see_member_roles?(source:, user: nil) def default_show_roles(member)
return false unless user can?(current_user, action_member_permission(:update, member), member) ||
can?(current_user, action_member_permission(:destroy, member), member) ||
user.is_admin? || source.members.exists?(user_id: user.id) can?(current_user, action_member_permission(:admin, member), member.source)
end end
def remove_member_message(member, user: nil) def remove_member_message(member, user: nil)

View file

@ -12,10 +12,10 @@ module NavHelper
end end
def page_sidebar_class def page_sidebar_class
if nav_menu_collapsed? if pinned_nav?
"page-sidebar-collapsed" "page-sidebar-expanded page-sidebar-pinned"
else else
"page-sidebar-expanded" "page-sidebar-collapsed"
end end
end end
@ -36,7 +36,15 @@ module NavHelper
end end
def nav_header_class 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 class_name
end end
@ -47,4 +55,8 @@ module NavHelper
def nav_control_class def nav_control_class
"nav-control" if current_user "nav-control" if current_user
end end
def pinned_nav?
cookies[:pin_nav] == 'true'
end
end end

View file

@ -34,7 +34,7 @@ module NotificationsHelper
def notification_description(level) def notification_description(level)
case level.to_sym case level.to_sym
when :participating when :participating
'You will only receive notifications from related resources' 'You will only receive notifications for threads you have participated in'
when :mention when :mention
'You will receive notifications only for comments in which you were @mentioned' 'You will receive notifications only for comments in which you were @mentioned'
when :watch when :watch
@ -43,6 +43,8 @@ module NotificationsHelper
'You will not get any notifications via email' 'You will not get any notifications via email'
when :global when :global
'Use your global notification setting' 'Use your global notification setting'
when :custom
'You will only receive notifications for the events you choose'
end end
end end
@ -62,22 +64,14 @@ module NotificationsHelper
end end
end end
def notification_level_radio_buttons # Identifier to trigger individually dropdowns and custom settings modals in the same view
html = "" def notifications_menu_identifier(type, notification_setting)
"#{type}-#{notification_setting.user_id}-#{notification_setting.source_id}-#{notification_setting.source_type}"
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 end
html.html_safe # Create hidden field to send notification setting source to controller
def hidden_setting_source_input(notification_setting)
return unless notification_setting.source_type
hidden_field_tag "#{notification_setting.source_type.downcase}[id]", notification_setting.source_id
end end
end end

View file

@ -41,7 +41,7 @@ module ProjectsHelper
author_html = author_html.html_safe author_html = author_html.html_safe
if opts[:name] 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 else
title = opts[:title].sub(":name", sanitize(author.name)) 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 link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe

View file

@ -1,10 +1,10 @@
module TodosHelper module TodosHelper
def todos_pending_count def todos_pending_count
current_user.todos.pending.count TodosFinder.new(current_user, state: :pending).execute.count
end end
def todos_done_count def todos_done_count
current_user.todos.done.count TodosFinder.new(current_user, state: :done).execute.count
end end
def todo_action_name(todo) def todo_action_name(todo)
@ -12,7 +12,7 @@ module TodosHelper
when Todo::ASSIGNED then 'assigned you' when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on' when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your' when Todo::BUILD_FAILED then 'The build failed for your'
when Todo::MARKED then 'marked this as a Todo for' when Todo::MARKED then 'added a todo for'
end end
end end

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