From 36fdbc6edb1c0884f68c1267902b33a78d7be6ae Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 26 Apr 2016 13:21:48 +0100 Subject: [PATCH 01/40] Updated contrib map away from cal heat map --- app/assets/javascripts/calendar.js.coffee | 106 ++++++++++++++---- .../stylesheets/framework/calendar.scss | 30 +++++ app/views/users/calendar.html.haml | 2 - 3 files changed, 114 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index d80e0e716c..ec153f61c2 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -1,27 +1,75 @@ class @Calendar constructor: (timestamps, starting_year, starting_month, calendar_activities_path) -> - cal = new CalHeatMap() - cal.init - itemName: ["contribution"] - data: timestamps - start: new Date(starting_year, starting_month) - domainLabelFormat: "%b" - id: "cal-heatmap" - domain: "month" - subDomain: "day" - range: 12 - tooltip: true - label: - position: "top" - legend: [ - 0 - 10 - 20 - 30 - ] - legendCellPadding: 3 - cellSize: $('.user-calendar').width() / 73 - onClick: (date, count) -> + # Get the highest value from the timestampes + highestValue = 0 + _.each timestamps, (count) -> + if count > highestValue + highestValue = count + + timestamps = _.chain(timestamps) + .map (stamp, key) -> + { + count: stamp + date: key + } + .groupBy (stamp, i) -> + Math.floor i / 7 + .toArray() + .value() + + monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + months = [] + svg = d3.select '#cal-heatmap' + .append 'svg' + .attr 'width', 53 * 17 + .attr 'height', 140 + + # Setup each day box + svg.selectAll 'g' + .data timestamps + .enter() + .append 'g' + .attr 'transform', (group, i) -> + _.each group, (stamp) -> + month = new Date(parseInt(stamp.date) * 1000).getMonth() + x = 17 * i + 1 + lastMonth = _.last(months) + + # If undefined, push + if !lastMonth? + months.push + month: month + x: x + else if lastMonth.x is x + lastMonth.month = month + else if lastMonth.month isnt month + months.push + month: month + x: x + "translate(#{17 * i + 1}, 18)" + .selectAll 'rect' + .data (stamp) -> + stamp + .enter() + .append 'rect' + .attr 'x', '0' + .attr 'y', (stamp, i) -> + 17 * i + .attr 'width', 15 + .attr 'height', 15 + .attr 'title', (stamp) -> + "#{stamp.count} contributions
#{gl.utils.formatDate parseInt(stamp.date) * 1000}" + .attr 'class', (stamp) -> + extraClass = '' + if stamp.count isnt 0 + diff = stamp.count / highestValue + classNumber = Math.floor (diff / 0.25) + 1 + extraClass += "user-contrib-cell-#{classNumber}" + + "user-contrib-cell #{extraClass} js-tooltip" + .attr 'data-container', 'body' + .on 'click', (stamp) -> + date = new Date(parseInt(stamp.date) * 1000) formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() $.ajax url: calendar_activities_path @@ -32,3 +80,17 @@ class @Calendar success: (data) -> $(".user-calendar-activities").html data + svg.append 'g' + .selectAll 'text' + .data months + .enter() + .append 'text' + .attr 'x', (date) -> + date.x + .attr 'y', 10 + .attr 'class', 'user-contrib-text' + .text (date) -> + monthNames[date.month] + + $('#cal-heatmap .js-tooltip').tooltip + html: true diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 11f39d583b..da03454d80 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -68,3 +68,33 @@ font-weight: 550; } } + +.user-contrib-cell { + fill: #ededed; + cursor: pointer; + + &:hover { + stroke: #000; + } +} + +.user-contrib-cell-1 { + fill: #acd5f2; +} + +.user-contrib-cell-3 { + fill: #7fa8d1; +} + +.user-contrib-cell-4 { + fill: #49729b; +} + +.user-contrib-cell-5 { + fill: #254e77; +} + +.user-contrib-text { + font-size: 12px; + fill: #959494; +} diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 1de71f37d1..2abee8be2a 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -6,5 +6,3 @@ #{@starting_month}, '#{user_calendar_activities_path}' ); - -.calendar-hint Summary of issues, merge requests, and push events From 18b361cf809251dd633e31d3dd6d877b31d8db7f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 27 Apr 2016 11:07:46 +0100 Subject: [PATCH 02/40] Group the contributing calendar by day This aligns the boxes correctly with the day on the left side of the calendar --- app/assets/javascripts/application.js.coffee | 1 - app/assets/javascripts/calendar.js.coffee | 147 +++++++++++++----- app/assets/stylesheets/application.scss | 1 - .../stylesheets/framework/calendar.scss | 69 ++------ app/assets/stylesheets/pages/profile.scss | 6 - app/controllers/users_controller.rb | 2 - app/views/users/calendar.html.haml | 17 +- app/views/users/show.html.haml | 7 +- lib/gitlab/contributions_calendar.rb | 2 +- 9 files changed, 132 insertions(+), 120 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index bffce5a0c0..ff69e27bbc 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -19,7 +19,6 @@ #= require jquery.scrollTo #= require jquery.turbolinks #= require d3 -#= require cal-heatmap #= require turbolinks #= require autosave #= require bootstrap/affix diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index ec153f61c2..9c4d529f7e 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -1,52 +1,83 @@ class @Calendar - constructor: (timestamps, starting_year, starting_month, calendar_activities_path) -> + constructor: (timestamps, calendar_activities_path) -> # Get the highest value from the timestampes highestValue = 0 _.each timestamps, (count) -> if count > highestValue highestValue = count - timestamps = _.chain(timestamps) - .map (stamp, key) -> - { - count: stamp - date: key - } - .groupBy (stamp, i) -> - Math.floor i / 7 - .toArray() - .value() + # Loop through the timestamps to create a group of objects + # The group of objects will be grouped based on the day of the week they are + timestampsTmp = [] + i = 0 + group = 0 + _.each timestamps, (count, date) -> + newDate = new Date parseInt(date) * 1000 + day = newDate.getDay() + + # Create a new group array if this is the first day of the week + # or if is first object + if (day is 0 and i isnt 0) or i is 0 + timestampsTmp.push [] + group++ + + innerArray = timestampsTmp[group-1] + + # Push to the inner array the values that will be used to render map + innerArray.push + count: count + date: newDate + day: day + + i++ + + # Color function for chart + color = d3 + .scale + .linear() + .range(['#acd5f2', '#254e77']) + .domain([0, highestValue]) + + # Color function for key + colorKey = d3 + .scale + .linear() + .range(['#acd5f2', '#254e77']) + .domain([0, 3]) + keyColors = ['#ededed', colorKey(0), colorKey(1), colorKey(2), colorKey(3)] monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] months = [] - svg = d3.select '#cal-heatmap' + svg = d3.select '.js-contrib-calendar' .append 'svg' - .attr 'width', 53 * 17 - .attr 'height', 140 + .attr 'width', 54 * 17 + .attr 'height', 167 + .attr 'class', 'contrib-calendar' # Setup each day box svg.selectAll 'g' - .data timestamps + .data timestampsTmp .enter() .append 'g' .attr 'transform', (group, i) -> - _.each group, (stamp) -> - month = new Date(parseInt(stamp.date) * 1000).getMonth() - x = 17 * i + 1 - lastMonth = _.last(months) + _.each group, (stamp, a) -> + if a is 0 and stamp.day is 0 + month = stamp.date.getMonth() + x = (17 * i + 1) + 17 + lastMonth = _.last(months) + if lastMonth? + lastMonthX = lastMonth.x - # If undefined, push - if !lastMonth? - months.push - month: month - x: x - else if lastMonth.x is x - lastMonth.month = month - else if lastMonth.month isnt month + if !lastMonth? months.push month: month x: x - "translate(#{17 * i + 1}, 18)" + else if month isnt lastMonth.month and x - 17 isnt lastMonthX + months.push + month: month + x: x + + "translate(#{(17 * i + 1) + 17}, 18)" .selectAll 'rect' .data (stamp) -> stamp @@ -54,22 +85,20 @@ class @Calendar .append 'rect' .attr 'x', '0' .attr 'y', (stamp, i) -> - 17 * i + (17 * stamp.day) .attr 'width', 15 .attr 'height', 15 .attr 'title', (stamp) -> - "#{stamp.count} contributions
#{gl.utils.formatDate parseInt(stamp.date) * 1000}" - .attr 'class', (stamp) -> - extraClass = '' + "#{stamp.count} contributions
#{gl.utils.formatDate stamp.date}" + .attr 'class', 'user-contrib-cell js-tooltip' + .attr 'fill', (stamp) -> if stamp.count isnt 0 - diff = stamp.count / highestValue - classNumber = Math.floor (diff / 0.25) + 1 - extraClass += "user-contrib-cell-#{classNumber}" - - "user-contrib-cell #{extraClass} js-tooltip" + color(stamp.count) + else + '#ededed' .attr 'data-container', 'body' .on 'click', (stamp) -> - date = new Date(parseInt(stamp.date) * 1000) + date = stamp.date formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() $.ajax url: calendar_activities_path @@ -80,6 +109,7 @@ class @Calendar success: (data) -> $(".user-calendar-activities").html data + # Month titles svg.append 'g' .selectAll 'text' .data months @@ -92,5 +122,44 @@ class @Calendar .text (date) -> monthNames[date.month] - $('#cal-heatmap .js-tooltip').tooltip + # Day titles + days = [{ + text: 'M' + y: 29 + (17 * 1) + }, { + text: 'W' + y: 29 + (17 * 3) + }, { + text: 'F' + y: 29 + (17 * 5) + }] + svg.append 'g' + .selectAll 'text' + .data days + .enter() + .append 'text' + .attr 'text-anchor', 'middle' + .attr 'x', 8 + .attr 'y', (day) -> + day.y + .text (day) -> + day.text + .attr 'class', 'user-contrib-text' + + # Key with color boxes + svg.append 'g' + .attr 'transform', "translate(18, #{17 * 8 + 16})" + .selectAll 'rect' + .data keyColors + .enter() + .append 'rect' + .attr 'width', 15 + .attr 'height', 15 + .attr 'x', (color, i) -> + 17 * i + .attr 'y', 0 + .attr 'fill', (color) -> + color + + $('.js-contrib-calendar .js-tooltip').tooltip html: true diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 69b3b6586d..efdb2b06a8 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -8,7 +8,6 @@ *= require select2 *= require_self *= require dropzone/basic - *= require cal-heatmap *= require cropper.css *= require animate */ diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index da03454d80..5b48801485 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -26,75 +26,28 @@ } } -/** -* This overwrites the default values of the cal-heatmap gem -*/ -.calendar { - .qi { - fill: #fff; - } +.user-calendar { + text-align: center; - .q1 { - fill: #ededed !important; - } - - .q2 { - fill: #acd5f2 !important; - } - - .q3 { - fill: #7fa8d1 !important; - } - - .q4 { - fill: #49729b !important; - } - - .q5 { - fill: #254e77 !important; - } - - .future { - visibility: hidden; - } - - .domain-background { - fill: none; - shape-rendering: crispedges; - } - - .ch-tooltip { - padding: 3px; - font-weight: 550; + .calendar { + display: inline-block; } } .user-contrib-cell { - fill: #ededed; - cursor: pointer; - &:hover { + cursor: pointer; stroke: #000; } } -.user-contrib-cell-1 { - fill: #acd5f2; -} - -.user-contrib-cell-3 { - fill: #7fa8d1; -} - -.user-contrib-cell-4 { - fill: #49729b; -} - -.user-contrib-cell-5 { - fill: #254e77; -} - .user-contrib-text { font-size: 12px; fill: #959494; } + +.calendar-hint { + margin-top: -23px; + float: right; + font-size: 12px; +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 843379a3f5..ef375e1386 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -66,12 +66,6 @@ } } -.calendar-hint { - margin-top: -12px; - float: right; - font-size: 12px; -} - .profile-link-holder { display: inline; diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 799421c185..a99632454d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -74,8 +74,6 @@ class UsersController < ApplicationController def calendar calendar = contributions_calendar @timestamps = calendar.timestamps - @starting_year = calendar.starting_year - @starting_month = calendar.starting_month render 'calendar', layout: false end diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 2abee8be2a..77f2ddefb1 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -1,8 +1,9 @@ -#cal-heatmap.calendar - :javascript - new Calendar( - #{@timestamps.to_json}, - #{@starting_year}, - #{@starting_month}, - '#{user_calendar_activities_path}' - ); +.clearfix.calendar + .js-contrib-calendar + .calendar-hint + Summary of issues, merge requests, and push events +:javascript + new Calendar( + #{@timestamps.to_json}, + '#{user_calendar_activities_path}' + ); diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 9017fd54fc..0c51330830 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -89,10 +89,9 @@ .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs - %div{ class: container_class } - .user-calendar{data: {href: user_calendar_path}} - %h4.center.light - %i.fa.fa-spinner.fa-spin + .user-calendar{data: {href: user_calendar_path}} + %h4.center.light + %i.fa.fa-spinner.fa-spin .user-calendar-activities .content_list{ data: {href: user_path} } diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 85583dce9e..9dc2602867 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -19,7 +19,7 @@ module Gitlab select('date(created_at) as date, count(id) as total_amount'). map(&:attributes) - dates = (1.year.ago.to_date..(Date.today + 1.day)).to_a + dates = (1.year.ago.to_date..Date.today).to_a dates.each do |date| date_id = date.to_time.to_i.to_s From c3de94404099193c0ca6551ef149da1522adacd1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 27 Apr 2016 11:09:40 +0100 Subject: [PATCH 03/40] Removed cal heatmap from gemfile --- Gemfile | 3 --- Gemfile.lock | 2 -- 2 files changed, 5 deletions(-) diff --git a/Gemfile b/Gemfile index 6949c16e96..ba1ae9cf90 100644 --- a/Gemfile +++ b/Gemfile @@ -177,9 +177,6 @@ gem 'ruby-fogbugz', '~> 0.2.1' # d3 gem 'd3_rails', '~> 3.5.0' -#cal-heatmap -gem 'cal-heatmap-rails', '~> 3.6.0' - # underscore-rails gem "underscore-rails", "~> 1.8.0" diff --git a/Gemfile.lock b/Gemfile.lock index 4b51bf58bb..b1cd5ab4b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,7 +99,6 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (8.2.1) - cal-heatmap-rails (3.6.0) capybara (2.6.2) addressable mime-types (>= 1.16) @@ -902,7 +901,6 @@ DEPENDENCIES bullet bundler-audit byebug - cal-heatmap-rails (~> 3.6.0) capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) carrierwave (~> 0.10.0) From 1c9440c2cf171e919f3e7317e9a9827576f9aafd Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 27 Apr 2016 12:45:14 +0100 Subject: [PATCH 04/40] tests update --- features/steps/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/user.rb b/features/steps/user.rb index b1d088f07f..59385a6ab5 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -34,7 +34,7 @@ class Spinach::Features::User < Spinach::FeatureSteps end step 'I should see contributions calendar' do - expect(page).to have_css('.cal-heatmap-container') + expect(page).to have_css('.js-contrib-calendar') end def contributed_project From bef5695a81af7a0afbb7ebe30d8ce344afe824f5 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 27 Apr 2016 17:38:22 +0100 Subject: [PATCH 05/40] Formats tooltip title & date correctly Correctly changes the activity when clicking a date - will also revert back to old data when clicking date again --- app/assets/javascripts/calendar.js.coffee | 36 ++++++++++++++++------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 9c4d529f7e..dd6adfe978 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -1,5 +1,6 @@ class @Calendar constructor: (timestamps, calendar_activities_path) -> + currentSelectedDate = '' # Get the highest value from the timestampes highestValue = 0 _.each timestamps, (count) -> @@ -89,7 +90,14 @@ class @Calendar .attr 'width', 15 .attr 'height', 15 .attr 'title', (stamp) -> - "#{stamp.count} contributions
#{gl.utils.formatDate stamp.date}" + contribText = 'No contributions' + + if stamp.count > 0 + contribText = "#{stamp.count} contribution#{if stamp.count > 1 then 's' else ''}" + + date = dateFormat(stamp.date, 'mmm d, yyyy') + + "#{contribText}
#{date}" .attr 'class', 'user-contrib-cell js-tooltip' .attr 'fill', (stamp) -> if stamp.count isnt 0 @@ -98,16 +106,22 @@ class @Calendar '#ededed' .attr 'data-container', 'body' .on 'click', (stamp) -> - date = stamp.date - formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() - $.ajax - url: calendar_activities_path - data: - date: formated_date - cache: false - dataType: "html" - success: (data) -> - $(".user-calendar-activities").html data + if currentSelectedDate is '' + currentSelectedDate = stamp.date + formated_date = currentSelectedDate.getFullYear() + "-" + (currentSelectedDate.getMonth()+1) + "-" + currentSelectedDate.getDate() + + $.ajax + url: calendar_activities_path + data: + date: formated_date + cache: false + dataType: 'html' + success: (data) -> + $('.user-calendar-activities').html data + else + currentSelectedDate = '' + $('.user-calendar-activities, .content_list').html '' + Pager.getOld() # Month titles svg.append 'g' From 93c0bb8041963d6677cc9c169a0e1e22221dd0cb Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 28 Apr 2016 11:03:14 +0100 Subject: [PATCH 06/40] Fixed issue with selected date not loading correctly Added loading icon to selected date box --- app/assets/javascripts/calendar.js.coffee | 44 ++++++++++--------- .../stylesheets/framework/calendar.scss | 16 +------ app/views/users/calendar_activities.html.haml | 42 ++++++++++-------- 3 files changed, 49 insertions(+), 53 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index dd6adfe978..701117747d 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -1,6 +1,10 @@ class @Calendar constructor: (timestamps, calendar_activities_path) -> currentSelectedDate = '' + daySpace = 1 + daySize = 15 + daySizeWithSpace = daySize + (daySpace * 2) + # Get the highest value from the timestampes highestValue = 0 _.each timestamps, (count) -> @@ -51,7 +55,7 @@ class @Calendar months = [] svg = d3.select '.js-contrib-calendar' .append 'svg' - .attr 'width', 54 * 17 + .attr 'width', 54 * daySizeWithSpace .attr 'height', 167 .attr 'class', 'contrib-calendar' @@ -64,7 +68,7 @@ class @Calendar _.each group, (stamp, a) -> if a is 0 and stamp.day is 0 month = stamp.date.getMonth() - x = (17 * i + 1) + 17 + x = (daySizeWithSpace * i + 1) + daySizeWithSpace lastMonth = _.last(months) if lastMonth? lastMonthX = lastMonth.x @@ -73,12 +77,12 @@ class @Calendar months.push month: month x: x - else if month isnt lastMonth.month and x - 17 isnt lastMonthX + else if month isnt lastMonth.month and x - daySizeWithSpace isnt lastMonthX months.push month: month x: x - "translate(#{(17 * i + 1) + 17}, 18)" + "translate(#{(daySizeWithSpace * i + 1) + daySizeWithSpace}, 18)" .selectAll 'rect' .data (stamp) -> stamp @@ -86,9 +90,9 @@ class @Calendar .append 'rect' .attr 'x', '0' .attr 'y', (stamp, i) -> - (17 * stamp.day) - .attr 'width', 15 - .attr 'height', 15 + (daySizeWithSpace * stamp.day) + .attr 'width', daySize + .attr 'height', daySize .attr 'title', (stamp) -> contribText = 'No contributions' @@ -106,22 +110,22 @@ class @Calendar '#ededed' .attr 'data-container', 'body' .on 'click', (stamp) -> - if currentSelectedDate is '' + if currentSelectedDate isnt stamp.date currentSelectedDate = stamp.date - formated_date = currentSelectedDate.getFullYear() + "-" + (currentSelectedDate.getMonth()+1) + "-" + currentSelectedDate.getDate() + formatted_date = currentSelectedDate.getFullYear() + "-" + (currentSelectedDate.getMonth()+1) + "-" + currentSelectedDate.getDate() $.ajax url: calendar_activities_path data: - date: formated_date + date: formatted_date cache: false dataType: 'html' + beforeSend: -> + $('.user-calendar-activities').html '
' success: (data) -> $('.user-calendar-activities').html data else - currentSelectedDate = '' - $('.user-calendar-activities, .content_list').html '' - Pager.getOld() + $('.user-calendar-activities').html '' # Month titles svg.append 'g' @@ -139,13 +143,13 @@ class @Calendar # Day titles days = [{ text: 'M' - y: 29 + (17 * 1) + y: 29 + (daySizeWithSpace * 1) }, { text: 'W' - y: 29 + (17 * 3) + y: 29 + (daySizeWithSpace * 3) }, { text: 'F' - y: 29 + (17 * 5) + y: 29 + (daySizeWithSpace * 5) }] svg.append 'g' .selectAll 'text' @@ -162,15 +166,15 @@ class @Calendar # Key with color boxes svg.append 'g' - .attr 'transform', "translate(18, #{17 * 8 + 16})" + .attr 'transform', "translate(18, #{daySizeWithSpace * 8 + 16})" .selectAll 'rect' .data keyColors .enter() .append 'rect' - .attr 'width', 15 - .attr 'height', 15 + .attr 'width', daySize + .attr 'height', daySize .attr 'x', (color, i) -> - 17 * i + daySizeWithSpace * i .attr 'y', 0 .attr 'fill', (color) -> color diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 5b48801485..81118b13be 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -5,24 +5,12 @@ } .user-calendar-activities { - .calendar_onclick_hr { - padding: 0; - margin: 10px 0; - } - .str-truncated { max-width: 70%; } - .text-expander { - background: #eee; - color: #555; - padding: 0 5px; - cursor: pointer; - margin-left: 4px; - &:hover { - background-color: #ddd; - } + .user-calendar-activities-loading { + font-size: 24px; } } diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 027a93a75f..630d97e339 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,23 +1,27 @@ %h4.prepend-top-20 - %span.light Contributions for + Contributions for %strong #{@calendar_date.to_s(:short)} -%ul.bordered-list - - @events.sort_by(&:created_at).each do |event| - %li - %span.light - %i.fa.fa-clock-o - = event.created_at.to_s(:time) - - if event.push? - #{event.action_name} #{event.ref_type} #{event.ref_name} - - else - = event_action_name(event) - - if event.target - %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] - - at - %strong - - if event.project - = link_to_project event.project +- if @events.any? + %ul.bordered-list + - @events.sort_by(&:created_at).each do |event| + %li + %span.light + %i.fa.fa-clock-o + = event.created_at.to_s(:time) + - if event.push? + #{event.action_name} #{event.ref_type} #{event.ref_name} - else - = event.project_name + = event_action_name(event) + - if event.target + %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] + + at + %strong + - if event.project + = link_to_project event.project + - else + = event.project_name +- else + %p + No contributions found for #{@calendar_date.to_s(:short)} From b54ea9de7c99df737d8f6fc358f8eb0ef8b20c76 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 28 Apr 2016 11:07:17 +0100 Subject: [PATCH 07/40] Updated spacing of the calendar --- app/assets/stylesheets/framework/calendar.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 81118b13be..8642b7530e 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -1,4 +1,7 @@ .calender-block { + padding-left: 0; + padding-right: 0; + @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) { overflow-x: scroll; } From 527d64463530df5a2f5f800885f5f84920b1058b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 16 May 2016 12:51:49 +0100 Subject: [PATCH 08/40] Moved into class methods --- app/assets/javascripts/calendar.js.coffee | 195 ++++++++++++---------- 1 file changed, 105 insertions(+), 90 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 701117747d..f9c7bffdad 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -1,32 +1,34 @@ class @Calendar - constructor: (timestamps, calendar_activities_path) -> - currentSelectedDate = '' - daySpace = 1 - daySize = 15 - daySizeWithSpace = daySize + (daySpace * 2) + constructor: (timestamps, @calendar_activities_path) -> + @currentSelectedDate = '' + @daySpace = 1 + @daySize = 15 + @daySizeWithSpace = @daySize + (@daySpace * 2) + @monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + @months = [] + @highestValue = 0 # Get the highest value from the timestampes - highestValue = 0 - _.each timestamps, (count) -> - if count > highestValue - highestValue = count + _.each timestamps, (count) => + if count > @highestValue + @highestValue = count # Loop through the timestamps to create a group of objects # The group of objects will be grouped based on the day of the week they are - timestampsTmp = [] + @timestampsTmp = [] i = 0 group = 0 - _.each timestamps, (count, date) -> + _.each timestamps, (count, date) => newDate = new Date parseInt(date) * 1000 day = newDate.getDay() # Create a new group array if this is the first day of the week # or if is first object if (day is 0 and i isnt 0) or i is 0 - timestampsTmp.push [] + @timestampsTmp.push [] group++ - innerArray = timestampsTmp[group-1] + innerArray = @timestampsTmp[group-1] # Push to the inner array the values that will be used to render map innerArray.push @@ -36,64 +38,61 @@ class @Calendar i++ - # Color function for chart - color = d3 - .scale - .linear() - .range(['#acd5f2', '#254e77']) - .domain([0, highestValue]) + # Init color functions + @color = @initColor() + @colorKey = @initColorKey() - # Color function for key - colorKey = d3 - .scale - .linear() - .range(['#acd5f2', '#254e77']) - .domain([0, 3]) - keyColors = ['#ededed', colorKey(0), colorKey(1), colorKey(2), colorKey(3)] + # Init the svg element + @renderSvg(group) + @renderDays() + @renderMonths() + @renderDayTitles() + @renderKey() - monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - months = [] - svg = d3.select '.js-contrib-calendar' + @initTooltips() + + renderSvg: (group) -> + @svg = d3.select '.js-contrib-calendar' .append 'svg' - .attr 'width', 54 * daySizeWithSpace + .attr 'width', (group + 1) * @daySizeWithSpace .attr 'height', 167 .attr 'class', 'contrib-calendar' - # Setup each day box - svg.selectAll 'g' - .data timestampsTmp + renderDays: -> + @svg.selectAll 'g' + .data @timestampsTmp .enter() .append 'g' - .attr 'transform', (group, i) -> - _.each group, (stamp, a) -> + .attr 'transform', (group, i) => + _.each group, (stamp, a) => if a is 0 and stamp.day is 0 month = stamp.date.getMonth() - x = (daySizeWithSpace * i + 1) + daySizeWithSpace - lastMonth = _.last(months) + x = (@daySizeWithSpace * i + 1) + @daySizeWithSpace + lastMonth = _.last(@months) if lastMonth? lastMonthX = lastMonth.x if !lastMonth? - months.push + @months.push month: month x: x - else if month isnt lastMonth.month and x - daySizeWithSpace isnt lastMonthX - months.push + else if month isnt lastMonth.month and x - @daySizeWithSpace isnt lastMonthX + @months.push month: month x: x - "translate(#{(daySizeWithSpace * i + 1) + daySizeWithSpace}, 18)" + "translate(#{(@daySizeWithSpace * i + 1) + @daySizeWithSpace}, 18)" .selectAll 'rect' .data (stamp) -> stamp .enter() .append 'rect' .attr 'x', '0' - .attr 'y', (stamp, i) -> - (daySizeWithSpace * stamp.day) - .attr 'width', daySize - .attr 'height', daySize - .attr 'title', (stamp) -> + .attr 'y', (stamp, i) => + (@daySizeWithSpace * stamp.day) + .attr 'width', @daySize + .attr 'height', @daySize + .attr 'title', (stamp) => contribText = 'No contributions' if stamp.count > 0 @@ -103,55 +102,26 @@ class @Calendar "#{contribText}
#{date}" .attr 'class', 'user-contrib-cell js-tooltip' - .attr 'fill', (stamp) -> + .attr 'fill', (stamp) => if stamp.count isnt 0 - color(stamp.count) + @color(stamp.count) else '#ededed' .attr 'data-container', 'body' - .on 'click', (stamp) -> - if currentSelectedDate isnt stamp.date - currentSelectedDate = stamp.date - formatted_date = currentSelectedDate.getFullYear() + "-" + (currentSelectedDate.getMonth()+1) + "-" + currentSelectedDate.getDate() + .on 'click', @clickDay - $.ajax - url: calendar_activities_path - data: - date: formatted_date - cache: false - dataType: 'html' - beforeSend: -> - $('.user-calendar-activities').html '
' - success: (data) -> - $('.user-calendar-activities').html data - else - $('.user-calendar-activities').html '' - - # Month titles - svg.append 'g' - .selectAll 'text' - .data months - .enter() - .append 'text' - .attr 'x', (date) -> - date.x - .attr 'y', 10 - .attr 'class', 'user-contrib-text' - .text (date) -> - monthNames[date.month] - - # Day titles + renderDayTitles: -> days = [{ text: 'M' - y: 29 + (daySizeWithSpace * 1) + y: 29 + (@daySizeWithSpace * 1) }, { text: 'W' - y: 29 + (daySizeWithSpace * 3) + y: 29 + (@daySizeWithSpace * 3) }, { text: 'F' - y: 29 + (daySizeWithSpace * 5) + y: 29 + (@daySizeWithSpace * 5) }] - svg.append 'g' + @svg.append 'g' .selectAll 'text' .data days .enter() @@ -164,20 +134,65 @@ class @Calendar day.text .attr 'class', 'user-contrib-text' - # Key with color boxes - svg.append 'g' - .attr 'transform', "translate(18, #{daySizeWithSpace * 8 + 16})" + renderMonths: -> + @svg.append 'g' + .selectAll 'text' + .data @months + .enter() + .append 'text' + .attr 'x', (date) -> + date.x + .attr 'y', 10 + .attr 'class', 'user-contrib-text' + .text (date) => + @monthNames[date.month] + + renderKey: -> + keyColors = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)] + @svg.append 'g' + .attr 'transform', "translate(18, #{@daySizeWithSpace * 8 + 16})" .selectAll 'rect' .data keyColors .enter() .append 'rect' - .attr 'width', daySize - .attr 'height', daySize - .attr 'x', (color, i) -> - daySizeWithSpace * i + .attr 'width', @daySize + .attr 'height', @daySize + .attr 'x', (color, i) => + @daySizeWithSpace * i .attr 'y', 0 .attr 'fill', (color) -> color + initColor: -> + d3.scale + .linear() + .range(['#acd5f2', '#254e77']) + .domain([0, @highestValue]) + + initColorKey: -> + d3.scale + .linear() + .range(['#acd5f2', '#254e77']) + .domain([0, 3]) + + clickDay: (stamp) -> + if @currentSelectedDate isnt stamp.date + @currentSelectedDate = stamp.date + formatted_date = @currentSelectedDate.getFullYear() + "-" + (@currentSelectedDate.getMonth()+1) + "-" + @currentSelectedDate.getDate() + + $.ajax + url: @calendar_activities_path + data: + date: formatted_date + cache: false + dataType: 'html' + beforeSend: -> + $('.user-calendar-activities').html '
' + success: (data) -> + $('.user-calendar-activities').html data + else + $('.user-calendar-activities').html '' + + initTooltips: -> $('.js-contrib-calendar .js-tooltip').tooltip html: true From 74b74e59327ef4e393b937d33ba10c43c1e1087b Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 17 May 2016 15:01:03 +0200 Subject: [PATCH 09/40] fix for import URL URI problem when URL contains a space --- lib/gitlab/import_url.rb | 6 +++++- spec/lib/gitlab/import_url_spec.rb | 21 +++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/import_url.rb b/lib/gitlab/import_url.rb index d23b013c1f..f1191cc3a5 100644 --- a/lib/gitlab/import_url.rb +++ b/lib/gitlab/import_url.rb @@ -1,7 +1,7 @@ module Gitlab class ImportUrl def initialize(url, credentials: nil) - @url = URI.parse(URI.encode(url)) + @url = URI.parse(encode(url)) @credentials = credentials end @@ -37,5 +37,9 @@ module Gitlab def valid_credentials? credentials && credentials.is_a?(Hash) && credentials.any? end + + def encode(url) + URI.decode(url).size < url.size ? url : URI.encode(url) + end end end diff --git a/spec/lib/gitlab/import_url_spec.rb b/spec/lib/gitlab/import_url_spec.rb index f758cb8693..46029b121d 100644 --- a/spec/lib/gitlab/import_url_spec.rb +++ b/spec/lib/gitlab/import_url_spec.rb @@ -3,19 +3,32 @@ require 'spec_helper' describe Gitlab::ImportUrl do let(:credentials) { { user: 'blah', password: 'password' } } + let(:url) { "https://github.com/me/project.git" } let(:import_url) do - Gitlab::ImportUrl.new("https://github.com/me/project.git", credentials: credentials) + described_class.new(url, credentials: credentials) end - describe :full_url do + describe 'full_url' do it { expect(import_url.full_url).to eq("https://blah:password@github.com/me/project.git") } end - describe :sanitized_url do + describe 'sanitized_url' do it { expect(import_url.sanitized_url).to eq("https://github.com/me/project.git") } end - describe :credentials do + describe 'credentials' do it { expect(import_url.credentials).to eq(credentials) } end + + context 'URL encoding' do + describe 'not encoded URL' do + let(:url) { "https://github.com/me/my project.git" } + it { expect(import_url.sanitized_url).to eq("https://github.com/me/my%20project.git") } + end + + describe 'already encoded URL' do + let(:url) { "https://github.com/me/my%20project.git" } + it { expect(import_url.sanitized_url).to eq("https://github.com/me/my%20project.git") } + end + end end From 8a91f3f27399745722fe527ee76ab18bc5b26245 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 17 May 2016 15:04:33 +0200 Subject: [PATCH 10/40] added CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 20e2fcb806..bcf44b9a8d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,6 +61,7 @@ v 8.8.0 (unreleased) v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) + - Fix import by `Any Git URL` broken if the URL contains a space v 8.7.5 - Fix relative links in wiki pages. !4050 From 1cbc898223d2f9b78cb6494108d2aed00d19d00e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 20 May 2016 18:50:16 -0500 Subject: [PATCH 11/40] Improve design of Pipeline view --- CHANGELOG | 1 + app/assets/stylesheets/pages/pipelines.scss | 26 ++++++++++++++-- .../projects/ci/commits/_commit.html.haml | 31 ++++++++++--------- app/views/projects/commit/_ci_stage.html.haml | 1 + app/views/projects/pipelines/index.html.haml | 6 ++-- 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 67fca2c6f6..0884ac1947 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.8.0 (unreleased) - Added inline diff styling for `change_title` system notes. (Adam Butler) - Project#open_branches has been cleaned up and no longer loads entire records into memory. - Escape HTML in commit titles in system note messages + - Improve design of Pipeline View - Fix creation of Ci::Commit object which can lead to pending, failed in some scenarios - Improve multiple branch push performance by memoizing permission checking - Log to application.log when an admin starts and stops impersonating a user diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 546176b65e..87b0562c29 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,4 +1,24 @@ -.pipeline-stage { - overflow: hidden; - text-overflow: ellipsis; +.pipelines { + .stage { + max-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .duration, .finished_at { + margin: 4px 0; + } + + .commit-title { + margin: 0; + } + + .controls { + white-space: nowrap; + } + + .btn { + margin: 4px; + } } diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index 13162b41f9..2ea9333913 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -12,10 +12,10 @@ · = link_to commit.short_sha, namespace_project_commit_path(@project.namespace, @project, commit.sha), class: "commit-id monospace"   - - if commit.latest? - %span.label.label-success latest - if commit.tag? %span.label.label-primary tag + - elsif commit.latest? + %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest - if commit.triggered? %span.label.label-primary triggered - if commit.yaml_errors.present? @@ -23,30 +23,33 @@ - if commit.builds.any?(&:stuck?) %span.label.label-warning stuck - %p - %span - - if commit_data = commit.commit_data - = link_to_gfm commit_data.title, namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message" - - else - Cant find HEAD commit for this branch + %p.commit-title + - if commit_data = commit.commit_data + = link_to_gfm truncate(commit_data.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message" + - else + Cant find HEAD commit for this branch - stages_status = commit.statuses.stages_status - stages.each do |stage| %td - - if status = stages_status[stage] - - tooltip = "#{stage.titleize}: #{status}" - %span.has-tooltip{ title: "#{tooltip}", class: "ci-status-icon-#{status}" } + - status = stages_status[stage] + - tooltip = "#{stage.titleize}: #{status || 'not found'}" + - if status + = link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status || 'skipped'}", title: tooltip do = ci_icon_for_status(status) + - else + .light + \- %td - if commit.started_at && commit.finished_at - %p + %p.duration = icon("clock-o")   #{duration_in_words(commit.finished_at, commit.started_at)} - if commit.finished_at - %p + %p.finished_at = icon("calendar")   #{time_ago_with_tooltip(commit.finished_at)} @@ -67,11 +70,9 @@ %span #{build.name} - if can?(current_user, :update_pipeline, @project) -   - if commit.retryable? && commit.builds.failed.any? = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do = icon("repeat") -   - if commit.active? = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do = icon("remove") diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml index aaa318e1eb..ae7bb01223 100644 --- a/app/views/projects/commit/_ci_stage.html.haml +++ b/app/views/projects/commit/_ci_stage.html.haml @@ -1,6 +1,7 @@ %tr %th{colspan: 10} %strong + %a{name: stage} - status = statuses.latest.status %span{class: "ci-status-link ci-status-icon-#{status}"} = ci_icon_for_status(status) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 9d5b6d367c..4e2e819070 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -44,7 +44,7 @@ - else #{@scope.titleize} for this project -%ul.content-list +%ul.content-list.pipelines - stages = @pipelines.stages - if @pipelines.blank? %li @@ -56,8 +56,8 @@ %th ID %th Commit - stages.each do |stage| - %th - %span.pipeline-stage.has-tooltip{ title: "#{stage.titleize}" } + %th.stage + %span.has-tooltip{ title: "#{stage.titleize}" } = stage.titleize.pluralize %th %th From 8d0332368fba65f167f2109daf8a2184d781082b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 21 May 2016 09:52:50 -0500 Subject: [PATCH 12/40] Improve status rendering --- app/views/projects/ci/commits/_commit.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index 2ea9333913..9c63510d86 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -36,10 +36,10 @@ - status = stages_status[stage] - tooltip = "#{stage.titleize}: #{status || 'not found'}" - if status - = link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status || 'skipped'}", title: tooltip do + = link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do = ci_icon_for_status(status) - else - .light + .light.has-tooltip{ title: tooltip } \- %td From f2f345e6048f80e4d6510db102cd046fca43d4e4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 23 May 2016 09:05:01 +0200 Subject: [PATCH 13/40] avoid encoding import url and delegate good format to user --- lib/gitlab/import_url.rb | 6 +----- spec/lib/gitlab/import_url_spec.rb | 12 ------------ 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/lib/gitlab/import_url.rb b/lib/gitlab/import_url.rb index f1191cc3a5..3cfbc17b89 100644 --- a/lib/gitlab/import_url.rb +++ b/lib/gitlab/import_url.rb @@ -1,7 +1,7 @@ module Gitlab class ImportUrl def initialize(url, credentials: nil) - @url = URI.parse(encode(url)) + @url = URI.parse(url) @credentials = credentials end @@ -37,9 +37,5 @@ module Gitlab def valid_credentials? credentials && credentials.is_a?(Hash) && credentials.any? end - - def encode(url) - URI.decode(url).size < url.size ? url : URI.encode(url) - end end end diff --git a/spec/lib/gitlab/import_url_spec.rb b/spec/lib/gitlab/import_url_spec.rb index 46029b121d..7948386b0e 100644 --- a/spec/lib/gitlab/import_url_spec.rb +++ b/spec/lib/gitlab/import_url_spec.rb @@ -19,16 +19,4 @@ describe Gitlab::ImportUrl do describe 'credentials' do it { expect(import_url.credentials).to eq(credentials) } end - - context 'URL encoding' do - describe 'not encoded URL' do - let(:url) { "https://github.com/me/my project.git" } - it { expect(import_url.sanitized_url).to eq("https://github.com/me/my%20project.git") } - end - - describe 'already encoded URL' do - let(:url) { "https://github.com/me/my%20project.git" } - it { expect(import_url.sanitized_url).to eq("https://github.com/me/my%20project.git") } - end - end end From d83ce65c1a0a8bcec603faf342cd2d78075ffb8d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 23 May 2016 09:11:21 +0200 Subject: [PATCH 14/40] fix changelog and merge --- CHANGELOG | 4 +++- lib/gitlab/url_sanitizer.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1c26692f3a..21b383b200 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -83,9 +83,11 @@ v 8.8.0 - Allows MR authors to have the source branch removed when merging the MR. !2801 (Jeroen Jacobs) - When creating a .gitignore file a dropdown with templates will be provided +v 8.7.7 + - Fix import by `Any Git URL` broken if the URL contains a space + v 8.7.6 - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) - - Fix import by `Any Git URL` broken if the URL contains a space - Fix import from GitLab.com to a private instance failure. !4181 - Fix external imports not finding the import data. !4106 diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb index c59d53b941..7d02fe3c97 100644 --- a/lib/gitlab/url_sanitizer.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -7,7 +7,7 @@ module Gitlab end def initialize(url, credentials: nil) - @url = Addressable::URI.parse(URI.encode(url)) + @url = Addressable::URI.parse(url) @credentials = credentials end From 66cebb6f61193a5fb4199b85580a312ed6d94b42 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 21 May 2016 18:02:49 -0500 Subject: [PATCH 15/40] Remove `event_to_atom` helper --- app/helpers/events_helper.rb | 22 ------------------- .../dashboard/projects/index.atom.builder | 4 +--- app/views/events/_event.atom.builder | 20 +++++++++++++++++ app/views/groups/show.atom.builder | 4 +--- app/views/projects/show.atom.builder | 4 +--- app/views/users/show.atom.builder | 4 +--- 6 files changed, 24 insertions(+), 34 deletions(-) create mode 100644 app/views/events/_event.atom.builder diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index e148938170..bfedcb1c42 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -159,28 +159,6 @@ module EventsHelper "--broken encoding" end - def event_to_atom(xml, event) - if event.visible_to_user?(current_user) - xml.entry do - event_link = event_feed_url(event) - event_title = event_feed_title(event) - event_summary = event_feed_summary(event) - - xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" - xml.link href: event_link - xml.title truncate(event_title, length: 80) - xml.updated event.created_at.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) - xml.author do |author| - xml.name event.author_name - xml.email event.author_email - end - - xml.summary(type: "xhtml") { |x| x << event_summary unless event_summary.nil? } - end - end - end - def event_row_class(event) if event.body? "event-block" diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index d4daf07c6c..fb5be63b47 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id dashboard_projects_url xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(partial: 'events/event', collection: @events) if @events.any? end diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder new file mode 100644 index 0000000000..7890e717aa --- /dev/null +++ b/app/views/events/_event.atom.builder @@ -0,0 +1,20 @@ +return unless event.visible_to_user?(current_user) + +xml.entry do + xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" + xml.link href: event_feed_url(event) + xml.title truncate(event_feed_title(event), length: 80) + xml.updated event.created_at.xmlschema + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) + + xml.author do + xml.name event.author_name + xml.email event.author_email + end + + xml.summary(type: "xhtml") do |summary| + event_summary = event_feed_summary(event) + + summary << event_summary unless event_summary.nil? + end +end diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index c66b82bb48..b68bf444d2 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id group_url(@group) xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(@events) if @events.any? end diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index 9b3d3f069d..11310d5e1e 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id namespace_project_url(@project.namespace, @project) xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(@events) if @events.any? end diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder index e9e466c635..6c85e5f9fb 100644 --- a/app/views/users/show.atom.builder +++ b/app/views/users/show.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id user_url(@user) xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(@events) if @events.any? end From 692f11b05d6ec751d1944cb025812481345329a8 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 21 May 2016 18:04:28 -0500 Subject: [PATCH 16/40] Render a collection for the Commits atom feed --- app/views/projects/commits/_commit.atom.builder | 14 ++++++++++++++ app/views/projects/commits/show.atom.builder | 15 +-------------- 2 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 app/views/projects/commits/_commit.atom.builder diff --git a/app/views/projects/commits/_commit.atom.builder b/app/views/projects/commits/_commit.atom.builder new file mode 100644 index 0000000000..1657fb4616 --- /dev/null +++ b/app/views/projects/commits/_commit.atom.builder @@ -0,0 +1,14 @@ +xml.entry do + xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id) + xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id) + xml.title truncate(commit.title, length: 80) + xml.updated commit.committed_date.xmlschema + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email)) + + xml.author do |author| + xml.name commit.author_name + xml.email commit.author_email + end + + xml.summary markdown(commit.description, pipeline: :single_line) +end diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index e310fafd82..30bb741207 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -6,18 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id namespace_project_commits_url(@project.namespace, @project, @ref) xml.updated @commits.first.committed_date.xmlschema if @commits.any? - @commits.each do |commit| - xml.entry do - xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id) - xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id) - xml.title truncate(commit.title, length: 80) - xml.updated commit.committed_date.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email)) - xml.author do |author| - xml.name commit.author_name - xml.email commit.author_email - end - xml.summary markdown(commit.description, pipeline: :single_line) - end - end + xml << render(@commits) if @commits.any? end From 7001842dc463f1ba9ac4f1479c7310c75707c35b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 23 May 2016 17:06:26 -0400 Subject: [PATCH 17/40] Remove `issue_to_atom` helper --- app/helpers/issues_helper.rb | 17 ----------------- app/views/dashboard/issues.atom.builder | 5 +---- app/views/groups/issues.atom.builder | 5 +---- app/views/issues/_issue.atom.builder | 14 ++++++++++++++ app/views/projects/issues/index.atom.builder | 4 +--- 5 files changed, 17 insertions(+), 28 deletions(-) create mode 100644 app/views/issues/_issue.atom.builder diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 198d39455d..551409e885 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -105,23 +105,6 @@ module IssuesHelper return 'hidden' if issue.closed? == closed end - def issue_to_atom(xml, issue) - xml.entry do - xml.id namespace_project_issue_url(issue.project.namespace, - issue.project, issue) - xml.link href: namespace_project_issue_url(issue.project.namespace, - issue.project, issue) - xml.title truncate(issue.title, length: 80) - xml.updated issue.created_at.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) - xml.author do |author| - xml.name issue.author_name - xml.email issue.author_email - end - xml.summary issue.title - end - end - def merge_requests_sentence(merge_requests) # Sorting based on the `!123` or `group/project!123` reference will sort # local merge requests first. diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index 0d7b1b30dc..83c0c6da21 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -6,8 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id issues_dashboard_url xml.updated @issues.first.created_at.xmlschema if @issues.any? - @issues.each do |issue| - issue_to_atom(xml, issue) - end + xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? end - diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index a6eb9abada..c19671295a 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -6,8 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id issues_group_url xml.updated @issues.first.created_at.xmlschema if @issues.any? - @issues.each do |issue| - issue_to_atom(xml, issue) - end + xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? end - diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder new file mode 100644 index 0000000000..68a2d19e58 --- /dev/null +++ b/app/views/issues/_issue.atom.builder @@ -0,0 +1,14 @@ +xml.entry do + xml.id namespace_project_issue_url(issue.project.namespace, issue.project, issue) + xml.link href: namespace_project_issue_url(issue.project.namespace, issue.project, issue) + xml.title truncate(issue.title, length: 80) + xml.updated issue.created_at.xmlschema + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) + + xml.author do |author| + xml.name issue.author_name + xml.email issue.author_email + end + + xml.summary issue.title +end diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index ee8a941465..7ad7c9c87e 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id namespace_project_issues_url(@project.namespace, @project) xml.updated @issues.first.created_at.xmlschema if @issues.any? - @issues.each do |issue| - issue_to_atom(xml, issue) - end + xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? end From b2c411710d80a8d801876ec406f1c6035cc5b23f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 24 May 2016 10:59:59 -0500 Subject: [PATCH 18/40] Increase max-width of stage cells --- app/assets/stylesheets/pages/pipelines.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 87b0562c29..ce917a471c 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,6 +1,6 @@ .pipelines { .stage { - max-width: 0; + max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; From 4c058fc429a4173e033a4babcb379c52b5ed13e3 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 24 May 2016 12:16:37 -0500 Subject: [PATCH 19/40] Remove icons from tables; remove timestamp from pipelines, remove info block at top of pipelines --- app/assets/stylesheets/framework/nav.scss | 2 +- app/assets/stylesheets/pages/merge_requests.scss | 6 ------ app/assets/stylesheets/pages/pipelines.scss | 2 +- app/views/projects/ci/builds/_build.html.haml | 4 ---- app/views/projects/ci/commits/_commit.html.haml | 7 ------- .../_generic_commit_status.html.haml | 4 ---- app/views/projects/pipelines/index.html.haml | 10 +--------- 7 files changed, 3 insertions(+), 32 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index c70be58f48..afb0cd1a1e 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -281,7 +281,7 @@ } .page-with-layout-nav { - margin-top: 50px; + margin-top: $header-height + 2; &.controls-dropdown-visible { @media (max-width: $screen-xs-min) { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index c4005ba1e6..4f8a8748d3 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -280,11 +280,5 @@ background-color: $white-light; color: $gl-placeholder-color; } - - th, - td { - padding: 16px; - } } } - diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index ce917a471c..6128868b67 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,6 +1,6 @@ .pipelines { .stage { - max-width: 80px; + max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index e23a3782c6..5bd6e3f0eb 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -57,14 +57,10 @@ %td.duration - if build.duration - = icon("clock-o") -   #{duration_in_words(build.finished_at, build.started_at)} %td.timestamp - if build.finished_at - = icon("calendar") -   %span #{time_ago_with_tooltip(build.finished_at)} - if defined?(coverage) && coverage diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml index 9c63510d86..5b6b940a0c 100644 --- a/app/views/projects/ci/commits/_commit.html.haml +++ b/app/views/projects/ci/commits/_commit.html.haml @@ -45,14 +45,7 @@ %td - if commit.started_at && commit.finished_at %p.duration - = icon("clock-o") -   #{duration_in_words(commit.finished_at, commit.started_at)} - - if commit.finished_at - %p.finished_at - = icon("calendar") -   - #{time_ago_with_tooltip(commit.finished_at)} %td .controls.hidden-xs.pull-right diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 8129514964..8be124f387 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -50,14 +50,10 @@ %td.duration - if generic_commit_status.duration - = icon("clock-o") -   #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)} %td.timestamp - if generic_commit_status.finished_at - = icon("calendar") -   %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} - if defined?(coverage) && coverage diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 4e2e819070..a6c12814ad 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -36,14 +36,6 @@ = icon('wrench') %span CI Lint -.row-content-block - - if @scope == 'running' - Running pipelines for this project - - elsif @scope.nil? - Pipelines for this project - - else - #{@scope.titleize} for this project - %ul.content-list.pipelines - stages = @pipelines.stages - if @pipelines.blank? @@ -59,7 +51,7 @@ %th.stage %span.has-tooltip{ title: "#{stage.titleize}" } = stage.titleize.pluralize - %th + %th Duration %th = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages From 65925e699839e1f2e01c6c5f7a355d9fae0337d9 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 24 May 2016 12:31:46 -0500 Subject: [PATCH 20/40] Remove top block from builds page --- app/views/admin/builds/index.html.haml | 1 - app/views/projects/builds/index.html.haml | 3 --- spec/features/builds_spec.rb | 1 - 3 files changed, 5 deletions(-) diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index ed24757087..d74cf8598e 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -47,4 +47,3 @@ = render "admin/builds/build", build: build = paginate @builds, theme: 'gitlab' - diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 98f4a9416e..7c0bec264a 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -35,9 +35,6 @@ = icon('wrench') %span CI Lint -.row-content-block - #{(@scope || 'all').capitalize} builds from this project - %ul.content-list - if @builds.blank? %li diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index f83a78308e..090a941958 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -43,7 +43,6 @@ describe "Builds" do end it { expect(page).to have_selector('.nav-links li.active', text: 'All') } - it { expect(page).to have_selector('.row-content-block', text: 'All builds from this project') } it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } From b359d5d57f4b836c04e9e2ef7e1fcb3775bd5305 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 24 May 2016 17:55:57 -0700 Subject: [PATCH 21/40] Fix groups API to list only user's accessible projects Closes #17496 --- CHANGELOG | 1 + app/finders/group_projects_finder.rb | 2 +- lib/api/groups.rb | 3 +-- spec/requests/api/groups_spec.rb | 21 ++++++++++++++++++--- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a79c3048b7..0866ad89dd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.9.0 (unreleased) - Redesign navigation for project pages + - Fix groups API to list only user's accessible projects - Use gitlab-shell v3.0.0 v 8.8.2 (unreleased) diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index 3b9a421b11..aa8f4c1d0e 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -18,7 +18,7 @@ class GroupProjectsFinder < UnionFinder projects = [] if current_user - if @group.users.include?(current_user) + if @group.users.include?(current_user) || current_user.admin? projects << @group.projects unless only_shared projects << @group.shared_projects unless only_owned else diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 91e420832f..9d8b8d737a 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -95,8 +95,7 @@ module API # GET /groups/:id/projects get ":id/projects" do group = find_group(params[:id]) - projects = group.projects - projects = filter_projects(projects) + projects = GroupProjectsFinder.new(group).execute(current_user) projects = paginate projects present projects, with: Entities::Project end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 37ddab83c3..7ecefce80d 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -12,6 +12,7 @@ describe API::API, api: true do let!(:group2) { create(:group, :private) } let!(:project1) { create(:project, namespace: group1) } let!(:project2) { create(:project, namespace: group2) } + let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) } before do group1.add_owner(user1) @@ -147,9 +148,11 @@ describe API::API, api: true do context "when authenticated as user" do it "should return the group's projects" do get api("/groups/#{group1.id}/projects", user1) + expect(response.status).to eq(200) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(project1.name) + expect(json_response.length).to eq(2) + project_names = json_response.map { |proj| proj['name' ] } + expect(project_names).to match_array([project1.name, project3.name]) end it "should not return a non existing group" do @@ -162,6 +165,16 @@ describe API::API, api: true do expect(response.status).to eq(404) end + + it "should only return projects to which user has access" do + project3.team << [user3, :developer] + + get api("/groups/#{group1.id}/projects", user3) + + expect(response.status).to eq(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project3.name) + end end context "when authenticated as admin" do @@ -181,8 +194,10 @@ describe API::API, api: true do context 'when using group path in URL' do it 'should return any existing group' do get api("/groups/#{group1.path}/projects", admin) + expect(response.status).to eq(200) - expect(json_response.first['name']).to eq(project1.name) + project_names = json_response.map { |proj| proj['name' ] } + expect(project_names).to match_array([project1.name, project3.name]) end it 'should not return a non existing group' do From 5273335247660465a39ffdcb1c801807e84b3eba Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 24 May 2016 17:53:53 -0400 Subject: [PATCH 22/40] Fix forks creation when visibility level is restricted --- CHANGELOG | 1 + app/services/projects/fork_service.rb | 14 ++++++++++- lib/gitlab/visibility_level.rb | 7 ++++++ spec/services/projects/fork_service_spec.rb | 27 +++++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c59a66874c..73f21ed4d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.9.0 (unreleased) + - Allow forking projects with restricted visibility level - Redesign navigation for project pages - Use gitlab-shell v3.0.0 - Changed the Slack build message to use the singular duration if necessary (Aran Koning) diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 0577ae778d..de6dc38cc8 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -3,7 +3,7 @@ module Projects def execute new_params = { forked_from_project_id: @project.id, - visibility_level: @project.visibility_level, + visibility_level: allowed_visibility_level, description: @project.description, name: @project.name, path: @project.path, @@ -19,5 +19,17 @@ module Projects new_project = CreateService.new(current_user, new_params).execute new_project end + + private + + def allowed_visibility_level + project_level = @project.visibility_level + + if Gitlab::VisibilityLevel.non_restricted_level?(project_level) + project_level + else + Gitlab::VisibilityLevel.highest_allowed_level + end + end end end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index a1ee1cba21..9462f3368e 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -32,6 +32,13 @@ module Gitlab } end + def highest_allowed_level + restricted_levels = current_application_settings.restricted_visibility_levels + + allowed_levels = self.values - restricted_levels + allowed_levels.max || PRIVATE + end + def allowed_for?(user, level) user.is_admin? || allowed_level?(level.to_i) end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index d1ee60a0ae..31bb7120d8 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -42,6 +42,33 @@ describe Projects::ForkService, services: true do expect(@to_project.builds_enabled?).to be_truthy end end + + context "when project has restricted visibility level" do + context "and only one visibility level is restricted" do + before do + @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) + end + + it "creates fork with highest allowed level" do + forked_project = fork_project(@from_project, @to_user) + + expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + context "and all visibility levels are restricted" do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE]) + end + + it "creates fork with private visibility levels" do + forked_project = fork_project(@from_project, @to_user) + + expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + end + end end describe :fork_to_namespace do From b10b6f963dd0d0da26e1750263e6b970b839ba0f Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Wed, 25 May 2016 11:30:11 -0500 Subject: [PATCH 23/40] Add new HA diagrams [ci skip] --- doc/administration/high_availability/README.md | 4 ++++ .../high_availability/active-active-diagram.png | Bin 0 -> 29607 bytes .../active-passive-diagram.png | Bin 0 -> 24246 bytes 3 files changed, 4 insertions(+) create mode 100644 doc/administration/img/high_availability/active-active-diagram.png create mode 100644 doc/administration/img/high_availability/active-passive-diagram.png diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index 43d85ffb77..d74a786ac2 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -19,6 +19,8 @@ Components/Servers Required: - 2 servers/virtual machines (one active/one passive) +![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png) + ### Active/Active This architecture scales easily because all application servers handle @@ -26,6 +28,8 @@ user requests simultaneously. The database, Redis, and GitLab application are all deployed on separate servers. The configuration is **only** highly-available if the database, Redis and storage are also configured as such. +![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png) + **Steps to configure active/active:** 1. [Configure the database](database.md) diff --git a/doc/administration/img/high_availability/active-active-diagram.png b/doc/administration/img/high_availability/active-active-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..81259e0ae9348a16e66407946719ddf3d75d974b GIT binary patch literal 29607 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

=&`f@jjZ+V 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 DRoJv# literal 0 HcmV?d00001 From 2682c5884777f1bfd5cb0b5c85b1f3c188906460 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 22 Mar 2016 20:27:36 -0400 Subject: [PATCH 24/40] Customize ActionMailer preview path --- config/environments/development.rb | 1 + spec/mailers/previews/devise_mailer_preview.rb | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 spec/mailers/previews/devise_mailer_preview.rb diff --git a/config/environments/development.rb b/config/environments/development.rb index 4f39016bfa..8cca0039b4 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -39,6 +39,7 @@ Rails.application.configure do config.action_mailer.delivery_method = :letter_opener_web # Don't make a mess when bootstrapping a development environment config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1') + config.action_mailer.preview_path = 'spec/mailers/previews' config.eager_load = false end diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/spec/mailers/previews/devise_mailer_preview.rb new file mode 100644 index 0000000000..8a4d6e7250 --- /dev/null +++ b/spec/mailers/previews/devise_mailer_preview.rb @@ -0,0 +1,11 @@ +class DeviseMailerPreview < ActionMailer::Preview + def confirmation_instructions_for_signup + user = User.new(email: 'signup@example.com') + DeviseMailer.confirmation_instructions(user, 'faketoken', {}) + end + + def confirmation_instructions_for_new_email + user = User.last + DeviseMailer.confirmation_instructions(user, 'faketoken', {}) + end +end From 66ae3897e2a170133f389f1713129e5b49d88ecc Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 22 Mar 2016 20:28:05 -0400 Subject: [PATCH 25/40] Make the standard Devise confirmation email our new plaintext version --- .../devise/mailer/confirmation_instructions.html.erb | 9 --------- .../devise/mailer/confirmation_instructions.text.erb | 9 +++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 app/views/devise/mailer/confirmation_instructions.html.erb create mode 100644 app/views/devise/mailer/confirmation_instructions.text.erb diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb deleted file mode 100644 index c6fa8f0ee3..0000000000 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -

Welcome <%= @resource.name %>!

- -<% if @resource.unconfirmed_email.present? %> -

You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below:

-<% else %> -

You can confirm your account through the link below:

-<% end %> - -

<%= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/confirmation_instructions.text.erb b/app/views/devise/mailer/confirmation_instructions.text.erb new file mode 100644 index 0000000000..9f76edb76a --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.text.erb @@ -0,0 +1,9 @@ +Welcome, <%= @resource.name %>! + +<% if @resource.unconfirmed_email.present? %> +You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below: +<% else %> +You can confirm your account through the link below: +<% end %> + +<%= confirmation_url(@resource, confirmation_token: @token) %> From 3e3463218de17011bd4b29a7a27452d4f7652f35 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 22 Mar 2016 22:20:47 -0400 Subject: [PATCH 26/40] New confirmation email :fire: --- .../images/mailers/gitlab_header_logo.png | Bin 0 -> 7096 bytes .../images/mailers/gitlab_tanuki_2x.png | Bin 0 -> 2545 bytes app/assets/stylesheets/mailers/devise.scss | 134 ++++++++++++++++++ app/mailers/devise_mailer.rb | 2 + .../confirmation_instructions.html.haml | 16 +++ app/views/layouts/devise_mailer.html.haml | 34 +++++ config/application.rb | 2 +- .../mailers/previews/devise_mailer_preview.rb | 2 +- 8 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 app/assets/images/mailers/gitlab_header_logo.png create mode 100644 app/assets/images/mailers/gitlab_tanuki_2x.png create mode 100644 app/assets/stylesheets/mailers/devise.scss create mode 100644 app/views/devise/mailer/confirmation_instructions.html.haml create mode 100644 app/views/layouts/devise_mailer.html.haml diff --git a/app/assets/images/mailers/gitlab_header_logo.png b/app/assets/images/mailers/gitlab_header_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..35ca1860887f36346d7646ad9b51c79d8c811b60 GIT binary patch literal 7096 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 Hu0mjflsE?Y literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss new file mode 100644 index 0000000000..28611a5ec8 --- /dev/null +++ b/app/assets/stylesheets/mailers/devise.scss @@ -0,0 +1,134 @@ +// NOTE: This stylesheet is for the exclusive use of the `devise_mailer` layout +// used for Devise email templates, and _should not_ be included in any +// application stylesheets. +// +// Styles defined here are embedded directly into the resulting email HTML via +// the `premailer` gem. + +$body-background-color: #363636; +$message-background-color: #fafafa; + +$header-color: #6b4fbb; +$body-color: #444; +$cta-color: #e14329; +$footer-link-color: #7e7e7e; + +$font-family: Helvetica, Arial, sans-serif; + +body { + background-color: $body-background-color; + font-family: $font-family; + margin: 0; + padding: 0; +} + +table { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + + border: 0; + border-collapse: separate; + + &#wrapper { + background-color: $body-background-color; + width: 100%; + } + + &#header { + margin: 0 auto; + text-align: left; + width: 600px; + } + + &#body { + background-color: $message-background-color; + border: 1px solid #000; + border-radius: 4px; + margin: 0 auto; + width: 600px; + } + + &#footer { + color: $footer-link-color; + font-size: 14px; + text-align: center; + width: 100%; + } + + td { + &#body-container { + padding: 20px 40px; + } + } +} + +.center { + text-align: center; +} + +#logo { + border: none; + outline: none; + min-height: 88px; + width: 134px; +} + +#content { + h2 { + color: $header-color; + font-size: 30px; + font-weight: 400; + line-height: 34px; + margin-top: 0; + } + + p { + color: $body-color; + font-size: 17px; + line-height: 24px; + margin-bottom: 0; + } +} + +#cta { + border: 1px solid $cta-color; + border-radius: 3px; + display: inline-block; + margin: 20px 0; + padding: 12px 24px; + + a { + background-color: $message-background-color; + color: $cta-color; + display: inline-block; + text-decoration: none; + } +} + +#tanuki { + padding: 40px 0 0; + + img { + border: none; + outline: none; + width: 37px; + min-height: 36px; + } +} + +#tagline { + font-size: 22px; + font-weight: 100; + padding: 4px 0 40px; +} + +#social { + padding: 0 10px 20px; + width: 600px; + word-spacing: 20px; + + a { + color: $footer-link-color; + text-decoration: none; + } +} diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index b616add283..415f6e1288 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -1,4 +1,6 @@ class DeviseMailer < Devise::Mailer default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" default reply_to: Gitlab.config.gitlab.email_reply_to + + layout 'devise_mailer' end diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml new file mode 100644 index 0000000000..086bb8e083 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.haml @@ -0,0 +1,16 @@ +.center + - if @resource.unconfirmed_email.present? + #content + %h2= @resource.unconfirmed_email + %p Click the link below to confirm your email address. + #cta + = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) + - else + #content + - if Gitlab.com? + %h2 Thanks for signing up to GitLab! + - else + %h2 Welcome, #{@resource.name}! + %p To get started, click the link below to confirm your account. + #cta + = link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) diff --git a/app/views/layouts/devise_mailer.html.haml b/app/views/layouts/devise_mailer.html.haml new file mode 100644 index 0000000000..c258eafdd5 --- /dev/null +++ b/app/views/layouts/devise_mailer.html.haml @@ -0,0 +1,34 @@ +!!! 5 +%html + %head + %meta(content='text/html; charset=UTF-8' http-equiv='Content-Type') + = stylesheet_link_tag 'mailers/devise' + + %body + %table#wrapper + %tr + %td + %table#header + %td{valign: "top"} + = image_tag('mailers/gitlab_header_logo.png', id: 'logo', alt: 'GitLab Wordmark') + + %table#body + %tr + %td#body-container + = yield + + - if Gitlab.com? + %table#footer + %tr + %td#tanuki + = image_tag('mailers/gitlab_tanuki_2x.png', alt: 'GitLab Logo') + %tr + %td#tagline + Everyone can contribute + %tr + %td#social + = link_to 'Blog', 'https://about.gitlab.com/blog/' + = link_to 'Twitter', 'https://twitter.com/gitlab' + = link_to 'Facebook', 'https://www.facebook.com/gitlab/' + = link_to 'YouTube', 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg' + = link_to 'LinkedIn', 'https://www.linkedin.com/company/gitlab-com' diff --git a/config/application.rb b/config/application.rb index 0e5a77285e..de2bb08c97 100644 --- a/config/application.rb +++ b/config/application.rb @@ -80,7 +80,7 @@ module Gitlab config.assets.precompile << "*.png" config.assets.precompile << "print.css" config.assets.precompile << "notify.css" - config.assets.precompile << "mailers/repository_push_email.css" + config.assets.precompile << "mailers/*.css" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/spec/mailers/previews/devise_mailer_preview.rb index 8a4d6e7250..dc3062a433 100644 --- a/spec/mailers/previews/devise_mailer_preview.rb +++ b/spec/mailers/previews/devise_mailer_preview.rb @@ -1,6 +1,6 @@ class DeviseMailerPreview < ActionMailer::Preview def confirmation_instructions_for_signup - user = User.new(email: 'signup@example.com') + user = User.new(name: 'Jane Doe', email: 'signup@example.com') DeviseMailer.confirmation_instructions(user, 'faketoken', {}) end From 8083bc234a8ca9ef4eee2a4f0cb44e2f73cf33bc Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 14 Apr 2016 16:02:22 -0400 Subject: [PATCH 27/40] Don't remove IDs via premailer This makes it easier to debug style rules. --- config/initializers/premailer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb index b9176688bc..cb00d3cfe9 100644 --- a/config/initializers/premailer.rb +++ b/config/initializers/premailer.rb @@ -3,6 +3,6 @@ Premailer::Rails.config.merge!( generate_text_part: false, preserve_styles: true, remove_comments: true, - remove_ids: true, + remove_ids: false, remove_scripts: false ) From 91483bfeb718c7cede4dcc2372053195ea9a0c65 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 25 May 2016 16:56:44 -0400 Subject: [PATCH 28/40] Add CHANGELOG entry for redesigned confirmation emails [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 4a38505eee..1ad98fbc9f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.9.0 (unreleased) - Allow forking projects with restricted visibility level - Redesign navigation for project pages + - Redesign account and email confirmation emails - Use gitlab-shell v3.0.0 - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - Changed the Slack build message to use the singular duration if necessary (Aran Koning) From ce727ab6e6e75a760f72cfd9ce36092aeb41c7da Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 25 May 2016 17:13:20 -0400 Subject: [PATCH 29/40] Fix RuboCop --- spec/features/issues/filter_issues_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index bfbd06a29e..1f0594e6b0 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -186,7 +186,7 @@ describe 'Filter issues', feature: true do fill_in 'issue_search', with: 'testing' page.within '.issues-list' do - expect(page).to_not have_selector('.issue') + expect(page).not_to have_selector('.issue') end end end From ee0d3abcb8b3c8572be9dac1926c3f77f2636a9e Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 25 May 2016 15:04:21 -0600 Subject: [PATCH 30/40] Update state_machines-activerecord from 0.3.0 to 0.4.0. No changelog, see commits: https://github.com/state-machines/state_machines-activerecord/commits/master Includes Rails 5 support. --- Gemfile | 2 +- Gemfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index e854a31b42..c236f37e14 100644 --- a/Gemfile +++ b/Gemfile @@ -121,7 +121,7 @@ group :unicorn do end # State machine -gem "state_machines-activerecord", '~> 0.3.0' +gem "state_machines-activerecord", '~> 0.4.0' # Run events after state machine commits gem 'after_commit_queue' diff --git a/Gemfile.lock b/Gemfile.lock index 4533aa31d5..dbc0fd15ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -790,11 +790,11 @@ GEM activesupport (>= 4.0) sprockets (>= 3.0.0) state_machines (0.4.0) - state_machines-activemodel (0.3.0) - activemodel (~> 4.1) + state_machines-activemodel (0.4.0) + activemodel (>= 4.1, < 5.1) state_machines (>= 0.4.0) - state_machines-activerecord (0.3.0) - activerecord (~> 4.1) + state_machines-activerecord (0.4.0) + activerecord (>= 4.1, < 5.1) state_machines-activemodel (>= 0.3.0) stringex (2.5.2) systemu (2.6.5) @@ -1043,7 +1043,7 @@ DEPENDENCIES spring-commands-spinach (~> 1.1.0) spring-commands-teaspoon (~> 0.0.2) sprockets (~> 3.6.0) - state_machines-activerecord (~> 0.3.0) + state_machines-activerecord (~> 0.4.0) task_list (~> 1.0.2) teaspoon (~> 1.1.0) teaspoon-jasmine (~> 2.2.0) From 84fee47a763b6f1dda8b58e66c893cd8cf6dda8e Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 12 May 2016 08:23:31 -0500 Subject: [PATCH 31/40] Start scrolling tabs design --- app/assets/javascripts/layout_nav.js.coffee | 19 ++++++++++++++ app/assets/stylesheets/framework/nav.scss | 29 +++++++++++++++++++++ app/views/layouts/nav/_group.html.haml | 3 ++- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/layout_nav.js.coffee diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee new file mode 100644 index 0000000000..2f2f5aa85c --- /dev/null +++ b/app/assets/javascripts/layout_nav.js.coffee @@ -0,0 +1,19 @@ +class @LayoutNav + $(document).ready -> + $('#scrolling-tabs').on 'scroll', -> + cur = $(this).scrollLeft() + if cur == 0 + return + else + max = 289 + console.log "MAX:" + max + console.log "CUR:" + cur + if cur == max + $('.fa-arrow-right').addClass('end-scroll') + $('.nav-links').addClass('end-scroll') + else + $('.fa-arrow-right').removeClass('end-scroll') + $('.nav-links').removeClass('end-scroll') + return + $('#scrolling-tabs').trigger 'scroll' + return diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ee4f573f91..83c27eada3 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -256,6 +256,30 @@ display: none; } + .fa-arrow-right { + display: none; + position: absolute; + color: #7e7c7c; + bottom: 11px; + right: 0; + padding: 10px 10px 10px 25px; + background: -webkit-linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); + + &.end-scroll { + opacity: 0; + transition-duration: .3s; + } + } + + @media (max-width: 790px) { + margin-right: 19px; + + .fa-arrow-right { + display: block; + transition-duration: .3s; + } + } + li { a { @@ -276,6 +300,11 @@ color: $gl-icon-color; } } + + &.end-scroll { + margin-right: 0; + transition-duration: .3s; + } } } diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 3438005863..781c065125 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,6 +1,6 @@ = render 'layouts/nav/group_settings' -%ul.nav-links +%ul.nav-links#scrolling-tabs = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do = icon('group fw') @@ -35,3 +35,4 @@ = icon('users fw') %span Members + = icon('arrow-right') From 65a175bbd9d5e5a8f6937bcbee9505c67f168b38 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 16 May 2016 14:58:15 -0500 Subject: [PATCH 32/40] Remove arrows from mobile scroll fade out --- app/assets/javascripts/layout_nav.js.coffee | 24 ++---- app/assets/stylesheets/framework/nav.scss | 36 +++++---- app/assets/stylesheets/framework/sidebar.scss | 2 +- app/helpers/nav_helper.rb | 4 + app/views/layouts/nav/_group.html.haml | 75 ++++++++++--------- app/views/layouts/nav/_profile.html.haml | 3 +- 6 files changed, 75 insertions(+), 69 deletions(-) diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee index 2f2f5aa85c..4586702bc2 100644 --- a/app/assets/javascripts/layout_nav.js.coffee +++ b/app/assets/javascripts/layout_nav.js.coffee @@ -1,19 +1,11 @@ class @LayoutNav - $(document).ready -> + $ -> $('#scrolling-tabs').on 'scroll', -> - cur = $(this).scrollLeft() - if cur == 0 - return + currentPosition = $(this).scrollLeft() + return if currentPosition == 0 + if $('.nav-control').length + maxPosition = $(this)[0].scrollWidth - $(this).parent().width() + 59 else - max = 289 - console.log "MAX:" + max - console.log "CUR:" + cur - if cur == max - $('.fa-arrow-right').addClass('end-scroll') - $('.nav-links').addClass('end-scroll') - else - $('.fa-arrow-right').removeClass('end-scroll') - $('.nav-links').removeClass('end-scroll') - return - $('#scrolling-tabs').trigger 'scroll' - return + maxPosition = $(this)[0].scrollWidth - $(this).parent().width() + + $('.fade-out').toggleClass('end-scroll', currentPosition is maxPosition) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 83c27eada3..fe3a2aa26b 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -256,14 +256,18 @@ display: none; } - .fa-arrow-right { - display: none; + .fade-out { + opacity: 1; position: absolute; - color: #7e7c7c; - bottom: 11px; + bottom: 10px; right: 0; - padding: 10px 10px 10px 25px; + width: 43px; + height: 35px; + -webkit-transform: translateZ(0); background: -webkit-linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); + background: -o-linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); + background: -moz-linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); + background: linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); &.end-scroll { opacity: 0; @@ -272,10 +276,7 @@ } @media (max-width: 790px) { - margin-right: 19px; - - .fa-arrow-right { - display: block; + .fade-out { transition-duration: .3s; } } @@ -300,13 +301,20 @@ color: $gl-icon-color; } } - - &.end-scroll { - margin-right: 0; - transition-duration: .3s; - } } + .nav-control { + .fade-out { + right: 58px; + + @media (min-width: $screen-xs-max) { + right: 67px; + } + @media (max-width: $screen-xs-min) { + right: 0; + } + } + } } .page-with-layout-nav { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index f90d7a806d..67f491b6d9 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -324,7 +324,7 @@ .layout-nav { @media (max-width: $screen-xs-min) { - padding-right: 0;; + padding-right: 0; } @media (min-width: $screen-xs-min) and (max-width: $screen-md-min) { diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index fbb799eecd..597a12cd53 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -51,4 +51,8 @@ module NavHelper def layout_dropdown_class "controls-dropdown-visible" if current_user end + + def nav_control_class + "nav-control" if current_user + end end diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 781c065125..ba4a709265 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,38 +1,39 @@ -= render 'layouts/nav/group_settings' +%div{ class: nav_control_class } + = render 'layouts/nav/group_settings' -%ul.nav-links#scrolling-tabs - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do - = link_to group_path(@group), title: 'Home' do - = icon('group fw') - %span - Group - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: 'Activity' do - = icon('dashboard fw') - %span - Activity - = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group), title: 'Milestones' do - = icon('clock-o fw') - %span - Milestones - = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group), title: 'Issues' do - = icon('exclamation-circle fw') - %span - Issues - - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(issues.count) - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - = icon('tasks fw') - %span - Merge Requests - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(merge_requests.count) - = nav_link(controller: [:group_members]) do - = link_to group_group_members_path(@group), title: 'Members' do - = icon('users fw') - %span - Members - = icon('arrow-right') + %ul.nav-links#scrolling-tabs + = nav_link(path: 'groups#show', html_options: {class: 'home'}) do + = link_to group_path(@group), title: 'Home' do + = icon('group fw') + %span + Group + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + = icon('dashboard fw') + %span + Activity + = nav_link(controller: [:group, :milestones]) do + = link_to group_milestones_path(@group), title: 'Milestones' do + = icon('clock-o fw') + %span + Milestones + = nav_link(path: 'groups#issues') do + = link_to issues_group_path(@group), title: 'Issues' do + = icon('exclamation-circle fw') + %span + Issues + - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute + %span.badge.count= number_with_delimiter(issues.count) + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do + = icon('tasks fw') + %span + Merge Requests + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute + %span.badge.count= number_with_delimiter(merge_requests.count) + = nav_link(controller: [:group_members]) do + = link_to group_group_members_path(@group), title: 'Members' do + = icon('users fw') + %span + Members + .fade-out diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index d730840d63..d3d733d599 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,4 +1,4 @@ -%ul.nav-links +%ul.nav-links#scrolling-tabs = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do = icon('user fw') @@ -47,3 +47,4 @@ = icon('history fw') %span Audit Log + .fade-out From e8fcd4641a2ef67df2e94b7f161c1a061cd5eb77 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 18 May 2016 09:47:54 -0500 Subject: [PATCH 33/40] Check screen width before increasing max position --- app/assets/javascripts/layout_nav.js.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee index 4586702bc2..613243427c 100644 --- a/app/assets/javascripts/layout_nav.js.coffee +++ b/app/assets/javascripts/layout_nav.js.coffee @@ -3,9 +3,7 @@ class @LayoutNav $('#scrolling-tabs').on 'scroll', -> currentPosition = $(this).scrollLeft() return if currentPosition == 0 - if $('.nav-control').length - maxPosition = $(this)[0].scrollWidth - $(this).parent().width() + 59 - else - maxPosition = $(this)[0].scrollWidth - $(this).parent().width() + maxPosition = $(this)[0].scrollWidth - $(this).parent().width() + maxPosition += 59 if $('.nav-control').length and window.innerWidth > 480 $('.fade-out').toggleClass('end-scroll', currentPosition is maxPosition) From 7876d27db8b330f3c5e306f1c2e657a8b0df8819 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 18 May 2016 10:48:10 -0500 Subject: [PATCH 34/40] Move fade out div --- app/assets/stylesheets/framework/nav.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index fe3a2aa26b..9f5f2f81b8 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -259,10 +259,10 @@ .fade-out { opacity: 1; position: absolute; - bottom: 10px; + bottom: 16px; right: 0; width: 43px; - height: 35px; + height: 30px; -webkit-transform: translateZ(0); background: -webkit-linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); background: -o-linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); From 77c7fb1469446c76c84ce5883d99e0e3debaf515 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 18 May 2016 11:38:12 -0500 Subject: [PATCH 35/40] Fix coffeescript; remove fade out div so area is clickable; remove scrollbar on chrome --- app/assets/javascripts/layout_nav.js.coffee | 6 ++++-- app/assets/stylesheets/framework/nav.scss | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee index 613243427c..2f14e3f35a 100644 --- a/app/assets/javascripts/layout_nav.js.coffee +++ b/app/assets/javascripts/layout_nav.js.coffee @@ -2,8 +2,10 @@ class @LayoutNav $ -> $('#scrolling-tabs').on 'scroll', -> currentPosition = $(this).scrollLeft() - return if currentPosition == 0 + return if currentPosition is 0 + mobileScreenWidth = 480 + controlBtnWidth = $('.controls').width() maxPosition = $(this)[0].scrollWidth - $(this).parent().width() - maxPosition += 59 if $('.nav-control').length and window.innerWidth > 480 + maxPosition += controlBtnWidth if $('.nav-control').length and $(window).width() > mobileScreenWidth $('.fade-out').toggleClass('end-scroll', currentPosition is maxPosition) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 9f5f2f81b8..bf5981bef8 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -257,6 +257,7 @@ } .fade-out { + visibility: visible; opacity: 1; position: absolute; bottom: 16px; @@ -270,6 +271,7 @@ background: linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); &.end-scroll { + visibility: hidden; opacity: 0; transition-duration: .3s; } From 3c3402b0ae4c2f70cc421d97ad9605a9abeb9398 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 24 May 2016 11:30:53 -0500 Subject: [PATCH 36/40] Add fade out to left side of layout nav; remove dropdown nav helper; remove controls button from mobile --- app/assets/javascripts/layout_nav.js.coffee | 6 +- app/assets/stylesheets/framework/nav.scss | 64 ++++++++++----------- app/helpers/nav_helper.rb | 4 -- app/views/layouts/_page.html.haml | 2 +- app/views/layouts/nav/_group.html.haml | 3 +- app/views/layouts/nav/_profile.html.haml | 3 +- 6 files changed, 38 insertions(+), 44 deletions(-) diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee index 2f14e3f35a..43dc941a82 100644 --- a/app/assets/javascripts/layout_nav.js.coffee +++ b/app/assets/javascripts/layout_nav.js.coffee @@ -1,11 +1,13 @@ class @LayoutNav $ -> + $('.fade-left').addClass('end-scroll') $('#scrolling-tabs').on 'scroll', -> currentPosition = $(this).scrollLeft() - return if currentPosition is 0 + $('.fade-left').toggleClass('end-scroll', currentPosition is 0) + mobileScreenWidth = 480 controlBtnWidth = $('.controls').width() maxPosition = $(this)[0].scrollWidth - $(this).parent().width() maxPosition += controlBtnWidth if $('.nav-control').length and $(window).width() > mobileScreenWidth - $('.fade-out').toggleClass('end-scroll', currentPosition is maxPosition) + $('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index bf5981bef8..260127e311 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,3 +1,24 @@ +@mixin fade($gradient-direction) { + visibility: visible; + opacity: 1; + position: absolute; + bottom: 16px; + width: 43px; + height: 30px; + transition-duration: .3s; + -webkit-transform: translateZ(0); + background: -webkit-linear-gradient($gradient-direction, rgba(250, 250, 250, 0.4), $background-color 45%); + background: -o-linear-gradient($gradient-direction, rgba(250, 250, 250, 0.4), $background-color 45%); + background: -moz-linear-gradient($gradient-direction, rgba(250, 250, 250, 0.4), $background-color 45%); + background: linear-gradient($gradient-direction, rgba(250, 250, 250, 0.4), $background-color 45%); + + &.end-scroll { + visibility: hidden; + opacity: 0; + transition-duration: .3s; + } +} + .nav-links { padding: 0; margin: 0; @@ -210,12 +231,7 @@ padding: 7px 0 0; @media (max-width: $screen-xs-min) { - float: none; - padding: 0 9px; - - .dropdown-new { - width: 100%; - } + display: none; } i { @@ -256,31 +272,14 @@ display: none; } - .fade-out { - visibility: visible; - opacity: 1; - position: absolute; - bottom: 16px; + .fade-right { + @include fade(left); right: 0; - width: 43px; - height: 30px; - -webkit-transform: translateZ(0); - background: -webkit-linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); - background: -o-linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); - background: -moz-linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); - background: linear-gradient(left, rgba(250, 250, 250, 0.4), $background-color 45%); - - &.end-scroll { - visibility: hidden; - opacity: 0; - transition-duration: .3s; - } } - @media (max-width: 790px) { - .fade-out { - transition-duration: .3s; - } + .fade-left { + @include fade(right); + left: 0; } li { @@ -306,7 +305,8 @@ } .nav-control { - .fade-out { + + .fade-right { right: 58px; @media (min-width: $screen-xs-max) { @@ -322,12 +322,6 @@ .page-with-layout-nav { margin-top: $header-height + 2; - &.controls-dropdown-visible { - @media (max-width: $screen-xs-min) { - margin-top: 96px; - } - } - .right-sidebar { top: ($header-height * 2) + 2; } diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 597a12cd53..f685e54753 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -48,10 +48,6 @@ module NavHelper "page-with-layout-nav" if defined?(nav) && nav end - def layout_dropdown_class - "controls-dropdown-visible" if current_user - end - def nav_control_class "nav-control" if current_user end diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 5be0b546a6..1e961853c7 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -25,7 +25,7 @@ .layout-nav .container-fluid = render "layouts/nav/#{nav}" - .content-wrapper{ class: "#{layout_nav_class} #{layout_dropdown_class}" } + .content-wrapper{ class: "#{layout_nav_class}" } = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index ba4a709265..9b8319c51c 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -2,6 +2,7 @@ = render 'layouts/nav/group_settings' %ul.nav-links#scrolling-tabs + .fade-left = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do = icon('group fw') @@ -36,4 +37,4 @@ = icon('users fw') %span Members - .fade-out + .fade-right diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index d3d733d599..a31584c7f8 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,4 +1,5 @@ %ul.nav-links#scrolling-tabs + .fade-left = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do = icon('user fw') @@ -47,4 +48,4 @@ = icon('history fw') %span Audit Log - .fade-out + .fade-right From 1a1eaf481a19cbeedaf5b6137eef73031f33e3d2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 24 May 2016 16:06:40 -0500 Subject: [PATCH 37/40] Refactor nav links css --- app/assets/stylesheets/framework/nav.scss | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 260127e311..182dd05225 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -274,7 +274,14 @@ .fade-right { @include fade(left); - right: 0; + right: 58px; + + @media (min-width: $screen-xs-max) { + right: 67px; + } + @media (max-width: $screen-xs-min) { + right: 0; + } } .fade-left { @@ -303,20 +310,6 @@ } } } - - .nav-control { - - .fade-right { - right: 58px; - - @media (min-width: $screen-xs-max) { - right: 67px; - } - @media (max-width: $screen-xs-min) { - right: 0; - } - } - } } .page-with-layout-nav { From 1965ec6324d7a67aec485b98030794657624d3ec Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 24 May 2016 17:14:14 -0500 Subject: [PATCH 38/40] Add fade to project nav; fix bug with controls --- app/assets/stylesheets/framework/nav.scss | 22 ++- app/views/layouts/nav/_project.html.haml | 194 +++++++++++----------- 2 files changed, 113 insertions(+), 103 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 182dd05225..59e9a036f7 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -274,14 +274,7 @@ .fade-right { @include fade(left); - right: 58px; - - @media (min-width: $screen-xs-max) { - right: 67px; - } - @media (max-width: $screen-xs-min) { - right: 0; - } + right: 0; } .fade-left { @@ -310,6 +303,19 @@ } } } + + .nav-control { + .fade-right { + right: 58px; + + @media (min-width: $screen-xs-max) { + right: 67px; + } + @media (max-width: $screen-xs-min) { + right: 0; + } + } + } } .page-with-layout-nav { diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 33ba654bbe..bffa583fda 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -19,113 +19,117 @@ data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do Leave Project -%ul.nav-links - = nav_link(path: 'projects#show', html_options: {class: 'home'}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do - = icon('bookmark fw') - %span - Project - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - = icon('dashboard fw') - %span - Activity - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do - = icon('files-o fw') +%div{ class: nav_control_class } + %ul.nav-links#scrolling-tabs + .fade-left + = nav_link(path: 'projects#show', html_options: {class: 'home'}) do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do + = icon('bookmark fw') %span - Files - - - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - = icon('history fw') + Project + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + = icon('dashboard fw') %span - Commits + Activity + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do + = icon('files-o fw') + %span + Files - - if project_nav_tab? :pipelines - = nav_link(controller: :pipelines) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - = icon('ship fw') - %span - Pipelines - %span.badge.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count) + - if project_nav_tab? :commits + = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + = icon('history fw') + %span + Commits - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - = icon('cubes fw') - %span - Builds - %span.badge.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) + - if project_nav_tab? :pipelines + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + = icon('ship fw') + %span + Pipelines + %span.badge.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count) - - if project_nav_tab? :container_registry - = nav_link(controller: %w(container_registry)) do - = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do - = icon('hdd-o fw') - %span - Container Registry + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + = icon('cubes fw') + %span + Builds + %span.badge.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) - - if project_nav_tab? :graphs - = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do - = icon('area-chart fw') - %span - Graphs + - if project_nav_tab? :container_registry + = nav_link(controller: %w(container_registry)) do + = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do + = icon('hdd-o fw') + %span + Container Registry - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - = icon('clock-o fw') - %span - Milestones + - if project_nav_tab? :graphs + = nav_link(controller: %w(graphs)) do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do + = icon('area-chart fw') + %span + Graphs - - if project_nav_tab? :issues - = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do - = icon('exclamation-circle fw') - %span - Issues - - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + = icon('clock-o fw') + %span + Milestones - - if project_nav_tab? :merge_requests - = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - = icon('tasks fw') - %span - Merge Requests - %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) + - if project_nav_tab? :issues + = nav_link(controller: :issues) do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do + = icon('exclamation-circle fw') + %span + Issues + - if @project.default_issues_tracker? + %span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - = icon('tags fw') - %span - Labels + - if project_nav_tab? :merge_requests + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + = icon('tasks fw') + %span + Merge Requests + %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - = icon('book fw') - %span - Wiki + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + = icon('tags fw') + %span + Labels - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do - = icon('clipboard fw') - %span - Snippets + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + = icon('book fw') + %span + Wiki - -# Global shortcut to network page for compatibility - - if project_nav_tab? :network + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do + = icon('clipboard fw') + %span + Snippets + + -# Global shortcut to network page for compatibility + - if project_nav_tab? :network + %li.hidden + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do + Network + + -# Shortcut to create a new issue %li.hidden - = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do - Network + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do + Create a new issue - -# Shortcut to create a new issue - %li.hidden - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do - Create a new issue + .fade-right From 79f890ca8549132699bc8169eff73b5c7ff25699 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 25 May 2016 11:42:43 -0500 Subject: [PATCH 39/40] Add fade out to sub nav --- app/assets/javascripts/layout_nav.js.coffee | 8 ++-- app/assets/stylesheets/framework/nav.scss | 50 +++++++++++++++------ app/views/layouts/nav/_group.html.haml | 2 +- app/views/layouts/nav/_profile.html.haml | 2 +- app/views/layouts/nav/_project.html.haml | 2 +- app/views/shared/_event_filter.html.haml | 4 +- 6 files changed, 46 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee index 43dc941a82..3c39ec32f2 100644 --- a/app/assets/javascripts/layout_nav.js.coffee +++ b/app/assets/javascripts/layout_nav.js.coffee @@ -1,13 +1,13 @@ class @LayoutNav $ -> $('.fade-left').addClass('end-scroll') - $('#scrolling-tabs').on 'scroll', -> + $('.scrolling-tabs').scroll (event) -> + el = $(event.target) currentPosition = $(this).scrollLeft() - $('.fade-left').toggleClass('end-scroll', currentPosition is 0) - mobileScreenWidth = 480 controlBtnWidth = $('.controls').width() maxPosition = $(this)[0].scrollWidth - $(this).parent().width() maxPosition += controlBtnWidth if $('.nav-control').length and $(window).width() > mobileScreenWidth - $('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) + el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0) + el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 59e9a036f7..fdd60d85db 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,4 +1,4 @@ -@mixin fade($gradient-direction) { +@mixin fade($gradient-direction, $rgba, $gradient-color) { visibility: visible; opacity: 1; position: absolute; @@ -7,10 +7,10 @@ height: 30px; transition-duration: .3s; -webkit-transform: translateZ(0); - background: -webkit-linear-gradient($gradient-direction, rgba(250, 250, 250, 0.4), $background-color 45%); - background: -o-linear-gradient($gradient-direction, rgba(250, 250, 250, 0.4), $background-color 45%); - background: -moz-linear-gradient($gradient-direction, rgba(250, 250, 250, 0.4), $background-color 45%); - background: linear-gradient($gradient-direction, rgba(250, 250, 250, 0.4), $background-color 45%); + background: -webkit-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: -o-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%); &.end-scroll { visibility: hidden; @@ -19,6 +19,16 @@ } } +@mixin scrolling-links() { + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + &::-webkit-scrollbar { + display: none; + } +} + .nav-links { padding: 0; margin: 0; @@ -262,23 +272,17 @@ } .nav-links { + @include scrolling-links(); border-bottom: none; height: 51px; - white-space: nowrap; - overflow-x: auto; - overflow-y: hidden; - -webkit-overflow-scrolling: touch; - &::-webkit-scrollbar { - display: none; - } .fade-right { - @include fade(left); + @include fade(left, rgba(250, 250, 250, 0.4), $background-color); right: 0; } .fade-left { - @include fade(right); + @include fade(right, rgba(250, 250, 250, 0.4), $background-color); left: 0; } @@ -318,6 +322,24 @@ } } +.nav-block { + position: relative; + + .nav-links { + @include scrolling-links(); + + .fade-right { + @include fade(left, rgba(255, 255, 255, 0.4), $white-light); + right: 0; + } + + .fade-left { + @include fade(right, rgba(255, 255, 255, 0.4), $white-light); + left: 0; + } + } +} + .page-with-layout-nav { margin-top: $header-height + 2; diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 9b8319c51c..de15add361 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,7 +1,7 @@ %div{ class: nav_control_class } = render 'layouts/nav/group_settings' - %ul.nav-links#scrolling-tabs + %ul.nav-links.scrolling-tabs .fade-left = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index a31584c7f8..2efc6c48a4 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,4 +1,4 @@ -%ul.nav-links#scrolling-tabs +%ul.nav-links.scrolling-tabs .fade-left = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index bffa583fda..087b747270 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -20,7 +20,7 @@ Leave Project %div{ class: nav_control_class } - %ul.nav-links#scrolling-tabs + %ul.nav-links.scrolling-tabs .fade-left = nav_link(path: 'projects#show', html_options: {class: 'home'}) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index c38d9313db..3005500221 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,5 +1,7 @@ -%ul.nav-links.event-filter +%ul.nav-links.event-filter.scrolling-tabs + .fade-left = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' + .fade-right From ec6d456b5023b4a0700a4c9ad8a17ed64b0470e6 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 25 May 2016 17:12:39 -0500 Subject: [PATCH 40/40] Change to xs breakpoint, layout_nav updates --- app/assets/javascripts/layout_nav.js.coffee | 17 +++++++++-------- app/assets/stylesheets/framework/nav.scss | 5 ++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee index 3c39ec32f2..6adac6dac9 100644 --- a/app/assets/javascripts/layout_nav.js.coffee +++ b/app/assets/javascripts/layout_nav.js.coffee @@ -1,13 +1,14 @@ class @LayoutNav $ -> $('.fade-left').addClass('end-scroll') - $('.scrolling-tabs').scroll (event) -> - el = $(event.target) - currentPosition = $(this).scrollLeft() - mobileScreenWidth = 480 + $('.scrolling-tabs').on 'scroll', (event) -> + $this = $(this) + $el = $(event.target) + currentPosition = $this.scrollLeft() + size = bp.getBreakpointSize() controlBtnWidth = $('.controls').width() - maxPosition = $(this)[0].scrollWidth - $(this).parent().width() - maxPosition += controlBtnWidth if $('.nav-control').length and $(window).width() > mobileScreenWidth + maxPosition = $this.get(0).scrollWidth - $this.parent().width() + maxPosition += controlBtnWidth if size isnt 'xs' and $('.nav-control').length - el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0) - el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) + $el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0) + $el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index fdd60d85db..adfe554070 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -2,7 +2,7 @@ visibility: visible; opacity: 1; position: absolute; - bottom: 16px; + bottom: 12px; width: 43px; height: 30px; transition-duration: .3s; @@ -240,7 +240,7 @@ float: right; padding: 7px 0 0; - @media (max-width: $screen-xs-min) { + @media (max-width: $screen-xs-max) { display: none; } @@ -310,7 +310,6 @@ .nav-control { .fade-right { - right: 58px; @media (min-width: $screen-xs-max) { right: 67px;