summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Schatz <jschatz1@gmail.com>2016-03-03 00:13:19 +0000
committerJacob Schatz <jschatz1@gmail.com>2016-03-03 00:13:19 +0000
commit1fa7671f44291f78131c0fa31f6d1ffcb3ff6bbc (patch)
tree046701638803deb21c1d9242ea98138e6344bb9b
parent89270f77ed1ee5d74bba39edbbd5757d43af2a78 (diff)
parentd4981e9b4a9a548725eedff98b93c7ff3596c437 (diff)
downloadgitlab-ce-1fa7671f44291f78131c0fa31f6d1ffcb3ff6bbc.tar.gz
Merge branch 'improve-user-tabs' into 'master'
Add routes and actions for dynamic tab loading. Closes #13588 and #13584 See merge request !2961
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee3
-rw-r--r--app/assets/javascripts/pager.js.coffee3
-rw-r--r--app/assets/javascripts/user.js.coffee10
-rw-r--r--app/assets/javascripts/user_tabs.js.coffee146
-rw-r--r--app/controllers/users_controller.rb60
-rw-r--r--app/views/shared/groups/_list.html.haml6
-rw-r--r--app/views/shared/projects/_list.html.haml3
-rw-r--r--app/views/users/show.html.haml201
-rw-r--r--config/routes.rb9
-rw-r--r--features/steps/shared/user.rb16
-rw-r--r--features/user.feature13
11 files changed, 353 insertions, 117 deletions
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 89f1993797f..fe65ea8b14a 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -107,9 +107,6 @@ class Dispatcher
new ProjectFork()
when 'projects:artifacts:browse'
new BuildArtifacts()
- when 'users:show'
- new User()
- new Activities()
switch path.first()
when 'admin'
diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee
index d639303aed3..0ff83b7f0c8 100644
--- a/app/assets/javascripts/pager.js.coffee
+++ b/app/assets/javascripts/pager.js.coffee
@@ -1,6 +1,7 @@
@Pager =
init: (@limit = 0, preload, @disable = false) ->
- @loading = $(".loading")
+ @loading = $('.loading').first()
+
if preload
@offset = 0
@getOld()
diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee
index ec4271b092c..10ac064f9fc 100644
--- a/app/assets/javascripts/user.js.coffee
+++ b/app/assets/javascripts/user.js.coffee
@@ -1,10 +1,18 @@
class @User
- constructor: ->
+ constructor: (@opts) ->
$('.profile-groups-avatars').tooltip("placement": "top")
new ProjectsList()
+ @initTabs()
+
$('.hide-project-limit-message').on 'click', (e) ->
path = '/'
$.cookie('hide_project_limit_message', 'false', { path: path })
$(@).parents('.project-limit-message').remove()
e.preventDefault()
+
+ initTabs: ->
+ new UserTabs(
+ parentEl: '.user-profile'
+ action: @opts.action
+ )
diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee
new file mode 100644
index 00000000000..09b7eec9104
--- /dev/null
+++ b/app/assets/javascripts/user_tabs.js.coffee
@@ -0,0 +1,146 @@
+# 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
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 626213c6728..4b1cf242885 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -3,13 +3,6 @@ class UsersController < ApplicationController
before_action :set_user
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|
format.html
@@ -25,6 +18,45 @@ class UsersController < ApplicationController
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
calendar = contributions_calendar
@timestamps = calendar.timestamps
@@ -69,6 +101,20 @@ class UsersController < ApplicationController
limit_recent(20, params[:offset])
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
ProjectsFinder.new.execute(current_user)
end
diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml
new file mode 100644
index 00000000000..1aa7ed1f2eb
--- /dev/null
+++ b/app/views/shared/groups/_list.html.haml
@@ -0,0 +1,6 @@
+- if groups.any?
+ %ul.content-list
+ - groups.each_with_index do |group, i|
+ = render "shared/groups/group", group: group
+- else
+ %h3 No groups found
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 2446e6c30ae..e4bc9998163 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -6,6 +6,7 @@
- ci = false unless local_assigns[:ci] == 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
+- remote = false unless local_assigns[:remote] == true
%ul.projects-list.content-list
- if projects.any?
@@ -21,7 +22,7 @@
#{projects_limit} of #{pluralize(projects.count, 'project')} displayed.
= link_to '#', class: 'js-expand' do
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
.nothing-here-block No projects found
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index d109635fa1e..bca816f22cb 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -8,117 +8,110 @@
= render 'shared/show_aside'
-.cover-block
- .cover-controls
- - if @user == current_user
- = link_to profile_path, class: 'btn btn-gray' do
- = icon('pencil')
- - elsif current_user
- %span.report-abuse
- - if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
- = icon('exclamation-circle')
- - else
- = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
- = icon('exclamation-circle')
- - if current_user
- &nbsp;
- = 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
- = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
- .cover-title
- = @user.name
-
- .cover-desc
- %span.middle-dot-divider
- @#{@user.username}
- %span.middle-dot-divider
- Member since #{@user.created_at.to_s(:medium)}
-
- - if @user.bio.present?
+.user-profile
+ .cover-block
+ .cover-controls
+ - if @user == current_user
+ = link_to profile_path, class: 'btn btn-gray' do
+ = icon('pencil')
+ - elsif current_user
+ %span.report-abuse
+ - if @user.abuse_report
+ %button.btn.btn-danger{ title: 'Already reported for abuse',
+ data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
+ = icon('exclamation-circle')
+ - else
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
+ title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
+ = icon('exclamation-circle')
+ - if current_user
+ &nbsp;
+ = 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
+ = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
+ .cover-title
+ = @user.name
+
+ .cover-desc
+ %span.middle-dot-divider
+ @#{@user.username}
+ %span.middle-dot-divider
+ Member since #{@user.created_at.to_s(:medium)}
+
+ - if @user.bio.present?
+ .cover-desc
+ %p.profile-user-bio
+ = @user.bio
+
.cover-desc
- %p.profile-user-bio
- = @user.bio
-
- .cover-desc
- - unless @user.public_email.blank?
- .profile-link-holder.middle-dot-divider
- = link_to @user.public_email, "mailto:#{@user.public_email}"
- - unless @user.skype.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "skype:#{@user.skype}", title: "Skype" do
- = icon('skype')
- - unless @user.linkedin.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
- = icon('linkedin-square')
- - unless @user.twitter.blank?
- .profile-link-holder.middle-dot-divider
- = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
- = icon('twitter-square')
- - unless @user.website_url.blank?
- .profile-link-holder.middle-dot-divider
- = link_to @user.short_website_url, @user.full_website_url
- - unless @user.location.blank?
- .profile-link-holder.middle-dot-divider
- = icon('map-marker')
- = @user.location
-
- %ul.nav-links.center
- %li.active
- = link_to "#activity", 'data-toggle' => 'tab' do
- Activity
- - if @groups.any?
- %li
- = link_to "#groups", 'data-toggle' => 'tab' do
+ - unless @user.public_email.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.public_email, "mailto:#{@user.public_email}"
+ - unless @user.skype.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "skype:#{@user.skype}", title: "Skype" do
+ = icon('skype')
+ - unless @user.linkedin.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
+ = icon('linkedin-square')
+ - unless @user.twitter.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
+ = icon('twitter-square')
+ - unless @user.website_url.blank?
+ .profile-link-holder.middle-dot-divider
+ = link_to @user.short_website_url, @user.full_website_url
+ - unless @user.location.blank?
+ .profile-link-holder.middle-dot-divider
+ = icon('map-marker')
+ = @user.location
+
+ %ul.nav-links.center.user-profile-nav
+ %li.activity-tab
+ = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+ Activity
+ %li.groups-tab
+ = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
Groups
- - if @contributed_projects.present?
- %li
- = link_to "#contributed", 'data-toggle' => 'tab' do
+ %li.contributed-tab
+ = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
Contributed projects
- - if @projects.present?
- %li
- = link_to "#personal", 'data-toggle' => 'tab' do
+ %li.projects-tab
+ = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
Personal projects
-%div{ class: container_class }
- .tab-content
- .tab-pane.active#activity
- .gray-content-block.white.second-block
- %div{ class: container_class }
- .user-calendar
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
- .user-calendar-activities
+ %div{ class: container_class }
+ .tab-content
+ #activity.tab-pane
+ .gray-content-block.white.second-block
+ %div{ class: container_class }
+ .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} }
+ = spinner
- .content_list
- = spinner
+ #groups.tab-pane
+ - # This tab is always loaded via AJAX
+
+ #contributed.contributed-projects.tab-pane
+ - # This tab is always loaded via AJAX
- - if @groups.any?
- .tab-pane#groups
- %ul.content-list
- - @groups.each do |group|
- = render 'shared/groups/group', group: group
-
- - 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
+ #projects.tab-pane
+ - # This tab is always loaded via AJAX
+
+ .loading-status
+ = spinner
:javascript
- $(".user-calendar").load("#{user_calendar_path}");
+ var userProfile;
+
+ userProfile = new User({
+ action: "#{controller.action_name}"
+ });
diff --git a/config/routes.rb b/config/routes.rb
index a2acf170a6b..52c532601b4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -332,6 +332,15 @@ Rails.application.routes.draw do
get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities,
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,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb
index f0721094ee3..9856c510aa0 100644
--- a/features/steps/shared/user.rb
+++ b/features/steps/shared/user.rb
@@ -26,4 +26,20 @@ module SharedUser
step 'I have no ssh keys' do
@user.keys.delete_all
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
diff --git a/features/user.feature b/features/user.feature
index 35eae842e77..e0cadba30a1 100644
--- a/features/user.feature
+++ b/features/user.feature
@@ -5,10 +5,12 @@ Feature: User
# Signed out
+ @javascript
Scenario: I visit user "John Doe" page while not signed in when he owns a public project
Given "John Doe" owns internal project "Internal"
And "John Doe" owns public project "Community"
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should not see project "Internal"
@@ -16,28 +18,34 @@ Feature: User
# 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
Given "John Doe" owns public project "Community"
And "John Doe" owns internal project "Internal"
And I sign in as a user
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should see project "Internal"
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
Given "John Doe" owns internal project "Internal"
And I sign in as a user
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should see project "Internal"
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
Given I sign in as a user
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should not see project "Enterprise"
And I should not see project "Internal"
@@ -45,19 +53,23 @@ Feature: User
# 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
Given "John Doe" owns internal project "Internal"
And "John Doe" owns public project "Community"
And I sign in as "John Doe"
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should see project "Enterprise"
And I should see project "Internal"
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
Given I sign in as "John Doe"
When I visit user "John Doe" page
+ And I click on "Personal projects" tab
Then I should see user "John Doe" page
And I should see project "Enterprise"
And I should not see project "Internal"
@@ -68,6 +80,7 @@ Feature: User
Given I sign in as a user
And "John Doe" has contributions
When I visit user "John Doe" page
+ And I click on "Contributed projects" tab
Then I should see user "John Doe" page
And I should see contributed projects
And I should see contributions calendar