mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-17 00:26:07 +10:00
Merge branches 'feature/project-export' and 'master' of gitlab.com:gitlab-org/gitlab-ce into feature/project-export
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
.sass-cache/
|
||||
.secret
|
||||
.vagrant
|
||||
.byebug_history
|
||||
Vagrantfile
|
||||
backups/*
|
||||
config/aws.yml
|
||||
|
||||
+5
-28
@@ -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
|
||||
@@ -330,4 +307,4 @@ notify:slack:
|
||||
- master@gitlab-org/gitlab-ce
|
||||
- tags@gitlab-org/gitlab-ce
|
||||
- master@gitlab-org/gitlab-ee
|
||||
- tags@gitlab-org/gitlab-ee
|
||||
- tags@gitlab-org/gitlab-ee
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
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'
|
||||
|
||||
+13
-7
@@ -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')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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()
|
||||
$("#emoji_search").focus()
|
||||
else
|
||||
$.get "/emojis", (response) ->
|
||||
$(".add-award").after response
|
||||
$(".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
|
||||
$('.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>")
|
||||
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(".awards-controls").find(".emoji-icon").data("emoji", emoji)
|
||||
|
||||
$(".award").tooltip()
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
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
|
||||
@@ -0,0 +1,11 @@
|
||||
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")
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
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')
|
||||
@@ -112,7 +112,8 @@ class @Milestone
|
||||
$(previousTabClass).hide()
|
||||
$(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,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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,22 +362,26 @@ 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
|
||||
GitLab.GfmAutoComplete.setup()
|
||||
new DropzoneInput(form)
|
||||
if isNewForm
|
||||
GitLab.GfmAutoComplete.setup()
|
||||
new DropzoneInput(form)
|
||||
|
||||
form.show()
|
||||
textarea = form.find("textarea")
|
||||
textarea.focus()
|
||||
autosize(textarea)
|
||||
|
||||
if isNewForm
|
||||
autosize(textarea)
|
||||
|
||||
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
|
||||
# The textarea has the correct value, Chrome just won't show it unless we
|
||||
@@ -371,7 +390,8 @@ class @Notes
|
||||
textarea.val ""
|
||||
textarea.val value
|
||||
|
||||
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
|
||||
if isNewForm
|
||||
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
|
||||
|
||||
###
|
||||
Called in response to clicking the edit note link
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.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;
|
||||
|
||||
@@ -1,125 +1,133 @@
|
||||
.awards {
|
||||
@include clearfix;
|
||||
line-height: 34px;
|
||||
|
||||
.emoji-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 7px 0 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 3px;
|
||||
z-index: 1000;
|
||||
min-width: 160px;
|
||||
font-size: 14px;
|
||||
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);
|
||||
}
|
||||
|
||||
.award {
|
||||
@include border-radius(5px);
|
||||
.emoji-menu-content {
|
||||
padding: $gl-padding;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
overflow-y: scroll;
|
||||
|
||||
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{
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
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);
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,.175);
|
||||
|
||||
.emoji-menu-content {
|
||||
padding: $gl-padding;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
overflow-y: scroll;
|
||||
|
||||
h5 {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
float: left;
|
||||
margin: 3px;
|
||||
list-decorate: none;
|
||||
@include border-radius(5px);
|
||||
|
||||
&:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-menu-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.emoji-menu-list-item {
|
||||
padding: 3px;
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
.emoji-menu-btn {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-radius: $border-radius-base;
|
||||
transition: transform .15s cubic-bezier(.3, 0, .2, 2);
|
||||
|
||||
&:hover {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.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."
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# == 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
|
||||
@@ -1,18 +1,15 @@
|
||||
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 = []
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
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
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# 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
|
||||
|
||||
+17
-3
@@ -23,7 +23,7 @@ module Ci
|
||||
|
||||
LAST_CONTACT_TIME = 5.minutes.ago
|
||||
AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online']
|
||||
|
||||
|
||||
has_many :builds, class_name: 'Ci::Build'
|
||||
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
|
||||
has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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
|
||||
|
||||
+11
-1
@@ -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)
|
||||
|
||||
@@ -135,11 +135,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) }
|
||||
@@ -163,6 +160,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
|
||||
|
||||
|
||||
+18
-27
@@ -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?
|
||||
|
||||
+11
-1
@@ -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)
|
||||
|
||||
+17
-2
@@ -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
|
||||
#
|
||||
|
||||
+34
-11
@@ -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)
|
||||
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)}%")
|
||||
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(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
|
||||
|
||||
+22
-9
@@ -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)
|
||||
|
||||
+22
-1
@@ -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,7 +110,9 @@ class GitPushService < BaseService
|
||||
# a different branch.
|
||||
closed_issues = commit.closes_issues(current_user)
|
||||
closed_issues.each do |issue|
|
||||
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
|
||||
if can?(current_user, :update_issue, issue)
|
||||
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
|
||||
end
|
||||
end
|
||||
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,7 +21,9 @@ module MergeRequests
|
||||
|
||||
closed_issues = merge_request.closes_issues(current_user)
|
||||
closed_issues.each do |issue|
|
||||
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
|
||||
if can?(current_user, :update_issue, issue)
|
||||
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid }
|
||||
%span.milestone-row
|
||||
- project = issue.project
|
||||
%strong #{project.name_with_namespace} ·
|
||||
= 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"
|
||||
@@ -1,6 +0,0 @@
|
||||
.panel.panel-default
|
||||
.panel-heading= title
|
||||
%ul{ class: "well-list issues-sortable-list" }
|
||||
- if issues
|
||||
- issues.each do |issue|
|
||||
= render 'issue', issue: issue
|
||||
@@ -1,10 +0,0 @@
|
||||
%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} ·
|
||||
= 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"
|
||||
@@ -1,6 +0,0 @@
|
||||
.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
|
||||
@@ -1,25 +1,6 @@
|
||||
%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'
|
||||
·
|
||||
= 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
|
||||
|
||||
@@ -1,105 +1,5 @@
|
||||
- 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
|
||||
–
|
||||
#{@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
|
||||
= emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
|
||||
%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"])
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
.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
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.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,9 +3,10 @@
|
||||
= 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
|
||||
- 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')
|
||||
New Project
|
||||
= 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')
|
||||
New Project
|
||||
|
||||
= render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
= content_for :meta_tags do
|
||||
- if current_user
|
||||
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
|
||||
|
||||
- page_title "Activity"
|
||||
- header_title group_title(@group, "Activity", activity_group_path(@group))
|
||||
|
||||
%section.activities
|
||||
= render 'activities'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user