Commit 1fa7671f authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'improve-user-tabs' into 'master'

Add routes and actions for dynamic tab loading. Closes #13588 and #13584



See merge request !2961
parents 89270f77 d4981e9b
...@@ -107,9 +107,6 @@ class Dispatcher ...@@ -107,9 +107,6 @@ class Dispatcher
new ProjectFork() new ProjectFork()
when 'projects:artifacts:browse' when 'projects:artifacts:browse'
new BuildArtifacts() new BuildArtifacts()
when 'users:show'
new User()
new Activities()
switch path.first() switch path.first()
when 'admin' when 'admin'
......
@Pager = @Pager =
init: (@limit = 0, preload, @disable = false) -> init: (@limit = 0, preload, @disable = false) ->
@loading = $(".loading") @loading = $('.loading').first()
if preload if preload
@offset = 0 @offset = 0
@getOld() @getOld()
......
class @User class @User
constructor: -> constructor: (@opts) ->
$('.profile-groups-avatars').tooltip("placement": "top") $('.profile-groups-avatars').tooltip("placement": "top")
new ProjectsList() new ProjectsList()
@initTabs()
$('.hide-project-limit-message').on 'click', (e) -> $('.hide-project-limit-message').on 'click', (e) ->
path = '/' path = '/'
$.cookie('hide_project_limit_message', 'false', { path: path }) $.cookie('hide_project_limit_message', 'false', { path: path })
$(@).parents('.project-limit-message').remove() $(@).parents('.project-limit-message').remove()
e.preventDefault() e.preventDefault()
initTabs: ->
new UserTabs(
parentEl: '.user-profile'
action: @opts.action
)
# UserTabs
#
# Handles persisting and restoring the current tab selection and lazily-loading
# content on the Users#show page.
#
# ### Example Markup
#
# <ul class="nav-links">
# <li class="activity-tab active">
# <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
# Activity
# </a>
# </li>
# <li class="groups-tab">
# <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
# Groups
# </a>
# </li>
# <li class="contributed-tab">
# <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
# Contributed projects
# </a>
# </li>
# <li class="projects-tab">
# <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
# Personal projects
# </a>
# </li>
# </ul>
#
# <div class="tab-content">
# <div class="tab-pane" id="activity">
# Activity Content
# </div>
# <div class="tab-pane" id="groups">
# Groups Content
# </div>
# <div class="tab-pane" id="contributed">
# Contributed projects content
# </div>
# <div class="tab-pane" id="projects">
# Projects content
# </div>
# </div>
#
# <div class="loading-status">
# <div class="loading">
# Loading Animation
# </div>
# </div>
#
class @UserTabs
constructor: (opts) ->
{
@action = 'activity'
@defaultAction = 'activity'
@parentEl = $(document)
} = opts
# Make jQuery object if selector is provided
@parentEl = $(@parentEl) if typeof @parentEl is 'string'
# Store the `location` object, allowing for easier stubbing in tests
@_location = location
# Set tab states
@loaded = {}
for item in @parentEl.find('.nav-links a')
@loaded[$(item).attr 'data-action'] = false
# Actions
@actions = Object.keys @loaded
@bindEvents()
# Set active tab
@action = @defaultAction if @action is 'show'
@activateTab(@action)
bindEvents: ->
# Toggle event listeners
@parentEl
.off 'shown.bs.tab', '.nav-links a[data-toggle="tab"]'
.on 'shown.bs.tab', '.nav-links a[data-toggle="tab"]', @tabShown
tabShown: (event) =>
$target = $(event.target)
action = $target.data('action')
source = $target.attr('href')
@setTab(source, action)
@setCurrentAction(action)
activateTab: (action) ->
@parentEl.find(".nav-links .#{action}-tab a").tab('show')
setTab: (source, action) ->
return if @loaded[action] is true
if action is 'activity'
@loadActivities(source)
if action in ['groups', 'contributed', 'projects']
@loadTab(source, action)
loadTab: (source, action) ->
$.ajax
beforeSend: => @toggleLoading(true)
complete: => @toggleLoading(false)
dataType: 'json'
type: 'GET'
url: "#{source}.json"
success: (data) =>
tabSelector = 'div#' + action
@parentEl.find(tabSelector).html(data.html)
@loaded[action] = true
loadActivities: (source) ->
return if @loaded['activity'] is true
$calendarWrap = @parentEl.find('.user-calendar')
$calendarWrap.load($calendarWrap.data('href'))
new Activities()
@loaded['activity'] = true
toggleLoading: (status) ->
@parentEl.find('.loading-status .loading').toggle(status)
setCurrentAction: (action) ->
# Remove possible actions from URL
regExp = new RegExp('\/(' + @actions.join('|') + ')(\.html)?\/?$')
new_state = @_location.pathname
new_state = new_state.replace(/\/+$/, "") # remove trailing slashes
new_state = new_state.replace(regExp, '')
# Append the new action if we're on a tab other than 'activity'
unless action == @defaultAction
new_state += "/#{action}"
# Ensure parameters and hash come along for the ride
new_state += @_location.search + @_location.hash
history.replaceState {turbolinks: true, url: new_state}, document.title, new_state
new_state
...@@ -3,13 +3,6 @@ class UsersController < ApplicationController ...@@ -3,13 +3,6 @@ class UsersController < ApplicationController
before_action :set_user before_action :set_user
def show def show
@contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
@projects = PersonalProjectsFinder.new(@user).execute(current_user)
@projects = @projects.page(params[:page]).per(PER_PAGE)
@groups = @user.groups.order_id_desc
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -25,6 +18,45 @@ class UsersController < ApplicationController ...@@ -25,6 +18,45 @@ class UsersController < ApplicationController
end end
end end
def groups
load_groups
respond_to do |format|
format.html { render 'show' }
format.json do
render json: {
html: view_to_html_string("shared/groups/_list", groups: @groups)
}
end
end
end
def projects
load_projects
respond_to do |format|
format.html { render 'show' }
format.json do
render json: {
html: view_to_html_string("shared/projects/_list", projects: @projects, remote: true)
}
end
end
end
def contributed
load_contributed_projects
respond_to do |format|
format.html { render 'show' }
format.json do
render json: {
html: view_to_html_string("shared/projects/_list", projects: @contributed_projects)
}
end
end
end
def calendar def calendar
calendar = contributions_calendar calendar = contributions_calendar
@timestamps = calendar.timestamps @timestamps = calendar.timestamps
...@@ -69,6 +101,20 @@ class UsersController < ApplicationController ...@@ -69,6 +101,20 @@ class UsersController < ApplicationController
limit_recent(20, params[:offset]) limit_recent(20, params[:offset])
end end
def load_projects
@projects =
PersonalProjectsFinder.new(@user).execute(current_user)
.page(params[:page]).per(PER_PAGE)
end
def load_contributed_projects
@contributed_projects = contributed_projects.joined(@user)
end
def load_groups
@groups = @user.groups.order_id_desc
end
def projects_for_current_user def projects_for_current_user
ProjectsFinder.new.execute(current_user) ProjectsFinder.new.execute(current_user)
end end
......
- if groups.any?
%ul.content-list
- groups.each_with_index do |group, i|
= render "shared/groups/group", group: group
- else
%h3 No groups found
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
- ci = false unless local_assigns[:ci] == true - ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true
%ul.projects-list.content-list %ul.projects-list.content-list
- if projects.any? - if projects.any?
...@@ -21,7 +22,7 @@ ...@@ -21,7 +22,7 @@
#{projects_limit} of #{pluralize(projects.count, 'project')} displayed. #{projects_limit} of #{pluralize(projects.count, 'project')} displayed.
= link_to '#', class: 'js-expand' do = link_to '#', class: 'js-expand' do
Show all Show all
= paginate projects, theme: "gitlab" if projects.respond_to? :total_pages = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
- else - else
.nothing-here-block No projects found .nothing-here-block No projects found
......
...@@ -8,117 +8,110 @@ ...@@ -8,117 +8,110 @@
= render 'shared/show_aside' = render 'shared/show_aside'
.cover-block .user-profile
.cover-controls .cover-block
- if @user == current_user .cover-controls
= link_to profile_path, class: 'btn btn-gray' do - if @user == current_user
= icon('pencil') = link_to profile_path, class: 'btn btn-gray' do
- elsif current_user = icon('pencil')
%span.report-abuse - elsif current_user
- if @user.abuse_report %span.report-abuse
%button.btn.btn-danger{ title: 'Already reported for abuse', - if @user.abuse_report
data: { toggle: 'tooltip', placement: 'left', container: 'body' }} %button.btn.btn-danger{ title: 'Already reported for abuse',
= icon('exclamation-circle') data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
- else = icon('exclamation-circle')
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', - else
title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
= icon('exclamation-circle') title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
- if current_user = icon('exclamation-circle')
&nbsp; - if current_user
= link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do &nbsp;
= icon('rss') = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
= icon('rss')
.avatar-holder
= link_to avatar_icon(@user, 400), target: '_blank' do .avatar-holder
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' = link_to avatar_icon(@user, 400), target: '_blank' do
.cover-title = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
= @user.name .cover-title
= @user.name
.cover-desc
%span.middle-dot-divider .cover-desc
@#{@user.username} %span.middle-dot-divider
%span.middle-dot-divider @#{@user.username}
Member since #{@user.created_at.to_s(:medium)} %span.middle-dot-divider
Member since #{@user.created_at.to_s(:medium)}
- if @user.bio.present?
- if @user.bio.present?
.cover-desc
%p.profile-user-bio
= @user.bio
.cover-desc .cover-desc
%p.profile-user-bio - unless @user.public_email.blank?
= @user.bio .profile-link-holder.middle-dot-divider
= link_to @user.public_email, "mailto:#{@user.public_email}"
.cover-desc - unless @user.skype.blank?
- unless @user.public_email.blank? .profile-link-holder.middle-dot-divider
.profile-link-holder.middle-dot-divider = link_to "skype:#{@user.skype}", title: "Skype" do
= link_to @user.public_email, "mailto:#{@user.public_email}" = icon('skype')
- unless @user.skype.blank? - unless @user.linkedin.blank?
.profile-link-holder.middle-dot-divider .profile-link-holder.middle-dot-divider
= link_to "skype:#{@user.skype}", title: "Skype" do = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
= icon('skype') = icon('linkedin-square')
- unless @user.linkedin.blank? - unless @user.twitter.blank?
.profile-link-holder.middle-dot-divider .profile-link-holder.middle-dot-divider
= link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
= icon('linkedin-square') = icon('twitter-square')
- unless @user.twitter.blank? - unless @user.website_url.blank?
.profile-link-holder.middle-dot-divider .profile-link-holder.middle-dot-divider
= link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do = link_to @user.short_website_url, @user.full_website_url
= icon('twitter-square') - unless @user.location.blank?
- unless @user.website_url.blank? .profile-link-holder.middle-dot-divider
.profile-link-holder.middle-dot-divider = icon('map-marker')
= link_to @user.short_website_url, @user.full_website_url = @user.location
- unless @user.location.blank?
.profile-link-holder.middle-dot-divider %ul.nav-links.center.user-profile-nav
= icon('map-marker') %li.activity-tab
= @user.location = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
Activity
%ul.nav-links.center %li.groups-tab
%li.active = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
- if @groups.any?
%li
= link_to "#groups", 'data-toggle' => 'tab' do
Groups Groups
- if @contributed_projects.present? %li.contributed-tab
%li = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
= link_to "#contributed", 'data-toggle' => 'tab' do
Contributed projects Contributed projects
- if @projects.present? %li.projects-tab
%li = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
= link_to "#personal", 'data-toggle' => 'tab' do
Personal projects Personal projects
%div{ class: container_class } %div{ class: container_class }
.tab-content .tab-content
.tab-pane.active#activity #activity.tab-pane
.gray-content-block.white.second-block .gray-content-block.white.second-block
%div{ class: container_class } %div{ class: container_class }
.user-calendar .user-calendar{data: {href: user_calendar_path}}
%h4.center.light %h4.center.light
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
.user-calendar-activities .user-calendar-activities
.content_list{ data: {href: user_path} }
= spinner
.content_list #groups.tab-pane
= spinner - # This tab is always loaded via AJAX
#contributed.contributed-projects.tab-pane
- # This tab is always loaded via AJAX
- if @groups.any? #projects.tab-pane
.tab-pane#groups - # This tab is always loaded via AJAX
%ul.content-list
- @groups.each do |group| .loading-status
= render 'shared/groups/group', group: group = spinner
- if @contributed_projects.present?
.tab-pane#contributed
.contributed-projects
= render 'shared/projects/list',
projects: @contributed_projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: true
- if @projects.present?
.tab-pane#personal
.personal-projects
= render 'shared/projects/list',
projects: @projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: true
:javascript :javascript
$(".user-calendar").load("#{user_calendar_path}"); var userProfile;
userProfile = new User({
action: "#{controller.action_name}"
});
...@@ -332,6 +332,15 @@ Rails.application.routes.draw do ...@@ -332,6 +332,15 @@ Rails.application.routes.draw do
get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities, get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities,
constraints: { username: /.*/ } constraints: { username: /.*/ }
get 'u/:username/groups' => 'users#groups', as: :user_groups,
constraints: { username: /.*/ }
get 'u/:username/projects' => 'users#projects', as: :user_projects,
constraints: { username: /.*/ }
get 'u/:username/contributed' => 'users#contributed', as: :user_contributed_projects,
constraints: { username: /.*/ }
get '/u/:username' => 'users#show', as: :user, get '/u/:username' => 'users#show', as: :user,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
......
...@@ -26,4 +26,20 @@ module SharedUser ...@@ -26,4 +26,20 @@ module SharedUser
step 'I have no ssh keys' do step 'I have no ssh keys' do
@user.keys.delete_all @user.keys.delete_all
end end
step 'I click on "Personal projects" tab' do
page.within '.nav-links' do
click_link 'Personal projects'
end
expect(page).to have_css('.tab-content #projects.active')
end
step 'I click on "Contributed projects" tab' do
page.within '.nav-links' do
click_link 'Contributed projects'
end
expect(page).to have_css('.tab-content #contributed.active')
end
end end
...@@ -5,10 +5,12 @@ Feature: User ...@@ -5,10 +5,12 @@ Feature: User
# Signed out # Signed out
@javascript
Scenario: I visit user "John Doe" page while not signed in when he owns a public project Scenario: I visit user "John Doe" page while not signed in when he owns a public project
Given "John Doe" owns internal project "Internal" Given "John Doe" owns internal project "Internal"
And "John Doe" owns public project "Community" And "John Doe" owns public project "Community"
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should not see project "Enterprise" And I should not see project "Enterprise"
And I should not see project "Internal" And I should not see project "Internal"
...@@ -16,28 +18,34 @@ Feature: User ...@@ -16,28 +18,34 @@ Feature: User
# Signed in as someone else # Signed in as someone else
@javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he owns a public project Scenario: I visit user "John Doe" page while signed in as someone else when he owns a public project
Given "John Doe" owns public project "Community" Given "John Doe" owns public project "Community"
And "John Doe" owns internal project "Internal" And "John Doe" owns internal project "Internal"
And I sign in as a user And I sign in as a user
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should not see project "Enterprise" And I should not see project "Enterprise"
And I should see project "Internal" And I should see project "Internal"
And I should see project "Community" And I should see project "Community"
@javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a public project Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a public project
Given "John Doe" owns internal project "Internal" Given "John Doe" owns internal project "Internal"
And I sign in as a user And I sign in as a user
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should not see project "Enterprise" And I should not see project "Enterprise"
And I should see project "Internal" And I should see project "Internal"
And I should not see project "Community" And I should not see project "Community"
@javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a project I can see Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a project I can see
Given I sign in as a user Given I sign in as a user
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should not see project "Enterprise" And I should not see project "Enterprise"
And I should not see project "Internal" And I should not see project "Internal"
...@@ -45,19 +53,23 @@ Feature: User ...@@ -45,19 +53,23 @@ Feature: User
# Signed in as the user himself # Signed in as the user himself
@javascript
Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has a public project Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has a public project
Given "John Doe" owns internal project "Internal" Given "John Doe" owns internal project "Internal"
And "John Doe" owns public project "Community" And "John Doe" owns public project "Community"
And I sign in as "John Doe" And I sign in as "John Doe"
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should see project "Enterprise" And I should see project "Enterprise"
And I should see project "Internal" And I should see project "Internal"
And I should see project "Community" And I should see project "Community"
@javascript
Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has no public project Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has no public project
Given I sign in as "John Doe" Given I sign in as "John Doe"
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should see project "Enterprise" And I should see project "Enterprise"
And I should not see project "Internal" And I should not see project "Internal"
...@@ -68,6 +80,7 @@ Feature: User ...@@ -68,6 +80,7 @@ Feature: User
Given I sign in as a user Given I sign in as a user
And "John Doe" has contributions And "John Doe" has contributions
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Contributed projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should see contributed projects And I should see contributed projects
And I should see contributions calendar And I should see contributions calendar
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment