Skip to content
Snippets Groups Projects
Commit 3ad34c3a authored by Stan Hu's avatar Stan Hu
Browse files

Merge branch '20137-starrers' into 'master'

Add possibilty to view starrers ("stargazers") of a repository & any user's starred repositories

Closes #20137

See merge request gitlab-org/gitlab-ce!24690
parents 4985f6e2 d4078b53
No related branches found
No related tags found
No related merge requests found
Showing
with 361 additions and 69 deletions
Loading
Loading
@@ -143,7 +143,7 @@ export default class UserTabs {
this.loadOverviewTab();
}
 
const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
const loadableActions = ['groups', 'contributed', 'projects', 'starred', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
this.loadTab(action, endpoint);
}
Loading
Loading
Loading
Loading
@@ -18,7 +18,7 @@ export default class Star {
const isStarred = $starSpan.hasClass('starred');
$this
.parent()
.find('.star-count')
.find('.count')
.text(data.star_count);
 
if (isStarred) {
Loading
Loading
Loading
Loading
@@ -9,10 +9,6 @@
}
}
 
.member-sort-dropdown {
margin-left: $gl-padding-8;
}
.member {
&.is-overridden {
.btn-ldap-override {
Loading
Loading
@@ -62,36 +58,9 @@
}
}
 
.member-search-form {
position: relative;
@include media-breakpoint-up(sm) {
float: right;
}
.dropdown {
width: 100%;
margin-top: 5px;
.dropdown-menu-toggle {
vertical-align: middle;
width: 100%;
}
@include media-breakpoint-up(sm) {
margin-top: 0;
width: 155px;
}
}
.form-control {
width: 100%;
padding-right: 35px;
@include media-breakpoint-up(sm) {
width: 250px;
}
}
.member-access-text {
margin-left: auto;
line-height: 43px;
}
 
.member-search-btn {
Loading
Loading
@@ -177,7 +146,7 @@
padding-bottom: 1px;
}
 
.flex-project-members-form {
.flex-users-form {
flex-wrap: nowrap;
white-space: nowrap;
margin-left: auto;
Loading
Loading
.user-sort-dropdown {
margin-left: $gl-padding-8;
}
.user-search-form {
position: relative;
@include media-breakpoint-up(sm) {
float: right;
}
.dropdown {
width: 100%;
margin-top: 5px;
.dropdown-menu-toggle {
vertical-align: middle;
width: 100%;
}
@include media-breakpoint-up(sm) {
margin-top: 0;
width: 155px;
}
}
.form-control {
width: 100%;
padding-right: 35px;
@include media-breakpoint-up(sm) {
width: 250px;
}
}
}
.user-search-btn {
position: absolute;
right: 4px;
top: 0;
height: 35px;
padding-left: 10px;
padding-right: 10px;
color: $gray-darkest;
background: transparent;
border: 0;
outline: 0;
}
.flex-users-panel {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
@include media-breakpoint-down(sm) {
display: block;
.flex-project-title {
vertical-align: top;
display: inline-block;
max-width: 90%;
}
}
.flex-project-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.badge.badge-pill {
height: 17px;
line-height: 16px;
margin-right: 5px;
padding-top: 1px;
padding-bottom: 1px;
}
.flex-users-form {
flex-wrap: nowrap;
white-space: nowrap;
margin-left: auto;
}
}
.content-list.members-list li {
display: flex;
justify-content: space-between;
.list-item-name {
float: none;
display: flex;
flex: 1;
}
}
.card-body .user-info {
float: left;
.user {
color: $gl-text-color;
font-weight: $gl-font-weight-bold;
}
}
# frozen_string_literal: true
class Projects::StarrersController < Projects::ApplicationController
include SortingHelper
def index
@starrers = UsersStarProjectsFinder.new(@project, params, current_user: @current_user).execute
# Normally the number of public starrers is equal to the number of visible
# starrers. We need to fix the counts in two cases: when the current user
# is an admin (and can see everything) and when the current user has a
# private profile and has starred the project (and can see itself).
@public_count =
if @current_user&.admin?
@starrers.with_public_profile.count
elsif @current_user&.private_profile && has_starred_project?(@starrers)
@starrers.size - 1
else
@starrers.size
end
@total_count = @project.starrers.size
@private_count = @total_count - @public_count
@sort = params[:sort].presence || sort_value_name
@starrers = @starrers.sort_by_attribute(@sort).page(params[:page])
end
private
def has_starred_project?(starrers)
starrers.first { |starrer| starrer.user_id == current_user.id }
end
end
Loading
Loading
@@ -17,7 +17,7 @@ class UsersController < ApplicationController
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :user, except: [:exists]
before_action :authorize_read_user_profile!,
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets]
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :starred_projects, :snippets]
 
def show
respond_to do |format|
Loading
Loading
@@ -57,27 +57,30 @@ class UsersController < ApplicationController
def projects
load_projects
 
skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination])
skip_namespace = Gitlab::Utils.to_boolean(params[:skip_namespace])
compact_mode = Gitlab::Utils.to_boolean(params[:compact_mode])
respond_to do |format|
format.html { render 'show' }
format.json do
pager_json("shared/projects/_list", @projects.count, projects: @projects, skip_pagination: skip_pagination, skip_namespace: skip_namespace, compact_mode: compact_mode)
end
end
present_projects(@projects)
end
 
def contributed
load_contributed_projects
 
present_projects(@contributed_projects)
end
def starred
load_starred_projects
present_projects(@starred_projects)
end
def present_projects(projects)
skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination])
skip_namespace = Gitlab::Utils.to_boolean(params[:skip_namespace])
compact_mode = Gitlab::Utils.to_boolean(params[:compact_mode])
respond_to do |format|
format.html { render 'show' }
format.json do
render json: {
html: view_to_html_string("shared/projects/_list", projects: @contributed_projects)
}
pager_json("shared/projects/_list", projects.count, projects: projects, skip_pagination: skip_pagination, skip_namespace: skip_namespace, compact_mode: compact_mode)
end
end
end
Loading
Loading
@@ -120,6 +123,10 @@ class UsersController < ApplicationController
ContributedProjectsFinder.new(user).execute(current_user)
end
 
def starred_projects
StarredProjectsFinder.new(user, current_user: current_user).execute
end
def contributions_calendar
@contributions_calendar ||= Gitlab::ContributionsCalendar.new(user, current_user)
end
Loading
Loading
@@ -145,6 +152,12 @@ class UsersController < ApplicationController
prepare_projects_for_rendering(@contributed_projects)
end
 
def load_starred_projects
@starred_projects = starred_projects
prepare_projects_for_rendering(@starred_projects)
end
def load_groups
@groups = JoinedGroupsFinder.new(user).execute(current_user)
 
Loading
Loading
# frozen_string_literal: true
class StarredProjectsFinder < ProjectsFinder
def initialize(user, params: {}, current_user: nil)
super(
params: params,
current_user: current_user,
project_ids_relation: user.starred_projects.select(:id)
)
end
end
# frozen_string_literal: true
class UsersStarProjectsFinder
include CustomAttributesFilter
attr_accessor :params
def initialize(project, params = {}, current_user: nil)
@params = params
@project = project
@current_user = current_user
end
def execute
stars = UsersStarProject.all
stars = by_project(stars)
stars = by_search(stars)
stars = filter_visible_profiles(stars)
stars
end
private
def by_search(items)
params[:search].present? ? items.search(params[:search]) : items
end
def by_project(items)
items.by_project(@project)
end
def filter_visible_profiles(items)
items.with_visible_profile(@current_user)
end
end
Loading
Loading
@@ -601,6 +601,11 @@ module ProjectsHelper
end
end
 
def filter_starrer_path(options = {})
options = params.slice(:sort).merge(options).permit!
"#{request.path}?#{options.to_param}"
end
def sidebar_projects_paths
%w[
projects#show
Loading
Loading
Loading
Loading
@@ -167,6 +167,15 @@ module SortingHelper
}
end
 
def starrers_sort_options_hash
{
sort_value_name => sort_title_name,
sort_value_name_desc => sort_title_name_desc,
sort_value_recently_created => sort_title_recently_starred,
sort_value_oldest_created => sort_title_oldest_starred
}
end
def sortable_item(item, path, sorted_by)
link_to item, path, class: sorted_by == item ? 'is-active' : ''
end
Loading
Loading
@@ -327,6 +336,10 @@ module SortingHelper
s_('SortOptions|Oldest sign in')
end
 
def sort_title_oldest_starred
s_('SortOptions|Oldest starred')
end
def sort_title_oldest_updated
s_('SortOptions|Oldest updated')
end
Loading
Loading
@@ -347,6 +360,10 @@ module SortingHelper
s_('SortOptions|Recent sign in')
end
 
def sort_title_recently_starred
s_('SortOptions|Recently starred')
end
def sort_title_recently_updated
s_('SortOptions|Last updated')
end
Loading
Loading
Loading
Loading
@@ -89,7 +89,7 @@ module UsersHelper
tabs = []
 
if can?(current_user, :read_user_profile, @user)
tabs += [:overview, :activity, :groups, :contributed, :projects, :snippets]
tabs += [:overview, :activity, :groups, :contributed, :projects, :starred, :snippets]
end
 
tabs
Loading
Loading
Loading
Loading
@@ -282,6 +282,17 @@ class User < ApplicationRecord
scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) }
scope :with_emails, -> { preload(:emails) }
scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) }
scope :with_public_profile, -> { where(private_profile: false) }
def self.with_visible_profile(user)
return with_public_profile if user.nil?
if user.admin?
all
else
with_public_profile.or(where(id: user.id))
end
end
 
# Limits the users to those that have TODOs, optionally in the given state.
#
Loading
Loading
# frozen_string_literal: true
 
class UsersStarProject < ApplicationRecord
include Sortable
belongs_to :project, counter_cache: :star_count, touch: true
belongs_to :user
 
validates :user, presence: true
validates :user_id, uniqueness: { scope: [:project_id] }
validates :project, presence: true
alias_attribute :starred_since, :created_at
scope :order_user_name_asc, -> { joins(:user).merge(User.order_name_asc) }
scope :order_user_name_desc, -> { joins(:user).merge(User.order_name_desc) }
scope :by_project, -> (project) { where(project_id: project.id) }
scope :with_visible_profile, -> (user) { joins(:user).merge(User.with_visible_profile(user)) }
scope :with_public_profile, -> { joins(:user).merge(User.with_public_profile) }
class << self
def sort_by_attribute(method)
order_method = method || 'id_desc'
case order_method.to_s
when 'name_asc' then order_user_name_asc
when 'name_desc' then order_user_name_desc
else
order_by(order_method)
end
end
def search(query)
joins(:user).merge(User.search(query))
end
end
end
Loading
Loading
@@ -25,11 +25,11 @@
Members with access to
%strong= @group.name
%span.badge.badge-pill= @members.total_count
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline user-search-form flex-users-form' do
.form-group
.position-relative.append-right-8
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
%button.user-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
- if can_manage_members
= render 'shared/members/filter_2fa_dropdown'
Loading
Loading
Loading
Loading
@@ -8,7 +8,8 @@
= sprite_icon('star-o', { css_class: 'icon' })
%span= s_('ProjectOverview|Star')
%span.star-count.count-badge-count.d-flex.align-items-center
= @project.star_count
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'count' do
= @project.star_count
 
- else
.count-badge.d-inline-flex.align-item-stretch.append-right-8
Loading
Loading
@@ -16,4 +17,5 @@
= sprite_icon('star-o', { css_class: 'icon' })
%span= s_('ProjectOverview|Star')
%span.star-count.count-badge-count.d-flex.align-items-center
= @project.star_count
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'count' do
= @project.star_count
Loading
Loading
@@ -6,11 +6,11 @@
%span.flex-project-title
= _("Members of <strong>%{project_name}</strong>").html_safe % { project_name: sanitize(project.name, tags: []) }
%span.badge.badge-pill= members.total_count
= form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
= form_tag project_project_members_path(project), method: :get, class: 'form-inline user-search-form flex-users-form' do
.form-group
.position-relative
= search_field_tag :search, params[:search], { placeholder: _('Find existing members by name'), class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => _("Submit search") }
%button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") }
= icon("search")
= render 'shared/members/sort_dropdown'
%ul.content-list.members-list.qa-members-list
Loading
Loading
- starrer = local_assigns.fetch(:starrer)
.col-lg-3.col-md-4.col-sm-12
.card
.card-body
= image_tag avatar_icon_for_user(starrer.user, 40), class: "avatar s40", alt: ''
.user-info
.block-truncated
= link_to starrer.user.name, user_path(starrer.user), class: 'user js-user-link', data: { user_id: starrer.user.id }
.block-truncated
%span.cgray= starrer.user.to_reference
- if starrer.user == current_user
%span.badge.badge-success.prepend-left-5= _("It's you")
.block-truncated
= time_ago_with_tooltip(starrer.starred_since)
- page_title _("Starrers")
.top-area.adjust
.nav-text
- full_count_title = "#{@public_count} public and #{@private_count} private"
#{pluralize(@total_count, 'starrer')}: #{full_count_title}
- if @starrers.size > 0 || params[:search].present?
.nav-controls
= form_tag request.original_url, method: :get, class: 'form-inline user-search-form flex-users-form' do
.form-group
.position-relative
= search_field_tag :search, params[:search], { placeholder: _('Search'), class: 'form-control', spellcheck: false }
%button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") }
= icon("search")
.dropdown.inline.user-sort-dropdown
= dropdown_toggle(starrers_sort_options_hash[@sort], { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= _("Sort by")
- starrers_sort_options_hash.each do |value, title|
%li
= link_to filter_starrer_path(sort: value), class: ("is-active" if @sort == value) do
= title
- if @starrers.size > 0
.row.prepend-top-10
= render partial: 'starrer', collection: @starrers, as: :starrer
= paginate @starrers, theme: 'gitlab'
- else
- if params[:search].present?
.nothing-here-block= _('No starrers matched your search')
- else
.nothing-here-block= _('Nobody has starred this repository yet')
.dropdown.inline.member-sort-dropdown
.dropdown.inline.user-sort-dropdown
= dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
Loading
Loading
Loading
Loading
@@ -17,15 +17,20 @@
- contributed_projects_illustration_path = 'illustrations/profile-page/contributed-projects.svg'
- contributed_projects_current_user_empty_message_header = s_('UserProfile|Explore public groups to find projects to contribute to.')
- contributed_projects_visitor_empty_message = s_('UserProfile|This user hasn\'t contributed to any projects')
- starred_projects_illustration_path = 'illustrations/starred_empty.svg'
- starred_projects_current_user_empty_message_header = s_('UserProfile|Star projects to track their progress and show your appreciation.')
- starred_projects_visitor_empty_message = s_('UserProfile|This user hasn\'t starred any projects')
- own_projects_illustration_path = 'illustrations/profile-page/personal-project.svg'
- own_projects_current_user_empty_message_header = s_('UserProfile|You haven\'t created any personal projects.')
- own_projects_current_user_empty_message_description = s_('UserProfile|Your projects can be available publicly, internally, or privately, at your choice.')
- own_projects_visitor_empty_message = s_('UserProfile|This user doesn\'t have any personal projects')
- explore_page_empty_message = s_('UserProfile|Explore public groups to find projects to contribute to.')
- primary_button_label = _('New project')
- primary_button_link = new_project_path
- secondary_button_label = _('Explore groups')
- secondary_button_link = explore_groups_path
- new_project_button_label = _('New project')
- new_project_button_link = new_project_path
- explore_projects_button_label = _('Explore projects')
- explore_projects_button_link = explore_projects_path
- explore_groups_button_label = _('Explore groups')
- explore_groups_button_link = explore_groups_path
 
.js-projects-list-holder
- if any_projects?(projects)
Loading
Loading
@@ -48,15 +53,21 @@
- if @contributed_projects
= render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: contributed_projects_illustration_path,
current_user_empty_message_header: contributed_projects_current_user_empty_message_header,
primary_button_label: primary_button_label,
primary_button_link: primary_button_link,
secondary_button_label: secondary_button_label,
secondary_button_link: secondary_button_link,
primary_button_label: new_project_button_label,
primary_button_link: new_project_button_link,
secondary_button_label: explore_groups_button_label,
secondary_button_link: explore_groups_button_link,
visitor_empty_message: contributed_projects_visitor_empty_message }
- elsif @starred_projects
= render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: starred_projects_illustration_path,
current_user_empty_message_header: starred_projects_current_user_empty_message_header,
primary_button_label: explore_projects_button_label,
primary_button_link: explore_projects_button_link,
visitor_empty_message: starred_projects_visitor_empty_message }
- else
= render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: own_projects_illustration_path,
current_user_empty_message_header: own_projects_current_user_empty_message_header,
current_user_empty_message_description: own_projects_current_user_empty_message_description,
primary_button_label: primary_button_label,
primary_button_link: primary_button_link,
primary_button_label: new_project_button_label,
primary_button_link: new_project_button_link,
visitor_empty_message: defined?(explore_page) && explore_page ? explore_page_empty_message : own_projects_visitor_empty_message }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment