Commit 25e078c1 authored by James Lopez's avatar James Lopez

Merge branches 'feature/project-export' and 'feature/project-import' of...

Merge branches 'feature/project-export' and 'feature/project-import' of gitlab.com:gitlab-org/gitlab-ce into feature/project-import
parents 37c68fe0 ba1fcf36
......@@ -15,6 +15,7 @@
.sass-cache/
.secret
.vagrant
.byebug_history
Vagrantfile
backups/*
config/aws.yml
......
......@@ -12,16 +12,18 @@ cache:
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
# retry tests only in CI environment
RSPEC_RETRY_RETRY_COUNT: "3"
before_script:
- source ./scripts/prepare_build.sh
- ruby -v
- which ruby
- gem install bundler --no-ri --no-rdoc
- retry gem install bundler --no-ri --no-rdoc
- cp config/gitlab.yml.example config/gitlab.yml
- touch log/application.log
- touch log/test.log
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
stages:
......@@ -69,15 +71,6 @@ spec:services:
- ruby
- mysql
spec:benchmark:
stage: test
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
tags:
- ruby
- mysql
allow_failure: true
spec:other:
stage: test
script:
......@@ -241,22 +234,6 @@ spec:services:ruby22:
- ruby
- mysql
spec:benchmark:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
cache:
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
allow_failure: true
spec:other:ruby22:
stage: test
image: ruby:2.2
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased)
- Support Golang subpackage fetching (Stan Hu)
- Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during
setup. A password can be provided during setup (see installation docs), or
GitLab will ask the user to create a new one upon first visit.
- Fix issue when pushing to projects ending in .wiki
- Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
- Don't load all of GitLab in mail_room
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
- Strip leading and trailing spaces in URL validator (evuez)
- Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez)
- Return empty array instead of 404 when commit has no statuses in commit status API
- Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
- Rewrite logo to simplify SVG code (Sean Lang)
- Refactor and greatly improve search performance
- Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view
- Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino)
- Show labels in dashboard and group milestone views
- Add main language of a project in the list of projects (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
- Move group activity to separate page
v 8.5.5
- Ensure removing a project removes associated Todo entries
- Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior
- Add #upcoming filter to Milestone filter (Tiago Botelho)
v 8.5.4
- Do not cache requests for badges (including builds badge)
......@@ -23,6 +46,7 @@ v 8.5.3
- Sort starred projects on dashboard based on last activity by default
- Show commit message in JIRA mention comment
- Makes issue page and merge request page usable on mobile browsers.
- Improved UI for profile settings
v 8.5.2
- Fix sidebar overlapping content when screen width was below 1200px
......@@ -64,7 +88,7 @@ v 8.5.1
v 8.5.0
- Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu)
- Cache various Repository methods to improve performance (Yorick Peterse)
- Fix duplicated branch creation/deletion Web hooks/service notifications when using Web UI (Stan Hu)
- Fix duplicated branch creation/deletion Webhooks/service notifications when using Web UI (Stan Hu)
- Ensure rake tasks that don't need a DB connection can be run without one
- Update New Relic gem to 3.14.1.311 (Stan Hu)
- Add "visibility" flag to GET /projects api endpoint
......@@ -197,7 +221,7 @@ v 8.4.0
- Add housekeeping function to project settings page
- The default GitLab logo now acts as a loading indicator
- Fix caching issue where build status was not updating in project dashboard (Stan Hu)
- Accept 2xx status codes for successful Web hook triggers (Stan Hu)
- Accept 2xx status codes for successful Webhook triggers (Stan Hu)
- Fix missing date of month in network graph when commits span a month (Stan Hu)
- Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu)
- Don't notify users twice if they are both project watchers and subscribers (Stan Hu)
......@@ -297,7 +321,7 @@ v 8.3.0
- Fix broken group avatar upload under "New group" (Stan Hu)
- Update project repositorize size and commit count during import:repos task (Stan Hu)
- Fix API setting of 'public' attribute to false will make a project private (Stan Hu)
- Handle and report SSL errors in Web hook test (Stan Hu)
- Handle and report SSL errors in Webhook test (Stan Hu)
- Bump Redis requirement to 2.8 for Sidekiq 4 (Stan Hu)
- Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera)
- WIP identifier on merge requests no longer requires trailing space
......@@ -517,7 +541,7 @@ v 8.1.0
- Ensure code blocks are properly highlighted after a note is updated
- Fix wrong access level badge on MR comments
- Hide password in the service settings form
- Move CI web hooks page to project settings area
- Move CI webhooks page to project settings area
- Fix User Identities API. It now allows you to properly create or update user's identities.
- Add user preference to change layout width (Peter Göbel)
- Use commit status in merge request widget as preferred source of CI status
......@@ -560,7 +584,7 @@ v 8.0.3
- Fix URL shown in Slack notifications
- Fix bug where projects would appear to be stuck in the forked import state (Stan Hu)
- Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu)
- Add work_in_progress key to MR web hooks (Ben Boeckel)
- Add work_in_progress key to MR webhooks (Ben Boeckel)
v 8.0.2
- Fix default avatar not rendering in network graph (Stan Hu)
......@@ -851,7 +875,7 @@ v 7.12.0
- Fix milestone "Browse Issues" button.
- Set milestone on new issue when creating issue from index with milestone filter active.
- Make namespace API available to all users (Stan Hu)
- Add web hook support for note events (Stan Hu)
- Add webhook support for note events (Stan Hu)
- Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu)
- Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu)
- Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu)
......@@ -958,7 +982,7 @@ v 7.11.0
- Add "Create Merge Request" buttons to commits and branches pages and push event.
- Show user roles by comments.
- Fix automatic blocking of auto-created users from Active Directory.
- Call merge request web hook for each new commits (Arthur Gautier)
- Call merge request webhook for each new commits (Arthur Gautier)
- Use SIGKILL by default in Sidekiq::MemoryKiller
- Fix mentioning of private groups.
- Add style for <kbd> element in markdown
......@@ -1132,7 +1156,7 @@ v 7.9.0
- Add brakeman (security scanner for Ruby on Rails)
- Slack username and channel options
- Add grouped milestones from all projects to dashboard.
- Web hook sends pusher email as well as commiter
- Webhook sends pusher email as well as commiter
- Add Bitbucket omniauth provider.
- Add Bitbucket importer.
- Support referencing issues to a project whose name starts with a digit
......@@ -1255,7 +1279,7 @@ v 7.8.0
- Allow notification email to be set separately from primary email.
- API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger)
- Don't have Markdown preview fail for long comments/wiki pages.
- When test web hook - show error message instead of 500 error page if connection to hook url was reset
- When test webhook - show error message instead of 500 error page if connection to hook url was reset
- Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov)
- Added persistent collapse button for left side nav bar (Jason Blanchard)
- Prevent losing unsaved comments by automatically restoring them when comment page is loaded again.
......@@ -1272,7 +1296,7 @@ v 7.8.0
- Show projects user contributed to on user page. Show stars near project on user page.
- Improve database performance for GitLab
- Add Asana service (Jeremy Benoist)
- Improve project web hooks with extra data
- Improve project webhooks with extra data
v 7.7.2
- Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
......@@ -1757,7 +1781,7 @@ v 6.4.0
- Side-by-side diff view (Steven Thonus)
- Internal projects (Jason Hollingsworth)
- Allow removal of avatar (Drew Blessing)
- Project web hooks now support issues and merge request events
- Project webhooks now support issues and merge request events
- Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth)
- Expire event cache on avatar creation/removal (Drew Blessing)
- Archiving old projects (Steven Thonus)
......@@ -1827,7 +1851,7 @@ v 6.2.0
- Added search for projects by name to api (Izaak Alpert)
- Make default user theme configurable (Izaak Alpert)
- Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev)
- Rake tasks for web hooks management (Jonhnny Weslley)
- Rake tasks for webhooks management (Jonhnny Weslley)
- Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov)
- API: Remove group
- API: Remove project
......@@ -2030,7 +2054,7 @@ v 4.2.0
- Async gitolite calls
- added satellites logs
- can_create_group, can_create_team booleans for User
- Process web hooks async
- Process webhooks async
- GFM: Fix images escaped inside links
- Network graph improved
- Switchable branches for network graph
......@@ -2064,7 +2088,7 @@ v 4.1.0
v 4.0.0
- Remove project code and path from API. Use id instead
- Return valid cloneable url to repo for web hook
- Return valid cloneable url to repo for webhook
- Fixed backup issue
- Reorganized settings
- Fixed commits compare
......
......@@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.2.0'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.4.2'
gem 'omniauth-saml', '~> 1.5.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
......@@ -263,7 +263,9 @@ group :development, :test do
gem 'database_cleaner', '~> 1.4.0'
gem 'factory_girl_rails', '~> 4.6.0'
gem 'rspec-rails', '~> 3.3.0'
gem 'rspec-retry'
gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
......@@ -273,7 +275,7 @@ group :development, :test do
gem 'capybara', '~> 2.4.0'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.8.1'
gem 'poltergeist', '~> 1.9.0'
gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine', '~> 2.2.0'
......
......@@ -358,7 +358,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
gitlab_git (9.0.0)
gitlab_git (9.0.1)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -532,8 +532,8 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
omniauth-saml (1.4.2)
omniauth (~> 1.1)
omniauth-saml (1.5.0)
omniauth (~> 1.3)
ruby-saml (~> 1.1, >= 1.1.1)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
......@@ -552,7 +552,7 @@ GEM
parser (2.2.3.0)
ast (>= 1.1, < 3.0)
pg (0.18.4)
poltergeist (1.8.1)
poltergeist (1.9.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
......@@ -679,6 +679,8 @@ GEM
rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0)
rspec-retry (0.4.5)
rspec-core
rspec-support (3.3.0)
rubocop (0.35.1)
astrolabe (~> 1.3)
......@@ -690,7 +692,7 @@ GEM
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-progressbar (1.7.5)
ruby-saml (1.1.1)
ruby-saml (1.1.2)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
ruby2ruby (2.2.0)
......@@ -764,6 +766,8 @@ GEM
capybara (>= 2.0.0)
railties (>= 3)
spinach (>= 0.4)
spinach-rerun-reporter (0.0.2)
spinach (~> 0.8)
spring (1.6.4)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
......@@ -971,14 +975,14 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.2.0)
omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.4.2)
omniauth-saml (~> 1.5.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
paranoia (~> 2.0)
pg (~> 0.18.2)
poltergeist (~> 1.8.1)
poltergeist (~> 1.9.0)
pry-rails
quiet_assets (~> 1.0.2)
rack-attack (~> 4.3.1)
......@@ -999,6 +1003,7 @@ DEPENDENCIES
rouge (~> 1.10.1)
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0)
rspec-retry
rubocop (~> 0.35.0)
ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0)
......@@ -1017,6 +1022,7 @@ DEPENDENCIES
six (~> 0.2.0)
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.6.4)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0)
......
......@@ -4,6 +4,7 @@
namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
labels_path: "/api/:version/projects/:id/labels"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
......@@ -61,6 +62,19 @@
).done (projects) ->
callback(projects)
newLabel: (project_id, data, callback) ->
url = Api.buildUrl(Api.labels_path)
url = url.replace(':id', project_id)
data.private_token = gon.api_token
$.ajax(
url: url
type: "POST"
data: data
dataType: "json"
).done (label) ->
callback(label)
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
......
......@@ -220,17 +220,17 @@ $ ->
.off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs'
$gutterIcon = $('aside .gutter-toggle').find('i')
$gutterIcon = $('.js-sidebar-toggle').find('i')
if $gutterIcon.hasClass('fa-angle-double-right')
$gutterIcon.closest('a').trigger('click')
$(document)
.off 'click', 'aside .gutter-toggle'
.on 'click', 'aside .gutter-toggle', (e, triggered) ->
.off 'click', '.js-sidebar-toggle'
.on 'click', '.js-sidebar-toggle', (e, triggered) ->
e.preventDefault()
$this = $(this)
$thisIcon = $this.find 'i'
$allGutterToggleIcons = $('.gutter-toggle i')
$allGutterToggleIcons = $('.js-sidebar-toggle i')
if $thisIcon.hasClass('fa-angle-double-right')
$allGutterToggleIcons
.removeClass('fa-angle-double-right')
......
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
$(".add-award").click (event) =>
$(".js-add-award").on "click", (event) =>
event.stopPropagation()
event.preventDefault()
......@@ -9,27 +9,46 @@ class @AwardsHandler
$("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible")
$(".emoji-menu").hide()
$(".emoji-menu").removeClass "is-visible"
$(".awards")
.off "click"
.on "click", ".js-emoji-btn", @handleClick
@renderFrequentlyUsedBlock()
@setupSearch()
handleClick: (e) ->
e.preventDefault()
emoji = $(this)
.find(".icon")
.data "emoji"
awards_handler.addAward emoji
showEmojiMenu: ->
if $(".emoji-menu").length
$(".emoji-menu").show()
if $(".emoji-menu").is ".is-visible"
$(".emoji-menu").removeClass "is-visible"
$("#emoji_search").blur()
else
$(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus()
else
$.get "/emojis", (response) ->
$(".add-award").after response
$(".emoji-menu").show()
$('.js-add-award').addClass "is-loading"
$.get "/emojis", (response) =>
$('.js-add-award').removeClass "is-loading"
$(".js-award-holder").append response
setTimeout =>
$(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus()
@setupSearch()
, 200
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
$(".emoji-menu").hide()
$(".emoji-menu").removeClass "is-visible"
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
......@@ -39,7 +58,7 @@ class @AwardsHandler
if @isActive(emoji)
@decrementCounter(emoji)
else
counter = @findEmojiIcon(emoji).siblings(".counter")
counter = @findEmojiIcon(emoji).siblings(".js-counter")
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
......@@ -53,7 +72,7 @@ class @AwardsHandler
@findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter")
counter = @findEmojiIcon(emoji).siblings(".js-counter")
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
......@@ -70,9 +89,13 @@ class @AwardsHandler
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ")
authors = award_block
.attr("data-original-title")
.split(", ")
authors.splice(authors.indexOf("me"),1)
award_block.closest(".award").attr("data-original-title", authors.join(", "))
award_block
.closest(".js-emoji-btn")
.attr("data-original-title", authors.join(", "))
@resetTooltip(award_block)
addMeToAuthorList: (emoji) ->
......@@ -98,14 +121,18 @@ class @AwardsHandler
emojiCssClass = @resolveNameToCssClass(emoji)
nodes = []
nodes.push("<div class='award active' title='me'>")
nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>")
nodes.push("<div class='counter'>1</div>")
nodes.push("</div>")
emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji)
$(".award").tooltip()
nodes.push(
"<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
"<span class='award-control-text js-counter'>1</span>",
"</button>"
)
emoji_node = $(nodes.join("\n"))
.insertBefore(".js-award-holder")
.find(".emoji-icon")
.data("emoji", emoji)
$('.award-control').tooltip()
resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
......@@ -128,7 +155,7 @@ class @AwardsHandler
callback.call()
findEmojiIcon: (emoji) ->
$(".award [data-emoji='#{emoji}']")
$(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
......@@ -164,13 +191,13 @@ class @AwardsHandler
term = $(ev.target).val()
# Clean previous search results
$("ul.emoji-search,h5.emoji-search").remove()
$("ul.emoji-menu-search, h5.emoji-search").remove()
if term
# Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show()
ul = $("<ul>").addClass("emoji-search").append(found_emojis)
ul = $("<ul>").addClass("emoji-menu-list emoji-menu-search").append(found_emojis)
$(".emoji-menu-content ul, .emoji-menu-content h5").hide()
$(".emoji-menu-content").append(h5).append(ul)
else
......
......@@ -4,6 +4,8 @@ class CiBuild
constructor: (build_url, build_status) ->
clearInterval(CiBuild.interval)
@initScrollButtonAffix()
if build_status == "running" || build_status == "pending"
#
# Bind autoscroll button to follow build output
......@@ -38,4 +40,15 @@ class CiBuild
checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
initScrollButtonAffix: ->
$buildScroll = $('#js-build-scroll')
$body = $('body')
$buildTrace = $('#build-trace')
$buildScroll.affix(
offset:
bottom: ->
$body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
)
@CiBuild = CiBuild
......@@ -23,7 +23,7 @@ class Dispatcher
new Issue()
shortcut_handler = new ShortcutsIssuable()
new ZenMode()
when 'projects:milestones:show'
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
......@@ -74,8 +74,9 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
new TreeView() if $('#tree-slider').length
when 'groups:show'
when 'groups:activity'
new Activities()
when 'groups:show'
shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index'
new GroupMembers()
......
class GitLabDropdownFilter
BLUR_KEYCODES = [27, 40]
constructor: (@dropdown, @options) ->
@input = @dropdown.find(".dropdown-input .dropdown-input-field")
# Key events
timeout = ""
@input.on "keyup", (e) =>
if e.keyCode is 13 && @input.val() isnt ""
if @options.enterCallback
@options.enterCallback()
return
clearTimeout timeout
timeout = setTimeout =>
blur_field = @shouldBlur e.keyCode
search_text = @input.val()
if blur_field
@input.blur()
if @options.remote
@options.query search_text, (data) =>
@options.callback(data)
else
@filter search_text
, 250
shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0
filter: (search_text) ->
data = @options.data()
results = data
if search_text isnt ""
results = fuzzaldrinPlus.filter(data, search_text,
key: @options.keys
)
@options.callback results
class GitLabDropdownRemote
constructor: (@dataEndpoint, @options) ->
execute: ->
if typeof @dataEndpoint is "string"
@fetchData()
else if typeof @dataEndpoint is "function"
if @options.beforeSend
@options.beforeSend()
# Fetch the data by calling the data funcfion
@dataEndpoint "", (data) =>
if @options.success
@options.success(data)
if @options.beforeSend
@options.beforeSend()
# Fetch the data through ajax if the data is a string
fetchData: ->
$.ajax(
url: @dataEndpoint,
dataType: @options.dataType,
beforeSend: =>
if @options.beforeSend
@options.beforeSend()
success: (data) =>
if @options.success
@options.success(data)
)
class GitLabDropdown
LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
constructor: (@el, @options) ->
self = @
@dropdown = $(@el).parent()
search_fields = if @options.search then @options.search.fields else [];
if @options.data
# Remote data
@remote = new GitLabDropdownRemote @options.data, {
dataType: @options.dataType,
beforeSend: @toggleLoading.bind(@)
success: (data) =>
@fullData = data
@parseData @fullData
}
# Init filiterable
if @options.filterable
@filter = new GitLabDropdownFilter @dropdown,
remote: @options.filterRemote
query: @options.data
keys: @options.search.fields
data: =>
return @fullData
callback: (data) =>
@parseData data
enterCallback: =>
@selectFirstRow()
# Event listeners
@dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden
if @dropdown.find(".dropdown-toggle-page").length
@dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
e.preventDefault()
e.stopPropagation()
@togglePage()
if @options.selectable
selector = ".dropdown-content a"
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) ->
self.rowClicked $(@)
if self.options.clicked
self.options.clicked()
toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
togglePage: ->
menu = $('.dropdown-menu', @dropdown)
if menu.hasClass(PAGE_TWO_CLASS)
if @remote
@remote.execute()
menu.toggleClass PAGE_TWO_CLASS
parseData: (data) ->
@renderedData = data
# Render each row
html = $.map data, (obj) =>
return @renderItem(obj)
if @options.filterable and data.length is 0
# render no matching results
html = [@noResults()]
# Render the full menu
full_html = @renderMenu(html.join(""))
@appendMenu(full_html)
opened: =>
contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is ""
@remote.execute()
if @options.filterable
@dropdown.find(".dropdown-input-field").focus()
hidden: =>
if @options.filterable
@dropdown.find(".dropdown-input-field").blur().val("")
if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
# Render the full menu
renderMenu: (html) ->
menu_html = ""
if @options.renderMenu
menu_html = @options.renderMenu(html)
else
menu_html = "<ul>#{html}</ul>"
return menu_html
# Append the menu into the dropdown
appendMenu: (html) ->
selector = '.dropdown-content'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content"
$(selector, @dropdown).html html
# Render the row
renderItem: (data) ->
html = ""
return "<li class='divider'></li>" if data is "divider"
if @options.renderRow
# Call the render function
html = @options.renderRow(data)
else
selected = if @options.isSelected then @options.isSelected(data) else false
url = if @options.url then @options.url(data) else "#"
text = if @options.text then @options.text(data) else ""
cssClass = "";
if selected
cssClass = "is-active"
html = "<li>"
html += "<a href='#{url}' class='#{cssClass}'>"
html += text
html += "</a>"
html += "</li>"
return html
noResults: ->
html = "<li>"
html += "<a href='#' class='is-focused'>"
html += "No matching results."
html += "</a>"
html += "</li>"
rowClicked: (el) ->
fieldName = @options.fieldName
field = @dropdown.parent().find("input[name='#{fieldName}']")
if el.hasClass(ACTIVE_CLASS)
field.remove()
else
fieldName = @options.fieldName
selectedIndex = el.parent().index()
if @renderedData
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
if @options.multiSelect
oldValue = field.val()
if oldValue
value = "#{oldValue},#{value}"
else
@dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
field.remove()
# Toggle active class for the tick mark
el.toggleClass "is-active"
if value
if !field.length
# Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' />"
@dropdown.before input
@dropdown.parent().find("input[name='#{fieldName}']").val value
selectFirstRow: ->
selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a"
# similute a click on the first link
$(selector).trigger "click"
$.fn.glDropdown = (opts) ->
return @.each ->
new GitLabDropdown @, opts
class @IssueStatusSelect
constructor: ->
$('.js-issue-status').each (i, el) ->
fieldName = $(el).data("field-name")
$(el).glDropdown(
selectable: true
fieldName: fieldName
id: (obj, el) ->
$(el).data("id")
)
class @LabelsSelect
constructor: ->
$('.js-label-select').each (i, dropdown) ->
projectId = $(dropdown).data('project-id')
labelUrl = $(dropdown).data("labels")
selectedLabel = $(dropdown).data('selected')
if selectedLabel
selectedLabel = selectedLabel.split(",")
newLabelField = $('#new_label_name')
newColorField = $('#new_label_color')
showNo = $(dropdown).data('show-no')
showAny = $(dropdown).data('show-any')
if newLabelField.length
$('.suggest-colors-dropdown a').on "click", (e) ->
e.preventDefault()
e.stopPropagation()
newColorField.val $(this).data("color")
$('.js-dropdown-label-color-preview')
.css 'background-color', $(this).data("color")
.addClass 'is-active'
$('.js-new-label-btn').on "click", (e) ->
e.preventDefault()
e.stopPropagation()
if newLabelField.val() isnt "" && newColorField.val() isnt ""
$('.js-new-label-btn').disable()
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
$('.dropdown-menu-back', $(dropdown).parent()).trigger "click"
$(dropdown).glDropdown(
data: (term, callback) ->
# We have to fetch the JS version of the labels list because there is no
# public facing JSON url for labels
$.ajax(
url: labelUrl
).done (data) ->
html = $(data)
data = []
html.find('.label-row a').each ->
data.push(
title: $(@).text().trim()
)
if showNo
data.unshift(
id: "0"
title: 'No label'
)
if showAny
data.unshift(
title: 'Any label'
)
if data.length > 2
data.splice 2, 0, "divider"
callback data
renderRow: (label) ->
if $.isArray(selectedLabel)
selected = ""
$.each selectedLabel, (i, selectedLbl) ->
selectedLbl = selectedLbl.trim()
if selected is "" && label.title is selectedLbl
selected = "is-active"
else
selected = if label.title is selectedLabel then "is-active" else ""
"<li>
<a href='#' class='#{selected}'>
#{label.title}
</a>
</li>"
filterable: true
search:
fields: ['title']
selectable: true
fieldName: $(dropdown).data('field-name')
id: (label) ->
label.title
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
)
......@@ -6,6 +6,7 @@
class @MarkdownPreview
# Minimum number of users referenced before triggering a warning
referenceThreshold: 10
ajaxCache: {}
showPreview: (form) ->
preview = form.find('.js-md-preview')
......@@ -24,12 +25,16 @@ class @MarkdownPreview
renderMarkdown: (text, success) ->
return unless window.markdown_preview_path
return success(@ajaxCache.response) if text == @ajaxCache.text
$.ajax
type: 'POST'
url: window.markdown_preview_path
data: { text: text }
dataType: 'json'
success: success
success: (response) =>
@ajaxCache = text: text, response: response
success(response)
hideReferencedUsers: (form) ->
referencedUsers = form.find('.referenced-users')
......@@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview()
previewButtonSelector = '.js-md-preview-button'
writeButtonSelector = '.js-md-write-button'
lastTextareaPreviewed = null
$.fn.setupMarkdownPreview = ->
$form = $(this)
......@@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = ->
form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
form_textarea.on 'blur', -> markdownPreview.showPreview($form)
$(document).on 'click', previewButtonSelector, (e) ->
e.preventDefault()
$(document).on 'markdown-preview:show', (e, $form) ->
return unless $form
$form = $(this).closest('form')
lastTextareaPreviewed = $form.find('textarea.markdown-area')
# toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active')
......@@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) ->
markdownPreview.showPreview($form)
$(document).on 'click', writeButtonSelector, (e) ->
e.preventDefault()
$(document).on 'markdown-preview:hide', (e, $form) ->
return unless $form
$form = $(this).closest('form')
lastTextareaPreviewed = null
# toggle tabs
$form.find(writeButtonSelector).parent().addClass('active')
......@@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) ->
# toggle content
$form.find('.md-write-holder').show()
$form.find('textarea.markdown-area').focus()
$form.find('.md-preview-holder').hide()
$(document).on 'markdown-preview:toggle', (e, keyboardEvent) ->
$target = $(keyboardEvent.target)
if $target.is('textarea.markdown-area')
$(document).triggerHandler('markdown-preview:show', [$target.closest('form')])
keyboardEvent.preventDefault()
else if lastTextareaPreviewed
$target = lastTextareaPreviewed
$(document).triggerHandler('markdown-preview:hide', [$target.closest('form')])
keyboardEvent.preventDefault()
$(document).on 'click', previewButtonSelector, (e) ->
e.preventDefault()
$form = $(this).closest('form')
$(document).triggerHandler('markdown-preview:show', [$form])
$(document).on 'click', writeButtonSelector, (e) ->
e.preventDefault()
$form = $(this).closest('form')
$(document).triggerHandler('markdown-preview:hide', [$form])
......@@ -189,7 +189,7 @@ class @MergeRequestTabs
$('.container-fluid').removeClass('container-limited')
shrinkView: ->
$gutterIcon = $('.gutter-toggle i')
$gutterIcon = $('.js-sidebar-toggle i')
# Wait until listeners are set
setTimeout( ->
......@@ -197,4 +197,3 @@ class @MergeRequestTabs
if $gutterIcon.is('.fa-angle-double-right')
$gutterIcon.closest('a').trigger('click',[true])
, 0)
......@@ -69,7 +69,7 @@ class @Milestone
@bindIssuesSorting()
@bindMergeRequestSorting()
@bindTabsSwitching
@bindTabsSwitching()
bindIssuesSorting: ->
$("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
......@@ -104,7 +104,7 @@ class @Milestone
).disableSelection()
bindMergeRequestSorting: ->
bindTabsSwitching: ->
$('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
currentTabClass = $(e.target).data('show')
previousTabClass = $(e.relatedTarget).data('show')
......@@ -113,6 +113,7 @@ class @Milestone
$(currentTabClass).removeClass('hidden')
$(currentTabClass).show()
bindMergeRequestSorting: ->
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true,
......
class @MilestoneSelect
constructor: ->
$('.js-milestone-select').each (i, dropdown) ->
projectId = $(dropdown).data('project-id')
milestonesUrl = $(dropdown).data('milestones')
selectedMilestone = $(dropdown).data('selected')
showNo = $(dropdown).data('show-no')
showAny = $(dropdown).data('show-any')
useId = $(dropdown).data('use-id')
$(dropdown).glDropdown(
data: (term, callback) ->
$.ajax(
url: milestonesUrl
).done (data) ->
html = $(data)
data = []
html.find('.milestone strong a').each ->
link = $(@).attr("href").split("/")
data.push(
id: link[link.length - 1]
title: $(@).text().trim()
)
if showNo
data.unshift(
id: "0"
title: 'No Milestone'
)
if showAny
data.unshift(
title: 'Any Milestone'
)
if data.length > 2
data.splice 2, 0, "divider"
callback(data)
filterable: true
search:
fields: ['title']
selectable: true
fieldName: $(dropdown).data('field-name')
text: (milestone) ->
milestone.title
id: (milestone) ->
if !useId
if milestone.title isnt "Any milestone"
milestone.title
else
""
else
milestone.id
isSelected: (milestone) ->
milestone.title is selectedMilestone
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
)
......@@ -30,8 +30,11 @@ class @Notes
$(document).on "ajax:success", ".js-main-target-form", @addNote
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
# catch note ajax errors
$(document).on "ajax:error", ".js-main-target-form", @addNoteError
# change note in UI after update
$(document).on "ajax:success", "form.edit_note", @updateNote
$(document).on "ajax:success", "form.edit-note", @updateNote
# Edit note link
$(document).on "click", ".js-note-edit", @showEditForm
......@@ -51,6 +54,9 @@ class @Notes
$(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
$(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
# reset main target form when clicking discard
$(document).on "click", ".js-note-discard", @resetMainTargetForm
# update the file name when an attachment is selected
$(document).on "change", ".js-note-attachment-input", @updateFormAttachment
......@@ -72,7 +78,7 @@ class @Notes
cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form"
$(document).off "ajax:success", "form.edit_note"
$(document).off "ajax:success", "form.edit-note"
$(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete"
......@@ -85,6 +91,7 @@ class @Notes
$(document).off "keyup", ".js-note-text"
$(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close"
$(document).off "click", ".js-note-discard"
$('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container'
......@@ -219,7 +226,7 @@ class @Notes
Resets text and preview.
Resets buttons.
###
resetMainTargetForm: ->
resetMainTargetForm: (e) =>
form = $(".js-main-target-form")
# remove validation errors
......@@ -231,6 +238,8 @@ class @Notes
form.find(".js-note-text").data("autosave").reset()
@updateTargetButtons(e)
reenableTargetFormSubmitButton: ->
form = $(".js-main-target-form")
......@@ -274,8 +283,10 @@ class @Notes
form.removeClass "js-new-note-form"
form.find('.div-dropzone').remove()
# hide discard button
form.find('.js-note-discard').hide()
# setup preview buttons
form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
previewButton = form.find(".js-md-preview-button")
textarea = form.find(".js-note-text")
......@@ -309,6 +320,10 @@ class @Notes
addNote: (xhr, note, status) =>
@renderNote(note)
addNoteError: (xhr, note, status) =>
flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
flash.pinTo('.md-area')
###
Called in response to the new note form being submitted
......@@ -347,21 +362,25 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-body > .note-text").hide()
note.find(".note-header").hide()
base_form = note.find(".note-edit-form")
form = base_form.clone().insertAfter(base_form)
form.addClass('current-note-edit-form gfm-form')
form.find('.div-dropzone').remove()
form = note.find(".note-edit-form")
isNewForm = form.is(':not(.gfm-form)')
if isNewForm
form.addClass('gfm-form')
form.addClass('current-note-edit-form')
form.show()
# Show the attachment delete link
note.find(".js-note-attachment-delete").show()
# Setup markdown form
if isNewForm
GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
form.show()
textarea = form.find("textarea")
textarea.focus()
if isNewForm
autosize(textarea)
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
......@@ -371,6 +390,7 @@ class @Notes
textarea.val ""
textarea.val value
if isNewForm
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
###
......@@ -383,7 +403,9 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-body > .note-text").show()
note.find(".note-header").show()
note.find(".current-note-edit-form").remove()
note.find(".current-note-edit-form")
.removeClass("current-note-edit-form")
.hide()
###
Called in response to deleting a note of any kind.
......@@ -462,6 +484,11 @@ class @Notes
form.find("#note_line_code").val dataHolder.data("lineCode")
form.find("#note_noteable_type").val dataHolder.data("noteableType")
form.find("#note_noteable_id").val dataHolder.data("noteableId")
form.find('.js-note-discard')
.show()
.removeClass('js-note-discard')
.addClass('js-close-discussion-note-form')
.text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form
form.find(".js-note-text").focus()
form.addClass "js-discussion-note-form"
......@@ -561,21 +588,52 @@ class @Notes
updateCloseButton: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
form.find('.js-note-target-close').text('Close')
closebtn = form.find('.js-note-target-close')
closebtn.text(closebtn.data('original-text'))
updateTargetButtons: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
reopenbtn = form.find('.js-note-target-reopen')
closebtn = form.find('.js-note-target-close')
discardbtn = form.find('.js-note-discard')
if textarea.val().trim().length > 0
form.find('.js-note-target-reopen').text('Comment & reopen')
form.find('.js-note-target-close').text('Comment & close')
form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
form.find('.js-note-target-close').addClass('btn-comment-and-close')
reopentext = reopenbtn.data('alternative-text')
closetext = closebtn.data('alternative-text')
if reopenbtn.text() isnt reopentext
reopenbtn.text(reopentext)
if closebtn.text() isnt closetext
closebtn.text(closetext)
if reopenbtn.is(':not(.btn-comment-and-reopen)')
reopenbtn.addClass('btn-comment-and-reopen')
if closebtn.is(':not(.btn-comment-and-close)')
closebtn.addClass('btn-comment-and-close')
if discardbtn.is(':hidden')
discardbtn.show()
else
form.find('.js-note-target-reopen').text('Reopen')
form.find('.js-note-target-close').text('Close')
form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
form.find('.js-note-target-close').removeClass('btn-comment-and-close')
reopentext = reopenbtn.data('original-text')
closetext = closebtn.data('original-text')
if reopenbtn.text() isnt reopentext
reopenbtn.text(reopentext)
if closebtn.text() isnt closetext
closebtn.text(closetext)
if reopenbtn.is(':not(.btn-comment-and-reopen)')
reopenbtn.removeClass('btn-comment-and-reopen')
if closebtn.is(':not(.btn-comment-and-close)')
closebtn.removeClass('btn-comment-and-close')
if discardbtn.is(':visible')
discardbtn.hide()
initTaskList: ->
@enableTaskList()
......
......@@ -4,12 +4,13 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit()
$('.update-username form').on 'ajax:before', ->
$('.loading-gif').show()
$('.update-username').on 'ajax:before', ->
$('.loading-username').show()
$(this).find('.update-success').hide()
$(this).find('.update-failed').hide()
$('.update-username form').on 'ajax:complete', ->
$('.update-username').on 'ajax:complete', ->
$('.loading-username').hide()
$(this).find('.btn-save').enable()
$(this).find('.loading-gif').hide()
......
......@@ -2,6 +2,7 @@
init: ->
$(".projects-list-filter").off('keyup')
this.initSearch()
this.initPagination()
initSearch: ->
@timer = null
......@@ -29,3 +30,8 @@
# Change url so if user reload a page - search results are saved
history.replaceState {page: project_filter_url}, document.title, project_filter_url
dataType: "json"
initPagination: ->
$('.projects-list-holder .pagination').on('ajax:success', (e, data) ->
$('.projects-list-holder').replaceWith(data.html)
)
......@@ -4,11 +4,15 @@ class @Shortcuts
Mousetrap.reset()
Mousetrap.bind('?', @selectiveHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL?
selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp)
toggleMarkdownPreview: (e) =>
$(document).triggerHandler('markdown-preview:toggle', [e])
@showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show')
......@@ -35,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove()
$('.hidden-shortcut').show()
e.preventDefault()
Mousetrap.stopCallback = (->
defaultStopCallback = Mousetrap.stopCallback
return (e, element, combo) ->
# allowed shortcuts if textarea, input, contenteditable are focused
if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1
return false
else
return defaultStopCallback.apply(@, arguments)
)()
......@@ -3,6 +3,81 @@ class @UsersSelect
@usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json"
$('.js-user-search').each (i, dropdown) =>
@projectId = $(dropdown).data('project-id')
@showCurrentUser = $(dropdown).data('current-user')
showNullUser = $(dropdown).data('null-user')
showAnyUser = $(dropdown).data('any-user')
firstUser = $(dropdown).data('first-user')
selectedId = $(dropdown).data('selected')
$(dropdown).glDropdown(
data: (term, callback) =>
@users term, (users) =>
if term.length is 0
showDivider = 0
if firstUser
# Move current user to the front of the list
for obj, index in users
if obj.username == firstUser
users.splice(index, 1)
users.unshift(obj)
break
if showNullUser
showDivider += 1
users.unshift(
name: 'Unassigned',
id: 0
)
if showAnyUser
showDivider += 1
name = showAnyUser
name = 'Any User' if name == true
anyUser = {
name: name,
id: null
}
users.unshift(anyUser)
if showDivider
users.splice(showDivider, 0, "divider")
# Send the data back
callback users
filterable: true
filterRemote: true
search:
fields: ['name', 'username']
selectable: true
fieldName: $(dropdown).data('field-name')
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
renderRow: (user) ->
username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false
selected = if user.id is selectedId then "is-active" else ""
img = ""
if avatar
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
"<li>
<a href='#' class='dropdown-menu-user-link #{selected}'>
#{img}
<strong class='dropdown-menu-user-full-name'>
#{user.name}
</strong>
<span class='dropdown-menu-user-username'>
#{username}
</span>
</a>
</li>"
)
$('.ajax-users-select').each (i, select) =>
@projectId = $(select).data('project-id')
@groupId = $(select).data('group-id')
......
......@@ -28,6 +28,10 @@
border-bottom: 1px solid $border-color;
color: $gl-gray;
a {
color: $md-link-color;
}
&.oneline-block {
line-height: 42px;
}
......@@ -153,3 +157,7 @@
float: right;
}
}
.content-block-small {
padding: 10px 0;
}
......@@ -12,11 +12,13 @@
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top:20px }
.prepend-left-10 { margin-left:10px }
.prepend-left-default { margin-left:$gl-padding }
.prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left:20px }
.append-right-5 { margin-right: 5px }
.append-right-10 { margin-right:10px }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right:20px }
.append-bottom-0 { margin-bottom:0 }
.append-bottom-10 { margin-bottom:10px }
.append-bottom-15 { margin-bottom:15px }
.append-bottom-20 { margin-bottom:20px }
......
......@@ -17,6 +17,47 @@
.dropdown-menu {
display: block;
}
.dropdown-menu-toggle {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
}
.dropdown-menu-toggle {
position: relative;
width: 160px;
padding: 6px 20px 6px 10px;
background-color: $dropdown-toggle-bg;
color: $dropdown-toggle-color;
font-size: 15px;
text-align: left;
border: 1px solid $dropdown-toggle-border-color;
border-radius: 2px;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.fa {
position: absolute;
top: 50%;
right: 6px;
margin-top: -4px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
}
&:hover, {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
}
.dropdown-menu {
......@@ -24,7 +65,7 @@
position: absolute;
top: 100%;
left: 0;
z-index: 9999;
z-index: 9;
width: 240px;
margin-top: 2px;
margin-bottom: 0;
......@@ -36,6 +77,21 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading {
.dropdown-content {
display: none;
}
.dropdown-loading {
display: block;
}
}
ul {
margin: 0;
padding: 0;
}
li {
text-align: left;
list-style: none;
......@@ -61,13 +117,70 @@
white-space: nowrap;
overflow: hidden;
&:hover {
&:hover,
&:focus,
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
outline: 0;
}
}
}
.dropdown-menu-paging {
.dropdown-page-two,
.dropdown-menu-back {
display: none;
}
&.is-page-two {
.dropdown-page-one {
display: none;
}
.dropdown-page-two,
.dropdown-menu-back {
display: block;
}
}
}
.dropdown-menu-user {
.avatar {
float: left;
width: 30px;
height: 30px;
margin: 0 10px 0 0;
}
}
.dropdown-menu-user-link {
padding-top: 7px;
padding-bottom: 7px;
}
.dropdown-menu-user-full-name {
display: block;
margin-bottom: 2px;
font-weight: 600;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-menu-user-username {
display: block;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-select {
width: 280px;
}
.dropdown-menu-align-right {
left: auto;
right: 0;
......@@ -81,8 +194,9 @@
&::before {
content: "\f00c";
position: absolute;
left: 4px;
top: 8px;
left: 5px;
top: 50%;
margin-top: -7px;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
......@@ -94,9 +208,136 @@
}
.dropdown-header {
padding-left: 10px;
padding-right: 10px;
padding-left: 5px;
padding-right: 5px;
color: $dropdown-header-color;
font-size: 13px;
line-height: 22px;
}
.dropdown-title {
position: relative;
margin-bottom: 10px;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 10px;
font-weight: 600;
line-height: 1;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
border-bottom: 1px solid $dropdown-divider-color;
overflow: hidden;
}
.dropdown-title-button {
position: absolute;
top: -1px;
padding: 0;
color: $dropdown-title-btn-color;
font-size: 14px;
border: 0;
background: none;
outline: 0;
&:hover {
color: darken($dropdown-title-btn-color, 15%);
}
}
.dropdown-menu-close {
right: 0;
}
.dropdown-menu-back {
left: 0;
}
.dropdown-input {
position: relative;
margin-bottom: 10px;
.fa {
position: absolute;
top: 10px;
right: 10px;
color: #C7C7C7;
font-size: 12px;
pointer-events: none;
}
}
.dropdown-input-field {
width: 100%;
padding: 0 7px;
color: $dropdown-input-color;
line-height: 30px;
border: 1px solid $dropdown-divider-color;
border-radius: 2px;
outline: 0;
&:focus {
color: $dropdown-link-color;
border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+ .fa {
color: $dropdown-link-color;
}
}
&:hover {
+ .fa {
color: $dropdown-link-color;
}
}
}
.dropdown-content {
max-height: 215px;
overflow-y: scroll;
}
.dropdown-footer {
padding-top: 10px;
margin-top: 10px;
font-size: 13px;
border-top: 1px solid $dropdown-divider-color;
}
.dropdown-footer-list {
font-size: 14px;
a {
padding-left: 10px;
}
}
.dropdown-loading {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
z-index: 9;
background-color: $dropdown-loading-bg;
font-size: 28px;
.fa {
position: absolute;
top: 50%;
left: 50%;
margin-top: -14px;
margin-left: -14px;
}
}
.dropdown-menu-labels {
.label {
position: relative;
width: 30px;
margin-right: 5px;
text-indent: -99999px;
}
}
......@@ -169,6 +169,7 @@
*/
&.code {
padding: 0;
-webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
}
}
}
......
.filter-item {
margin-right: 6px;
vertical-align: top;
}
@media (min-width: 800px) {
......
......@@ -63,7 +63,7 @@
border-bottom: none;
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) {
@media (max-width: $screen-sm-max) {
width: 100%;
}
}
......
......@@ -149,13 +149,13 @@
}
&:hover > a.anchor {
$size: 16px;
$size: 14px;
position: absolute;
right: 100%;
top: 50%;
margin-top: -$size/2;
margin-right: 0px;
padding-right: 20px;
margin-top: -11px;
margin-right: 0;
padding-right: 15px;
display: inline-block;
width: $size;
height: $size;
......
......@@ -34,13 +34,15 @@ $error-exclamation-point: #E62958;
$border-radius-default: 3px;
$list-title-color: #333333;
$list-text-color: #555555;
$profile-settings-link-color: $md-link-color;
$btn-transparent-color: #8F8F8F;
$ssh-key-icon-color: #8F8F8F;
$ssh-key-icon-size: 18px;
$provider-btn-group-border: #E5E5E5;
$provider-btn-not-active-color: #4688F1;
/*
* Color schema
*/
......@@ -70,7 +72,7 @@ $orange-light: rgba(252, 109, 38, 0.80);
$orange-normal: #E75E40;
$orange-dark: #CE5237;
$red-light: #F43263;
$red-light: #F06559;
$red-normal: #E52C5A;
$red-dark: #D22852;
......@@ -94,7 +96,7 @@ $border-orange-light: #fc6d26;
$border-orange-normal: #CE5237;
$border-orange-dark: #C14E35;
$border-red-light: #E52C5A;
$border-red-light: #F24F41;
$border-red-normal: #D22852;
$border-red-dark: #CA264F;
......@@ -138,3 +140,22 @@ $dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494;
$dropdown-caret-color: #54565B;
$dropdown-title-btn-color: #BFBFBF;
$dropdown-input-color: #C7C7C7;
$dropdown-input-focus-border: rgb(58, 171, 240);
$dropdown-input-focus-shadow: rgba(#000, .2);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff;
$dropdown-toggle-color: #626262;
$dropdown-toggle-border-color: #EAEAEA;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
$dropdown-toggle-icon-color: #C4C4C4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
/*
* Award emoji
*/
$award-emoji-menu-bg: #FFF;
$award-emoji-menu-border: #F1F2F4;
$award-emoji-new-btn-icon-color: #DCDCDC;
.awards {
@include clearfix;
line-height: 34px;
.emoji-icon {
width: 20px;
height: 20px;
margin: 7px 0 0 5px;
}
.award {
@include border-radius(5px);
border: 1px solid;
padding: 0px 10px;
float: left;
margin-right: 5px;
border-color: $border-color;
cursor: pointer;
&:hover {
background-color: #dce0e5;
}
&.active {
border-color: $border-gray-light;
background-color: $gray-light;
&:hover {
background-color: #dce0e5;
}
.counter {
font-weight: bold;
}
}
.icon {
float: left;
margin-right: 10px;
}
.counter {
float: left;
}
}
.awards-controls {
position: relative;
margin-left: 10px;
float: left;
.add-award {
font-size: 24px;
color: $gl-gray;
position: relative;
top: 2px;
&:hover,
&:link {
text-decoration: none;
}
}
}
.emoji-menu{
.emoji-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 3px;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0,0,0,.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
background-color: $award-emoji-menu-bg;
border: 1px solid $award-emoji-menu-border;
border-radius: $border-radius-base;
box-shadow: 0 6px 12px rgba(0,0,0,.175);
pointer-events: none;
opacity: 0;
transform: scale(.2);
transform-origin: 0 -45px;
transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
&.is-visible {
pointer-events: all;
opacity: 1;
transform: scale(1);
}
.emoji-menu-content {
padding: $gl-padding;
......@@ -90,36 +37,97 @@
height: 300px;
overflow-y: scroll;
h5 {
clear: left;
input.emoji-search{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right 5px center;
background-size: 16px;
}
ul {
list-style-type: none;
margin-left: -20px;
margin-bottom: 20px;
overflow: auto;
}
}
input.emoji-search{
background: image-url("icon-search.png") 240px no-repeat;
}
.emoji-menu-list {
list-style: none;
padding-left: 0;
margin-bottom: 0;
}
.emoji-menu-list-item {
padding: 3px;
margin-left: 1px;
margin-right: 1px;
}
li {
.emoji-menu-btn {
display: block;
cursor: pointer;
width: 30px;
height: 30px;
text-align: center;
float: left;
margin: 3px;
list-decorate: none;
@include border-radius(5px);
padding: 0;
background: none;
border: 0;
border-radius: $border-radius-base;
transition: transform .15s cubic-bezier(.3, 0, .2, 2);
&:hover {
background-color: #ccc;
background-color: transparent;
outline: 0;
transform: scale(1.3);
}
&:focus,
&:active {
outline: 0;
}
.emoji-icon {
display: inline-block;
position: relative;
top: 3px;
}
}
.award-menu-holder {
display: inline-block;
position: relative;
}
.award-control {
margin-right: 5px;
padding-left: 5px;
padding-right: 5px;
line-height: 20px;
outline: 0;
&.active,
&:active {
background-color: $white-dark;
box-shadow: none;
outline: 0;
}
&.is-loading {
.award-control-icon {
display: none;
}
.award-control-icon-loading {
display: block;
}
}
.icon,
.award-control-icon {
float: left;
margin-right: 5px;
font-size: 20px;
}
.award-control-icon-loading {
display: none;
}
.award-control-icon {
color: $award-emoji-new-btn-icon-color;
}
}
......@@ -27,10 +27,25 @@
}
.scroll-controls {
position: fixed;
bottom: 10px;
left: 250px;
z-index: 100;
&.affix-top {
position: absolute;
top: 10px;
right: 25px;
}
&.affix-bottom {
position: absolute;
right: 25px;
}
&.affix {
right: 30px;
bottom: 15px;
@media (min-width: $screen-md-min) {
right: 26%;
}
}
a {
display: block;
......
......@@ -90,6 +90,7 @@
position: relative;
font-family: $monospace_font;
$left: 12px;
overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
.max-width-marker {
width: 72ch;
color: rgba(0, 0, 0, 0.0);
......
......@@ -7,6 +7,28 @@
display: inline-block;
margin-right: 10px;
}
&.suggest-colors-dropdown {
margin-bottom: 5px;
a {
@include border-radius(0);
width: 36.7px;
margin-right: 0;
margin-bottom: -5px;
}
}
}
.dropdown-label-color-preview {
display: none;
margin-top: 5px;
width: 100%;
height: 25px;
&.is-active {
display: block;
}
}
.label-row {
......
......@@ -19,10 +19,11 @@ li.milestone {
width: 105px;
}
.issue-row {
.issuable-row {
.color-label {
border-radius: 2px;
padding: 3px !important;
margin-right: 7px;
}
// Issue title
......@@ -44,20 +45,15 @@ li.milestone {
}
}
.issues-sortable-list {
.issue-detail {
.issues-sortable-list, .merge_requests-sortable-list {
.issuable-detail {
display: block;
margin-top: 7px;
.issue-number{
.issuable-number {
color: rgba(0,0,0,0.44);
margin-right: 5px;
}
.color-label {
padding: 6px 10px;
margin-right: 7px;
margin-top: 10px;
}
.avatar {
float: none;
}
......
.account-page {
fieldset {
margin-bottom: 15px;
padding-bottom: 15px;
.profile-avatar-form-option {
hr {
margin: 10px 0;
}
}
......@@ -20,7 +19,7 @@
.account-btn-link,
.profile-settings-sidebar a {
color: $profile-settings-link-color;
color: $md-link-color;
}
.oauth-buttons {
......@@ -172,6 +171,47 @@
.profile-settings-content {
a {
color: $profile-settings-link-color;
color: $md-link-color;
}
}
.change-username-title {
color: $gl-warning;
}
.remove-account-title {
color: $gl-danger;
}
.provider-btn-group {
display: inline-block;
margin-right: 10px;
border: 1px solid $provider-btn-group-border;
border-radius: 3px;
&:last-child {
margin-right: 0;
}
}
.provider-btn-image {
display: inline-block;
padding: 5px 10px;
border-right: 1px solid $provider-btn-group-border;
> img {
width: 20px;
}
}
.provider-btn {
display: inline-block;
padding: 5px 10px;
margin-left: -3px;
line-height: 22px;
background-color: $gray-light;
&.not-active {
color: $provider-btn-not-active-color;
}
}
......@@ -26,5 +26,13 @@
margin-right: 10px;
font-size: $gl-font-size;
border: 1px solid;
line-height: 40px;
line-height: 32px;
}
.markdown-snippet-copy {
position: fixed;
top: -10px;
left: -10px;
max-height: 0;
max-width: 0;
}
......@@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController
private
def group
@group = Group.find_by(path: params[:id])
@group ||= Group.find_by(path: params[:id])
end
def group_params
......
......@@ -246,6 +246,8 @@ class ApplicationController < ActionController::Base
def ldap_security_check
if current_user && current_user.requires_ldap_check?
return unless current_user.try_obtain_ldap_lease
unless Gitlab::LDAP::Access.allowed?(current_user)
sign_out current_user
flash[:alert] = "Access denied for your LDAP account."
......
# == FilterProjects
#
# Controller concern to handle projects filtering
# * by name
# * by archived state
#
module FilterProjects
extend ActiveSupport::Concern
def filter_projects(projects)
projects = projects.search(params[:filter_projects]) if params[:filter_projects].present?
projects = projects.non_archived if params[:archived].blank?
projects
end
end
class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects
before_action :event_filter
def index
@projects = current_user.authorized_projects.sorted_by_activity.non_archived
@projects = @projects.sort(@sort = params[:sort])
@projects = current_user.authorized_projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE)
terms = params[:filter_projects]
if terms.present?
@projects = @projects.search(terms)
end
@projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank?
@last_push = current_user.recent_push
respond_to do |format|
......@@ -32,16 +29,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def starred
@projects = current_user.starred_projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE)
terms = params[:filter_projects]
if terms.present?
@projects = @projects.search(terms)
end
@projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank?
@last_push = current_user.recent_push
@groups = []
......
class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects
def index
@projects = ProjectsFinder.new.execute(current_user)
@tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.non_archived
@projects = @projects.search(params[:search]) if params[:search].present?
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
......@@ -22,9 +22,8 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending
@projects = TrendingProjectsFinder.new.execute(current_user)
@projects = @projects.non_archived
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@projects = filter_projects(@projects)
@projects = @projects.page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
......@@ -38,9 +37,9 @@ class Explore::ProjectsController < Explore::ApplicationController
def starred
@projects = ProjectsFinder.new.execute(current_user)
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@projects = @projects.page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
......
class GroupsController < Groups::ApplicationController
include FilterProjects
include IssuesAction
include MergeRequestsAction
......@@ -14,7 +15,7 @@ class GroupsController < Groups::ApplicationController
# Load group projects
before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
before_action :event_filter, only: [:show, :events]
before_action :event_filter, only: [:activity]
layout :determine_layout
......@@ -41,7 +42,8 @@ class GroupsController < Groups::ApplicationController
def show
@last_push = current_user.recent_push if current_user
@projects = @projects.includes(:namespace)
@projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
respond_to do |format|
......@@ -60,8 +62,10 @@ class GroupsController < Groups::ApplicationController
end
end
def events
def activity
respond_to do |format|
format.html
format.json do
load_events
pager_json("events/_events", @events.count)
......@@ -98,7 +102,7 @@ class GroupsController < Groups::ApplicationController
end
def load_projects
@projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity.non_archived
@projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity
end
# Dont allow unauthorized access to group
......
......@@ -7,6 +7,9 @@ class Projects::AvatarsController < Projects::ApplicationController
@blob = @repository.blob_at_branch('master', @project.avatar_in_git)
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
return if cached_blob?
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
headers['Content-Disposition'] = 'inline'
headers['Content-Type'] = safe_content_type(@blob)
......
......@@ -32,10 +32,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def show
@issues = @milestone.issues
@users = @milestone.participants.uniq
@merge_requests = @milestone.merge_requests
@labels = @milestone.labels
end
def create
......
......@@ -13,6 +13,8 @@ class Projects::RawController < Projects::ApplicationController
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
return if cached_blob?
if @blob.lfs_pointer?
send_lfs_object
else
......
class ProjectsController < ApplicationController
include ExtractsPath
prepend_before_action :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
......@@ -242,16 +241,6 @@ class ProjectsController < ApplicationController
end
end
def render_go_import
return unless params["go-get"] == "1"
@namespace = params[:namespace_id]
@id = params[:project_id] || params[:id]
@id = @id.gsub(/\.git\Z/, "")
render "go_import", layout: false
end
def repo_exists?
project.repository_exists? && !project.empty_repo?
end
......
......@@ -244,10 +244,17 @@ class IssuableFinder
items
end
def filter_by_upcoming_milestone?
params[:milestone_title] == '#upcoming'
end
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
items = items.where(milestone_id: [-1, nil])
elsif filter_by_upcoming_milestone?
upcoming = Milestone.where(project_id: projects).upcoming
items = items.joins(:milestone).where(milestones: { title: upcoming.title })
else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
......@@ -263,11 +270,9 @@ class IssuableFinder
def by_label(items)
if labels?
if filter_by_no_label?
items = items.
joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id").
where(label_links: { id: nil })
items = items.without_label
else
items = items.joins(:labels).where(labels: { title: label_names })
items = items.with_label(label_names)
if projects
items = items.where(labels: { project_id: projects })
......
......@@ -52,7 +52,10 @@ class ProjectsFinder
def all_projects(current_user)
if current_user
[current_user.authorized_projects, public_and_internal_projects]
[
current_user.authorized_projects,
public_and_internal_projects
]
else
[Project.public_only]
end
......
......@@ -4,7 +4,7 @@ class SnippetsFinder
case filter
when :all then
snippets(current_user).fresh.non_expired
snippets(current_user).fresh
when :by_user then
by_user(current_user, params[:user], params[:scope])
when :by_project
......@@ -27,7 +27,7 @@ class SnippetsFinder
end
def by_user(current_user, user, scope)
snippets = user.snippets.fresh.non_expired
snippets = user.snippets.fresh
return snippets.are_public unless current_user
......@@ -48,7 +48,7 @@ class SnippetsFinder
end
def by_project(current_user, project)
snippets = project.snippets.fresh.non_expired
snippets = project.snippets.fresh
if current_user
if project.team.member?(current_user.id)
......
......@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
user = User.find_by(email: user_or_email.downcase)
user = User.find_by_any_email(user_or_email.try(:downcase))
end
if user
......
......@@ -152,4 +152,25 @@ module BlobHelper
'application/octet-stream'
end
end
def cached_blob?
stale = stale?(etag: @blob.id) # The #stale? method sets cache headers.
# Because we are opionated we set the cache headers ourselves.
response.cache_control[:public] = @project.public?
if @ref && @commit && @ref == @commit.id
# This is a link to a commit by its commit SHA. That means that the blob
# is immutable. The only reason to invalidate the cache is if the commit
# was deleted or if the user lost access to the repository.
response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE
else
# A branch or tag points at this blob. That means that the expected blob
# value may change over time.
response.cache_control[:max_age] = Blob::CACHE_TIME
end
response.etag = @blob.id
!stale
end
end
......@@ -42,12 +42,12 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
def render_ci_status(ci_commit)
def render_ci_status(ci_commit, tooltip_placement: 'auto left')
link_to ci_status_icon(ci_commit),
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_status_label(ci_commit)}",
data: { toggle: 'tooltip', placement: 'left' }
data: { toggle: 'tooltip', placement: tooltip_placement }
end
def no_runners_for_project?(project)
......
......@@ -211,4 +211,15 @@ module CommitsHelper
def clean(string)
Sanitize.clean(string, remove_contents: true)
end
def limited_commits(commits)
if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
[
commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE),
commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE
]
else
[commits, 0]
end
end
end
module DropdownsHelper
def dropdown_tag(toggle_text, options: {}, &block)
content_tag :div, class: "dropdown" do
data_attr = { toggle: "dropdown" }
if options.has_key?(:data)
data_attr = options[:data].merge(data_attr)
end
dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
output = ""
if options.has_key?(:title)
output << dropdown_title(options[:title])
end
if options.has_key?(:filter)
output << dropdown_filter(options[:placeholder])
end
output << content_tag(:div, class: "dropdown-content") do
capture(&block) if block && !options.has_key?(:footer_content)
end
if block && options.has_key?(:footer_content)
output << content_tag(:div, class: "dropdown-footer") do
capture(&block)
end
end
output << dropdown_loading
output.html_safe
end
dropdown_output.html_safe
end
end
def dropdown_toggle(toggle_text, data_attr, options)
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
output << icon('chevron-down')
output.html_safe
end
end
def dropdown_title(title, back: false)
content_tag :div, class: "dropdown-title" do
title_output = ""
if back
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
icon('arrow-left')
end
end
title_output << content_tag(:span, title)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
icon('times')
end
title_output.html_safe
end
end
def dropdown_filter(placeholder)
content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
filter_output << icon('search')
filter_output.html_safe
end
end
def dropdown_content(&block)
content_tag(:div, class: "dropdown-content") do
if block
capture(&block)
end
end
end
def dropdown_footer(&block)
content_tag(:div, class: "dropdown-footer") do
if block
capture(&block)
end
end
end
def dropdown_loading
content_tag :div, class: "dropdown-loading" do
icon('spinner spin')
end
end
end
module ExploreHelper
def explore_projects_filter_path(options={})
def filter_projects_path(options={})
exist_opts = {
sort: params[:sort],
scope: params[:scope],
......@@ -9,15 +9,7 @@ module ExploreHelper
}
options = exist_opts.merge(options)
path = if explore_controller?
explore_projects_path
elsif current_action?(:starred)
starred_dashboard_projects_path
else
dashboard_projects_path
end
path = request.path
path << "?#{options.to_param}"
path
end
......
......@@ -9,6 +9,32 @@ module MilestonesHelper
end
end
def milestones_label_path(opts = {})
if @project
namespace_project_issues_path(@project.namespace, @project, opts)
elsif @group
issues_group_path(@group, opts)
else
issues_dashboard_path(opts)
end
end
def milestones_browse_issuables_path(milestone, type:)
opts = { milestone_title: milestone.title }
if @project
polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
elsif @group
polymorphic_url([type, @group], opts)
else
polymorphic_url([type, :dashboard], opts)
end
end
def milestone_issues_by_label_count(milestone, label, state:)
milestone.issues.with_label(label.title).send(state).size
end
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
......@@ -33,6 +59,7 @@ module MilestonesHelper
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any)
grouped_milestones.unshift(Milestone::Upcoming)
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end
......
......@@ -40,7 +40,7 @@ module SearchHelper
{ label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
{ label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
{ label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
{ label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Workflow Help", url: help_page_path("workflow", "README") },
]
end
......
module SnippetsHelper
def lifetime_select_options
options = [
['forever', nil],
['1 day', "#{Date.current + 1.day}"],
['1 week', "#{Date.current + 1.week}"],
['1 month', "#{Date.current + 1.month}"]
]
options_for_select(options)
end
def reliable_snippet_path(snippet)
if snippet.project_id?
namespace_project_snippet_path(snippet.project.namespace,
......
......@@ -16,6 +16,16 @@ module SortingHelper
}
end
def projects_sort_options_hash
{
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
}
end
def sort_title_oldest_updated
'Oldest updated'
end
......
......@@ -9,6 +9,7 @@ class Ability
when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject)
when Issue then issue_abilities(user, subject)
when ExternalIssue then external_issue_abilities(user, subject)
when Note then note_abilities(user, subject)
when ProjectSnippet then project_snippet_abilities(user, subject)
when PersonalSnippet then personal_snippet_abilities(user, subject)
......@@ -424,6 +425,10 @@ class Ability
end
end
def external_issue_abilities(user, subject)
project_abilities(user, subject.project)
end
private
def named_abilities(name)
......
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
#
# This method prevents the decorated object from evaluating to "truthy" when
......
......@@ -46,9 +46,23 @@ module Ci
acts_as_taggable
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# This method performs a *partial* match on tokens, thus a query for "a"
# will match any runner where the token contains the letter "a". As a result
# you should *not* use this method for non-admin purposes as otherwise users
# might be able to query a list of all runners.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def self.search(query)
where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query',
query: "%#{query.try(:downcase)}%")
t = arel_table
pattern = "%#{query}%"
where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end
def set_default_values
......
......@@ -29,12 +29,15 @@ module Issuable
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :opened, -> { with_state(:opened, :reopened) }
scope :only_opened, -> { with_state(:opened) }
scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) }
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) }
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
......@@ -58,12 +61,29 @@ module Issuable
end
module ClassMethods
# Searches for records with a matching title.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query)
where("LOWER(title) like :query", query: "%#{query.downcase}%")
where(arel_table[:title].matches("%#{query}%"))
end
# Searches for records with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def full_search(query)
where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%")
t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
def sort(method)
......
module Milestoneish
def closed_items_count
issues.closed.size + merge_requests.closed_and_merged.size
end
def total_items_count
issues.size + merge_requests.size
end
def complete?
total_items_count == closed_items_count
end
def percent_complete
((closed_items_count * 100) / total_items_count).abs
rescue ZeroDivisionError
0
end
def remaining_days
return 0 if !due_date || expired?
(due_date - Date.today).to_i
end
end
......@@ -2,16 +2,19 @@ class GlobalLabel
attr_accessor :title, :labels
alias_attribute :name, :title
delegate :color, :description, to: :@first_label
def self.build_collection(labels)
labels = labels.group_by(&:title)
labels.map do |title, label|
new(title, label)
labels.map do |title, labels|
new(title, labels)
end
end
def initialize(title, labels)
@title = title
@labels = labels
@first_label = labels.find { |lbl| lbl.description.present? } || labels.first
end
end
class GlobalMilestone
include Milestoneish
attr_accessor :title, :milestones
alias_attribute :name, :title
......@@ -28,33 +30,7 @@ class GlobalMilestone
end
def projects
milestones.map { |milestone| milestone.project }
end
def issue_count
milestones.map { |milestone| milestone.issues.count }.sum
end
def merge_requests_count
milestones.map { |milestone| milestone.merge_requests.count }.sum
end
def open_items_count
milestones.map { |milestone| milestone.open_items_count }.sum
end
def closed_items_count
milestones.map { |milestone| milestone.closed_items_count }.sum
end
def total_items_count
milestones.map { |milestone| milestone.total_items_count }.sum
end
def percent_complete
((closed_items_count * 100) / total_items_count).abs
rescue ZeroDivisionError
0
@projects ||= Project.for_milestones(milestones.map(&:id))
end
def state
......@@ -76,35 +52,20 @@ class GlobalMilestone
end
def issues
@issues ||= milestones.map(&:issues).flatten.group_by(&:state)
@issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
end
def merge_requests
@merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
@merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project)
end
def participants
@participants ||= milestones.map(&:participants).flatten.compact.uniq
end
def opened_issues
issues.values_at("opened", "reopened").compact.flatten
end
def closed_issues
issues['closed']
end
def opened_merge_requests
merge_requests.values_at("opened", "reopened").compact.flatten
end
def closed_merge_requests
merge_requests.values_at("closed", "merged", "locked").compact.flatten
end
def complete?
total_items_count == closed_items_count
def labels
@labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
.sort_by!(&:title)
end
def due_date
......
......@@ -33,8 +33,18 @@ class Group < Namespace
after_destroy :post_destroy_hook
class << self
# Searches for groups matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query)
where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%")
table = Namespace.arel_table
pattern = "%#{query}%"
where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
end
def sort(method)
......
......@@ -137,11 +137,8 @@ class MergeRequest < ActiveRecord::Base
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :opened, -> { with_states(:opened, :reopened) }
scope :merged, -> { with_state(:merged) }
scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :join_project, -> { joins(:target_project) }
......@@ -165,6 +162,24 @@ class MergeRequest < ActiveRecord::Base
super("merge_requests", /(?<merge_request>\d+)/)
end
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
# ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
# using multiple sub-queries especially when combined with an OR statement.
# UNIONs on the other hand perform much better in these cases.
#
# relation - An ActiveRecord::Relation that returns a list of Projects.
#
# Returns an ActiveRecord::Relation.
def self.in_projects(relation)
source = where(source_project_id: relation).select(:id)
target = where(target_project_id: relation).select(:id)
union = Gitlab::SQL::Union.new([source, target])
where("merge_requests.id IN (#{union.to_sql})")
end
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
......
......@@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base
include Sortable
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 500
COMMITS_SAFE_SIZE = 100
belongs_to :merge_request
......
......@@ -19,17 +19,19 @@ class Milestone < ActiveRecord::Base
MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include InternalId
include Sortable
include Referable
include StripAttribute
include Milestoneish
belongs_to :project
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :participants, through: :issues, source: :assignee
has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
......@@ -57,9 +59,18 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title
class << self
# Searches for milestones matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query)
query = "%#{query}%"
where("title like ? or description like ?", query, query)
t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
end
......@@ -71,6 +82,10 @@ class Milestone < ActiveRecord::Base
super("milestones", /(?<milestone>\d+)/)
end
def self.upcoming
self.where('due_date > ?', Time.now).order(due_date: :asc).first
end
def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]")
......@@ -92,30 +107,6 @@ class Milestone < ActiveRecord::Base
end
end
def open_items_count
self.issues.opened.count + self.merge_requests.opened.count
end
def closed_items_count
self.issues.closed.count + self.merge_requests.closed_and_merged.count
end
def total_items_count
self.issues.count + self.merge_requests.count
end
def percent_complete
((closed_items_count * 100) / total_items_count).abs
rescue ZeroDivisionError
0
end
def remaining_days
return 0 if !due_date || expired?
(due_date - Date.today).to_i
end
def expires_at
if due_date
if due_date.past?
......
......@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end
# Searches for namespaces matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation
def search(query)
where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
t = arel_table
pattern = "%#{query}%"
where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
end
def clean_path(path)
......
......@@ -44,6 +44,7 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
before_validation :set_award!
before_validation :clear_blank_line_code!
validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
......@@ -63,7 +64,7 @@ class Note < ActiveRecord::Base
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) }
scope :not_inline, ->{ where(line_code: nil) }
scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) }
......@@ -105,8 +106,18 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end
# Searches for notes matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search(query)
where("LOWER(note) like :query", query: "%#{query.downcase}%")
table = arel_table
pattern = "%#{query}%"
where(table[:note].matches(pattern))
end
def grouped_awards
......@@ -365,6 +376,10 @@ class Note < ActiveRecord::Base
private
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
end
def awards_supported?
(for_issue? || for_merge_request?) && !for_diff_line?
end
......
......@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
......
......@@ -151,6 +151,7 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects
has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
......@@ -215,6 +216,7 @@ class Project < ActiveRecord::Base
scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) }
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
state_machine :import_status, initial: :none do
event :import_start do
......@@ -264,13 +266,31 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end
# Searches for a list of projects based on the query given in `query`.
#
# On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
# search. On MySQL a regular "LIKE" is used as it's already
# case-insensitive.
#
# query - The search query as a String.
def search(query)
ptable = arel_table
ntable = Namespace.arel_table
pattern = "%#{query}%"
projects = select(:id).where(
ptable[:path].matches(pattern).
or(ptable[:name].matches(pattern)).
or(ptable[:description].matches(pattern))
)
namespaces = select(:id).
joins(:namespace).
where('LOWER(projects.name) LIKE :query OR
LOWER(projects.path) LIKE :query OR
LOWER(namespaces.name) LIKE :query OR
LOWER(projects.description) LIKE :query',
query: "%#{query.try(:downcase)}%")
where(ntable[:name].matches(pattern))
union = Gitlab::SQL::Union.new([projects, namespaces])
where("projects.id IN (#{union.to_sql})")
end
def search_by_visibility(level)
......@@ -278,7 +298,10 @@ class Project < ActiveRecord::Base
end
def search_by_title(query)
non_archived.where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%")
pattern = "%#{query}%"
table = Project.arel_table
non_archived.where(table[:name].matches(pattern))
end
def find_with_namespace(id)
......@@ -526,11 +549,11 @@ class Project < ActiveRecord::Base
end
def ci_services
services.select { |service| service.category == :ci }
services.where(category: :ci)
end
def ci_service
@ci_service ||= ci_services.find(&:activated?)
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
def jira_tracker?
......@@ -907,13 +930,13 @@ class Project < ActiveRecord::Base
end
def valid_runners_token? token
self.runners_token && self.runners_token == token
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
# TODO (ayufan): For now we use runners_token (backward compatibility)
# In 8.4 every build will have its own individual token valid for time of build
def valid_build_token? token
self.builds_enabled? && self.runners_token && self.runners_token == token
self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
def build_coverage_enabled?
......
......@@ -26,7 +26,7 @@ class CiService < Service
default_value_for :category, 'ci'
def valid_token?(token)
self.respond_to?(:token) && self.token.present? && self.token == token
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def supported_events
......
......@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
......@@ -23,6 +22,4 @@ class ProjectSnippet < Snippet
# Scopes
scope :fresh, -> { order("created_at DESC") }
scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
end
......@@ -133,18 +133,18 @@ class Repository
rugged.branches.create(branch_name, target)
end
expire_branches_cache
after_create_branch
find_branch(branch_name)
end
def add_tag(tag_name, ref, message = nil)
expire_tags_cache
before_push_tag
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end
def rm_branch(user, branch_name)
expire_branches_cache
before_remove_branch
branch = find_branch(branch_name)
oldrev = branch.try(:target)
......@@ -155,12 +155,12 @@ class Repository
rugged.branches.delete(branch_name)
end
expire_branches_cache
after_remove_branch
true
end
def rm_tag(tag_name)
expire_tags_cache
before_remove_tag
gitlab_shell.rm_tag(path_with_namespace, tag_name)
end
......@@ -183,6 +183,14 @@ class Repository
end
end
def branch_count
@branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count }
end
def tag_count
@tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
end
# Return repo size in megabytes
# Cached in redis
def size
......@@ -278,6 +286,16 @@ class Repository
@has_visible_content = nil
end
def expire_branch_count_cache
cache.expire(:branch_count)
@branch_count = nil
end
def expire_tag_count_cache
cache.expire(:tag_count)
@tag_count = nil
end
def rebuild_cache
cache_keys.each do |key|
cache.expire(key)
......@@ -313,9 +331,17 @@ class Repository
expire_root_ref_cache
end
# Runs code before creating a new tag.
def before_create_tag
# Runs code before pushing (= creating or removing) a tag.
def before_push_tag
expire_cache
expire_tags_cache
expire_tag_count_cache
end
# Runs code before removing a tag.
def before_remove_tag
expire_tags_cache
expire_tag_count_cache
end
# Runs code after a repository has been forked/imported.
......@@ -330,12 +356,21 @@ class Repository
# Runs code after a new branch has been created.
def after_create_branch
expire_branches_cache
expire_has_visible_content_cache
expire_branch_count_cache
end
# Runs code before removing an existing branch.
def before_remove_branch
expire_branches_cache
end
# Runs code after an existing branch has been removed.
def after_remove_branch
expire_has_visible_content_cache
expire_branch_count_cache
expire_branches_cache
end
def method_missing(m, *args, &block)
......@@ -812,6 +847,12 @@ class Repository
raw_repository.ls_files(actual_ref)
end
def main_language
unless empty?
Linguist::Repository.new(rugged, rugged.head.target_id).language
end
end
private
def cache
......
......@@ -10,7 +10,6 @@
# created_at :datetime
# updated_at :datetime
# file_name :string(255)
# expires_at :datetime
# type :string(255)
# visibility_level :integer default(0), not null
#
......@@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") }
scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
participant :author, :notes
......@@ -111,21 +108,37 @@ class Snippet < ActiveRecord::Base
nil
end
def expired?
expires_at && expires_at < Time.current
end
def visibility_level_field
visibility_level
end
class << self
# Searches for snippets with a matching title or file name.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search(query)
where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%")
t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
end
# Searches for snippets with matching content.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search_code(query)
where('(content LIKE :query)', query: "%#{query}%")
table = Snippet.arel_table
pattern = "%#{query}%"
where(table[:content].matches(pattern))
end
def accessible_to(user)
......
......@@ -286,8 +286,22 @@ class User < ActiveRecord::Base
end
end
# Searches users matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query)
where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%")
table = arel_table
pattern = "%#{query}%"
where(
table[:name].matches(pattern).
or(table[:email].matches(pattern)).
or(table[:username].matches(pattern))
)
end
def by_login(login)
......@@ -612,6 +626,13 @@ class User < ActiveRecord::Base
end
end
def try_obtain_ldap_lease
# After obtaining this lease LDAP checks will be blocked for 600 seconds
# (10 minutes) for this user.
lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600)
lease.try_obtain
end
def solo_owned_groups
@solo_owned_groups ||= owned_groups.select do |group|
group.owners == [self]
......
......@@ -12,8 +12,9 @@ class GitPushService < BaseService
# 1. Creates the push event
# 2. Updates merge requests
# 3. Recognizes cross-references from commit messages
# 4. Executes the project's web hooks
# 4. Executes the project's webhooks
# 5. Executes the project's services
# 6. Checks if the project's main language has changed
#
def execute
@project.repository.after_push_commit(branch_name)
......@@ -42,11 +43,24 @@ class GitPushService < BaseService
@push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev])
process_commit_messages
end
# Checks if the main language has changed in the project and if so
# it updates it accordingly
update_main_language
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
update_merge_requests
end
def update_main_language
current_language = @project.repository.main_language
unless current_language == @project.main_language
return @project.update_attributes(main_language: current_language)
end
true
end
protected
def update_merge_requests
......@@ -96,9 +110,11 @@ class GitPushService < BaseService
# a different branch.
closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
end
end
end
commit.create_cross_references!(authors[commit], closed_issues)
end
......
......@@ -2,7 +2,7 @@ class GitTagPushService
attr_accessor :project, :user, :push_data
def execute(project, user, oldrev, newrev, ref)
project.repository.before_create_tag
project.repository.before_push_tag
@project, @user = project, user
@push_data = build_push_data(oldrev, newrev, ref)
......
......@@ -21,9 +21,11 @@ module MergeRequests
closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
end
end
end
def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user)
......
......@@ -11,13 +11,21 @@ module Projects
end
def project_tree
%i(issues merge_requests labels milestones snippets releases events commit_statuses) + members
%i(issues labels milestones snippets releases events) + [members, merge_requests, commit_statuses]
end
private
def merge_requests
{ merge_requests: { include: :merge_request_diff } }
end
def commit_statuses
{ commit_statuses: { include: :commit } }
end
def members
[{ project_members: { include: [user: { only: [:id, :email, :username] }] } }]
{ project_members: { include: [user: { only: [:id, :email, :username] }] } }
end
def storage_path
......
......@@ -10,9 +10,8 @@ module Search
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group
project_ids = projects.pluck(:id)
Gitlab::SearchResults.new(project_ids, params[:search])
Gitlab::SearchResults.new(projects, params[:search])
end
end
end
......@@ -7,7 +7,7 @@ module Search
end
def execute
Gitlab::ProjectSearchResults.new(project.id,
Gitlab::ProjectSearchResults.new(project,
params[:search],
params[:repository_ref])
end
......
......@@ -7,8 +7,9 @@ module Search
end
def execute
snippet_ids = Snippet.accessible_to(current_user).pluck(:id)
Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
snippets = Snippet.accessible_to(current_user)
Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
end
end
......@@ -15,7 +15,7 @@
.nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
= render 'explore/projects/dropdown'
= render 'shared/projects/dropdown'
- if current_user.can_create_project?
= link_to new_project_path, class: 'btn btn-new' do
= icon('plus')
......
%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
%span.milestone-row
- project = issue.project
%strong #{project.name_with_namespace} &middot;
= link_to [project.namespace.becomes(Namespace), project, issue] do
%span.cgray ##{issue.iid}
= link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
.pull-right.assignee-icon
- if issue.assignee
= image_tag avatar_icon(issue.assignee, 16), class: "avatar s16"
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list issues-sortable-list" }
- if issues
- issues.each do |issue|
= render 'issue', issue: issue
%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid }
%span.milestone-row
- project = merge_request.project
%strong #{project.name_with_namespace} &middot;
= link_to [project.namespace.becomes(Namespace), project, merge_request] do
%span.cgray ##{merge_request.iid}
= link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
= image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16"
.panel.panel-default
.panel-heading= title
%ul{ class: "well-list merge_requests-sortable-list" }
- if merge_requests
- merge_requests.each do |merge_request|
= render 'merge_request', merge_request: merge_request
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
.row
.col-sm-6
%strong
= link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title)
.col-sm-6
.pull-right.light #{milestone.percent_complete}% complete
.row
.col-sm-6
= link_to issues_dashboard_path(milestone_title: milestone.title) do
= pluralize milestone.issue_count, 'Issue'
&middot;
= link_to merge_requests_dashboard_path(milestone_title: milestone.title) do
= pluralize milestone.merge_requests_count, 'Merge Request'
.col-sm-6
= milestone_progress_bar(milestone)
.row
.col-sm-6
.expiration
= render 'shared/milestone_expired', milestone: milestone
.projects
- milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
%span.label.label-gray
= milestone.project.name_with_namespace
= render 'shared/milestones/milestone',
milestone_path: dashboard_milestone_path(milestone.safe_title, title: milestone.title),
issues_path: issues_dashboard_path(milestone_title: milestone.title),
merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
milestone: milestone,
dashboard: true
- page_title @milestone.title, "Milestones"
- header_title "Milestones", dashboard_milestones_path
.detail-page-header
.status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
- if @milestone.closed?
Closed
- else
Open
%span.identifier
Milestone #{@milestone.title}
.detail-page-description.gray-content-block.second-block
%h2.title
= markdown escape_once(@milestone.title), pipeline: :single_line
- if @milestone.complete? && @milestone.active?
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. Navigate to the project to close the milestone.
.table-holder
%table.table
%thead
%tr
%th Project
%th Open issues
%th State
%th Due date
- @milestone.milestones.each do |milestone|
%tr
%td
= link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
%td
= milestone.issues.opened.count
%td
- if milestone.closed?
Closed
- else
Open
%td
= milestone.expires_at
.context
%p.lead
Progress:
#{@milestone.closed_items_count} closed
&ndash;
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
%span.badge= @milestone.issue_count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
%span.badge= @milestone.merge_requests_count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
%span.badge= @milestone.participants.count
.tab-content
.tab-pane.active#tab-issues
.gray-content-block.middle-block
.pull-right
= link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
.oneline
All issues in this milestone
.row.prepend-top-default
.col-md-6
= render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6
= render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests
.gray-content-block.middle-block
.pull-right
= link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
.oneline
All merge requests in this milestone
.row.prepend-top-default
.col-md-6
= render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6
= render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants
.gray-content-block.middle-block
.oneline
All participants to this milestone
%ul.bordered-list
- @milestone.participants.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
%strong= truncate(user.name, lenght: 40)
%br
%small.cgray= user.username
= render 'shared/milestones/top', milestone: @milestone
= render 'shared/milestones/summary', milestone: @milestone
= render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true
......@@ -4,7 +4,10 @@
.todo-title
%span.author-name
= link_to_author todo
- if todo.author
= link_to_author(todo)
- else
(removed)
%span.todo-label
= todo_action_name(todo)
= todo_target_link(todo)
......
......@@ -2,8 +2,10 @@
.emoji-menu-content
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- AwardEmoji.emoji_by_category.each do |category, emojis|
%h5= AwardEmoji::CATEGORIES[category]
%ul
%h5.emoji-menu-title
= AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji|
%li
%li.pull-left.text-center.emoji-menu-list-item
%button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"}
= emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
.dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_updated
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_projects_filter_path(sort: sort_value_name) do
= sort_title_name
= link_to explore_projects_filter_path(sort: sort_value_recently_created) do
= sort_title_recently_created
= link_to explore_projects_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
= link_to explore_projects_filter_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated
= link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
......@@ -10,11 +10,11 @@
%b.caret
%ul.dropdown-menu
%li
= link_to explore_projects_filter_path(visibility_level: nil) do
= link_to filter_projects_path(visibility_level: nil) do
Any
- Gitlab::VisibilityLevel.values.each do |level|
%li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
= link_to explore_projects_filter_path(visibility_level: level) do
= link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level)
= visibility_level_label(level)
......@@ -30,11 +30,11 @@
%b.caret
%ul.dropdown-menu
%li
= link_to explore_projects_filter_path(tag: nil) do
= link_to filter_projects_path(tag: nil) do
Any
- @tags.each do |tag|
%li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
= link_to explore_projects_filter_path(tag: tag.name) do
%i.fa.fa-tag
= link_to filter_projects_path(tag: tag.name) do
= icon('tag')
= tag.name
.hidden-xs
= render "events/event_last_push", event: @last_push
.nav-block
- if current_user
.controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
%i.fa.fa-rss
= render 'shared/event_filter'
.content_list
= spinner
......@@ -3,6 +3,7 @@
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- if @projects.present?
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
= render 'shared/projects/dropdown'
- if can? current_user, :create_projects, @group
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
= icon('plus')
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment