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
This commit is contained in:
commit
3ad34c3a24
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -9,10 +9,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.member-sort-dropdown {
|
||||
margin-left: $gl-padding-8;
|
||||
}
|
||||
|
||||
.member {
|
||||
&.is-overridden {
|
||||
.btn-ldap-override {
|
||||
|
@ -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 {
|
||||
|
@ -177,7 +146,7 @@
|
|||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.flex-project-members-form {
|
||||
.flex-users-form {
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
margin-left: auto;
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
.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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
# 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
|
|
@ -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|
|
||||
|
@ -57,6 +57,22 @@ class UsersController < ApplicationController
|
|||
def projects
|
||||
load_projects
|
||||
|
||||
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])
|
||||
|
@ -64,20 +80,7 @@ class UsersController < ApplicationController
|
|||
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
|
||||
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)
|
||||
}
|
||||
pager_json("shared/projects/_list", projects.count, projects: projects, skip_pagination: skip_pagination, skip_namespace: skip_namespace, compact_mode: compact_mode)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# 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
|
|
@ -0,0 +1,36 @@
|
|||
# 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
#
|
||||
|
|
|
@ -1,10 +1,37 @@
|
|||
# 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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
= sprite_icon('star-o', { css_class: 'icon' })
|
||||
%span= s_('ProjectOverview|Star')
|
||||
%span.star-count.count-badge-count.d-flex.align-items-center
|
||||
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'count' do
|
||||
= @project.star_count
|
||||
|
||||
- else
|
||||
|
@ -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
|
||||
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'count' do
|
||||
= @project.star_count
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
- 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)
|
|
@ -0,0 +1,32 @@
|
|||
- 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')
|
|
@ -1,4 +1,4 @@
|
|||
.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
|
||||
|
|
|
@ -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)
|
||||
|
@ -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 }
|
||||
|
|
|
@ -63,7 +63,9 @@
|
|||
- if project.archived
|
||||
%span.d-flex.icon-wrapper.badge.badge-warning archived
|
||||
- if stars
|
||||
%span.d-flex.align-items-center.icon-wrapper.stars.has-tooltip{ data: { container: 'body', placement: 'top' }, title: _('Stars') }
|
||||
= link_to project_starrers_path(project),
|
||||
class: "d-flex align-items-center icon-wrapper stars has-tooltip",
|
||||
title: _('Stars'), data: { container: 'body', placement: 'top' } do
|
||||
= sprite_icon('star', size: 14, css_class: 'append-right-4')
|
||||
= number_with_delimiter(project.star_count)
|
||||
- if forks
|
||||
|
|
|
@ -111,6 +111,10 @@
|
|||
%li.js-projects-tab
|
||||
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
|
||||
= s_('UserProfile|Personal projects')
|
||||
- if profile_tab?(:starred)
|
||||
%li.js-starred-tab
|
||||
= link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do
|
||||
= s_('UserProfile|Starred projects')
|
||||
- if profile_tab?(:snippets)
|
||||
%li.js-snippets-tab
|
||||
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
|
||||
|
@ -142,6 +146,10 @@
|
|||
#projects.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
|
||||
- if profile_tab?(:starred)
|
||||
#starred.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
|
||||
- if profile_tab?(:snippets)
|
||||
#snippets.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make starred projects and starrers of a project publicly visible
|
||||
merge_request: 24690
|
||||
author:
|
||||
type: added
|
|
@ -170,7 +170,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
get :recent
|
||||
end
|
||||
end
|
||||
|
||||
resources :releases, only: [:index]
|
||||
resources :starrers, only: [:index]
|
||||
resources :forks, only: [:index, :new, :create]
|
||||
resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
|
|||
get :groups
|
||||
get :projects
|
||||
get :contributed, as: :contributed_projects
|
||||
get :starred, as: :starred_projects
|
||||
get :snippets
|
||||
get :exists
|
||||
get :activity
|
||||
|
|
|
@ -465,6 +465,194 @@ GET /users/:user_id/projects
|
|||
]
|
||||
```
|
||||
|
||||
## List projects starred by a user
|
||||
|
||||
Get a list of visible projects owned by the given user. When accessed without authentication, only public projects are returned.
|
||||
|
||||
```
|
||||
GET /users/:user_id/starred_projects
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `user_id` | string | yes | The ID or username of the user. |
|
||||
| `archived` | boolean | no | Limit by archived status. |
|
||||
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private`. |
|
||||
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at`. |
|
||||
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc`. |
|
||||
| `search` | string | no | Return list of projects matching the search criteria. |
|
||||
| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned.. |
|
||||
| `owned` | boolean | no | Limit by projects explicitly owned by the current user. |
|
||||
| `membership` | boolean | no | Limit by projects that the current user is a member of. |
|
||||
| `starred` | boolean | no | Limit by projects starred by the current user. |
|
||||
| `statistics` | boolean | no | Include project statistics. |
|
||||
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only). |
|
||||
| `with_issues_enabled` | boolean | no | Limit by enabled issues feature. |
|
||||
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature. |
|
||||
| `min_access_level` | integer | no | Limit by current user minimal [access level](members.md). |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/users/5/starred_projects"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 4,
|
||||
"description": null,
|
||||
"default_branch": "master",
|
||||
"visibility": "private",
|
||||
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
|
||||
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
|
||||
"web_url": "http://example.com/diaspora/diaspora-client",
|
||||
"readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md",
|
||||
"tag_list": [
|
||||
"example",
|
||||
"disapora client"
|
||||
],
|
||||
"owner": {
|
||||
"id": 3,
|
||||
"name": "Diaspora",
|
||||
"created_at": "2013-09-30T13:46:02Z"
|
||||
},
|
||||
"name": "Diaspora Client",
|
||||
"name_with_namespace": "Diaspora / Diaspora Client",
|
||||
"path": "diaspora-client",
|
||||
"path_with_namespace": "diaspora/diaspora-client",
|
||||
"issues_enabled": true,
|
||||
"open_issues_count": 1,
|
||||
"merge_requests_enabled": true,
|
||||
"jobs_enabled": true,
|
||||
"wiki_enabled": true,
|
||||
"snippets_enabled": false,
|
||||
"resolve_outdated_diff_discussions": false,
|
||||
"container_registry_enabled": false,
|
||||
"created_at": "2013-09-30T13:46:02Z",
|
||||
"last_activity_at": "2013-09-30T13:46:02Z",
|
||||
"creator_id": 3,
|
||||
"namespace": {
|
||||
"id": 3,
|
||||
"name": "Diaspora",
|
||||
"path": "diaspora",
|
||||
"kind": "group",
|
||||
"full_path": "diaspora"
|
||||
},
|
||||
"import_status": "none",
|
||||
"archived": false,
|
||||
"avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
|
||||
"shared_runners_enabled": true,
|
||||
"forks_count": 0,
|
||||
"star_count": 0,
|
||||
"runners_token": "b8547b1dc37721d05889db52fa2f02",
|
||||
"public_jobs": true,
|
||||
"shared_with_groups": [],
|
||||
"only_allow_merge_if_pipeline_succeeds": false,
|
||||
"only_allow_merge_if_all_discussions_are_resolved": false,
|
||||
"request_access_enabled": false,
|
||||
"merge_method": "merge",
|
||||
"statistics": {
|
||||
"commit_count": 37,
|
||||
"storage_size": 1038090,
|
||||
"repository_size": 1038090,
|
||||
"lfs_objects_size": 0,
|
||||
"job_artifacts_size": 0
|
||||
},
|
||||
"_links": {
|
||||
"self": "http://example.com/api/v4/projects",
|
||||
"issues": "http://example.com/api/v4/projects/1/issues",
|
||||
"merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
|
||||
"repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
|
||||
"labels": "http://example.com/api/v4/projects/1/labels",
|
||||
"events": "http://example.com/api/v4/projects/1/events",
|
||||
"members": "http://example.com/api/v4/projects/1/members"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"description": null,
|
||||
"default_branch": "master",
|
||||
"visibility": "private",
|
||||
"ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
|
||||
"http_url_to_repo": "http://example.com/brightbox/puppet.git",
|
||||
"web_url": "http://example.com/brightbox/puppet",
|
||||
"readme_url": "http://example.com/brightbox/puppet/blob/master/README.md",
|
||||
"tag_list": [
|
||||
"example",
|
||||
"puppet"
|
||||
],
|
||||
"owner": {
|
||||
"id": 4,
|
||||
"name": "Brightbox",
|
||||
"created_at": "2013-09-30T13:46:02Z"
|
||||
},
|
||||
"name": "Puppet",
|
||||
"name_with_namespace": "Brightbox / Puppet",
|
||||
"path": "puppet",
|
||||
"path_with_namespace": "brightbox/puppet",
|
||||
"issues_enabled": true,
|
||||
"open_issues_count": 1,
|
||||
"merge_requests_enabled": true,
|
||||
"jobs_enabled": true,
|
||||
"wiki_enabled": true,
|
||||
"snippets_enabled": false,
|
||||
"resolve_outdated_diff_discussions": false,
|
||||
"container_registry_enabled": false,
|
||||
"created_at": "2013-09-30T13:46:02Z",
|
||||
"last_activity_at": "2013-09-30T13:46:02Z",
|
||||
"creator_id": 3,
|
||||
"namespace": {
|
||||
"id": 4,
|
||||
"name": "Brightbox",
|
||||
"path": "brightbox",
|
||||
"kind": "group",
|
||||
"full_path": "brightbox"
|
||||
},
|
||||
"import_status": "none",
|
||||
"import_error": null,
|
||||
"permissions": {
|
||||
"project_access": {
|
||||
"access_level": 10,
|
||||
"notification_level": 3
|
||||
},
|
||||
"group_access": {
|
||||
"access_level": 50,
|
||||
"notification_level": 3
|
||||
}
|
||||
},
|
||||
"archived": false,
|
||||
"avatar_url": null,
|
||||
"shared_runners_enabled": true,
|
||||
"forks_count": 0,
|
||||
"star_count": 0,
|
||||
"runners_token": "b8547b1dc37721d05889db52fa2f02",
|
||||
"public_jobs": true,
|
||||
"shared_with_groups": [],
|
||||
"only_allow_merge_if_pipeline_succeeds": false,
|
||||
"only_allow_merge_if_all_discussions_are_resolved": false,
|
||||
"request_access_enabled": false,
|
||||
"merge_method": "merge",
|
||||
"statistics": {
|
||||
"commit_count": 12,
|
||||
"storage_size": 2066080,
|
||||
"repository_size": 2066080,
|
||||
"lfs_objects_size": 0,
|
||||
"job_artifacts_size": 0
|
||||
},
|
||||
"_links": {
|
||||
"self": "http://example.com/api/v4/projects",
|
||||
"issues": "http://example.com/api/v4/projects/1/issues",
|
||||
"merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
|
||||
"repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
|
||||
"labels": "http://example.com/api/v4/projects/1/labels",
|
||||
"events": "http://example.com/api/v4/projects/1/events",
|
||||
"members": "http://example.com/api/v4/projects/1/members"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Get single project
|
||||
|
||||
Get a specific project. This endpoint can be accessed without authentication if
|
||||
|
@ -1155,6 +1343,51 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## List Starrers of a project
|
||||
|
||||
List the users who starred the specified project.
|
||||
|
||||
```
|
||||
GET /projects/:id/starrers
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `search` | string | no | Search for specific users. |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/starrers"
|
||||
```
|
||||
|
||||
Example responses:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"starred_since": "2019-01-28T14:47:30.642Z",
|
||||
"user":
|
||||
{
|
||||
"id": 1,
|
||||
"username": "jane_smith",
|
||||
"name": "Jane Smith",
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
|
||||
"web_url": "http://localhost:3000/jane_smith"
|
||||
}
|
||||
},
|
||||
"starred_since": "2018-01-02T11:40:26.570Z",
|
||||
"user":
|
||||
{
|
||||
"id": 2,
|
||||
"username": "janine_smith",
|
||||
"name": "Janine Smith",
|
||||
"state": "blocked",
|
||||
"avatar_url": "http://gravatar.com/../e32131cd8.jpeg",
|
||||
"web_url": "http://localhost:3000/janine_smith"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Languages
|
||||
|
||||
Get languages used in a project with percentage value.
|
||||
|
|
|
@ -27,6 +27,7 @@ On your profile page, you will see the following information:
|
|||
- Groups: [groups](../group/index.md) you're a member of
|
||||
- Contributed projects: [projects](../project/index.md) you contributed to
|
||||
- Personal projects: your personal projects (respecting the project's visibility level)
|
||||
- Starred projects: projects you starred
|
||||
- Snippets: your personal code [snippets](../snippets.md#personal-snippets)
|
||||
|
||||
## Profile settings
|
||||
|
@ -91,6 +92,7 @@ The following information will be hidden from the user profile page (`https://gi
|
|||
- Groups tab
|
||||
- Contributed projects tab
|
||||
- Personal projects tab
|
||||
- Starred projects tab
|
||||
- Snippets tab
|
||||
|
||||
To enable private profile:
|
||||
|
|
|
@ -77,6 +77,11 @@ module API
|
|||
expose :last_activity_on, as: :last_activity_at # Back-compat
|
||||
end
|
||||
|
||||
class UserStarsProject < Grape::Entity
|
||||
expose :starred_since
|
||||
expose :user, using: Entities::UserBasic
|
||||
end
|
||||
|
||||
class Identity < Grape::Entity
|
||||
expose :provider, :extern_uid
|
||||
end
|
||||
|
|
|
@ -115,6 +115,22 @@ module API
|
|||
|
||||
present_projects load_projects
|
||||
end
|
||||
|
||||
desc 'Get projects starred by a user' do
|
||||
success Entities::BasicProjectDetails
|
||||
end
|
||||
params do
|
||||
requires :user_id, type: String, desc: 'The ID or username of the user'
|
||||
use :collection_params
|
||||
use :statistics_params
|
||||
end
|
||||
get ":user_id/starred_projects" do
|
||||
user = find_user(params[:user_id])
|
||||
not_found!('User') unless user
|
||||
|
||||
starred_projects = StarredProjectsFinder.new(user, params: project_finder_params, current_user: current_user).execute
|
||||
present_projects starred_projects
|
||||
end
|
||||
end
|
||||
|
||||
resource :projects do
|
||||
|
@ -358,6 +374,19 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
desc 'Get the users who starred a project' do
|
||||
success Entities::UserBasic
|
||||
end
|
||||
params do
|
||||
optional :search, type: String, desc: 'Return list of users matching the search criteria'
|
||||
use :pagination
|
||||
end
|
||||
get ':id/starrers' do
|
||||
starrers = UsersStarProjectsFinder.new(user_project, params, current_user: current_user).execute
|
||||
|
||||
present paginate(starrers), with: Entities::UserStarsProject
|
||||
end
|
||||
|
||||
desc 'Get languages in project repository'
|
||||
get ':id/languages' do
|
||||
::Projects::RepositoryLanguagesService
|
||||
|
|
|
@ -7227,12 +7227,18 @@ msgstr ""
|
|||
msgid "No schedules"
|
||||
msgstr ""
|
||||
|
||||
msgid "No starrers matched your search"
|
||||
msgstr ""
|
||||
|
||||
msgid "No template"
|
||||
msgstr ""
|
||||
|
||||
msgid "No, directly import the existing email addresses and usernames."
|
||||
msgstr ""
|
||||
|
||||
msgid "Nobody has starred this repository yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8543,6 +8549,12 @@ msgstr ""
|
|||
msgid "ProjectOverview|Star"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectOverview|Starrer"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectOverview|Starrers"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectOverview|Unstar"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10268,6 +10280,9 @@ msgstr ""
|
|||
msgid "SortOptions|Oldest sign in"
|
||||
msgstr ""
|
||||
|
||||
msgid "SortOptions|Oldest starred"
|
||||
msgstr ""
|
||||
|
||||
msgid "SortOptions|Oldest updated"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10283,6 +10298,9 @@ msgstr ""
|
|||
msgid "SortOptions|Recent sign in"
|
||||
msgstr ""
|
||||
|
||||
msgid "SortOptions|Recently starred"
|
||||
msgstr ""
|
||||
|
||||
msgid "SortOptions|Sort direction"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10376,6 +10394,9 @@ msgstr ""
|
|||
msgid "StarredProjectsEmptyState|You don't have starred projects yet."
|
||||
msgstr ""
|
||||
|
||||
msgid "Starrers"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stars"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12218,6 +12239,12 @@ msgstr ""
|
|||
msgid "UserProfile|Snippets in GitLab can either be private, internal, or public."
|
||||
msgstr ""
|
||||
|
||||
msgid "UserProfile|Star projects to track their progress and show your appreciation."
|
||||
msgstr ""
|
||||
|
||||
msgid "UserProfile|Starred projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "UserProfile|Subscribe"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12230,6 +12257,9 @@ msgstr ""
|
|||
msgid "UserProfile|This user hasn't contributed to any projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "UserProfile|This user hasn't starred any projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "UserProfile|View all"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Projects::StarrersController do
|
||||
let(:user) { create(:user) }
|
||||
let(:private_user) { create(:user, private_profile: true) }
|
||||
let(:admin) { create(:user, admin: true) }
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
|
||||
before do
|
||||
user.toggle_star(project)
|
||||
private_user.toggle_star(project)
|
||||
end
|
||||
|
||||
describe 'GET index' do
|
||||
def get_starrers
|
||||
get :index,
|
||||
params: {
|
||||
namespace_id: project.namespace,
|
||||
project_id: project
|
||||
}
|
||||
end
|
||||
|
||||
context 'when project is public' do
|
||||
before do
|
||||
project.update_attribute(:visibility_level, Project::PUBLIC)
|
||||
end
|
||||
|
||||
context 'when no user is logged in' do
|
||||
before do
|
||||
get_starrers
|
||||
end
|
||||
|
||||
it 'only public starrers are visible' do
|
||||
user_ids = assigns[:starrers].map { |s| s['user_id'] }
|
||||
expect(user_ids).to include(user.id)
|
||||
expect(user_ids).not_to include(private_user.id)
|
||||
end
|
||||
|
||||
it 'public/private starrers counts are correct' do
|
||||
expect(assigns[:public_count]).to eq(1)
|
||||
expect(assigns[:private_count]).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when private user is logged in' do
|
||||
before do
|
||||
sign_in(private_user)
|
||||
|
||||
get_starrers
|
||||
end
|
||||
|
||||
it 'their star is also visible' do
|
||||
user_ids = assigns[:starrers].map { |s| s['user_id'] }
|
||||
expect(user_ids).to include(user.id, private_user.id)
|
||||
end
|
||||
|
||||
it 'public/private starrers counts are correct' do
|
||||
expect(assigns[:public_count]).to eq(1)
|
||||
expect(assigns[:private_count]).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when admin is logged in' do
|
||||
before do
|
||||
sign_in(admin)
|
||||
|
||||
get_starrers
|
||||
end
|
||||
|
||||
it 'all stars are visible' do
|
||||
user_ids = assigns[:starrers].map { |s| s['user_id'] }
|
||||
expect(user_ids).to include(user.id, private_user.id)
|
||||
end
|
||||
|
||||
it 'public/private starrers counts are correct' do
|
||||
expect(assigns[:public_count]).to eq(1)
|
||||
expect(assigns[:private_count]).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project is private' do
|
||||
before do
|
||||
project.update(visibility_level: Project::PRIVATE)
|
||||
end
|
||||
|
||||
it 'starrers are not visible for non logged in users' do
|
||||
get_starrers
|
||||
|
||||
expect(assigns[:starrers]).to be_blank
|
||||
end
|
||||
|
||||
context 'when user is logged in' do
|
||||
before do
|
||||
sign_in(project.creator)
|
||||
end
|
||||
|
||||
it 'only public starrers are visible' do
|
||||
get_starrers
|
||||
|
||||
user_ids = assigns[:starrers].map { |s| s['user_id'] }
|
||||
expect(user_ids).to include(user.id)
|
||||
expect(user_ids).not_to include(private_user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19,9 +19,9 @@ describe 'Search group member' do
|
|||
end
|
||||
|
||||
it 'renders member users' do
|
||||
page.within '.member-search-form' do
|
||||
page.within '.user-search-form' do
|
||||
fill_in 'search', with: member.name
|
||||
find('.member-search-btn').click
|
||||
find('.user-search-btn').click
|
||||
end
|
||||
|
||||
group_members_list = find(".card .content-list")
|
||||
|
|
|
@ -19,7 +19,7 @@ describe 'Groups > Members > Sort members' do
|
|||
|
||||
expect(first_member).to include(owner.name)
|
||||
expect(second_member).to include(developer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
|
||||
end
|
||||
|
||||
it 'sorts by access level ascending' do
|
||||
|
@ -27,7 +27,7 @@ describe 'Groups > Members > Sort members' do
|
|||
|
||||
expect(first_member).to include(developer.name)
|
||||
expect(second_member).to include(owner.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
|
||||
end
|
||||
|
||||
it 'sorts by access level descending' do
|
||||
|
@ -35,7 +35,7 @@ describe 'Groups > Members > Sort members' do
|
|||
|
||||
expect(first_member).to include(owner.name)
|
||||
expect(second_member).to include(developer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
|
||||
end
|
||||
|
||||
it 'sorts by last joined' do
|
||||
|
@ -43,7 +43,7 @@ describe 'Groups > Members > Sort members' do
|
|||
|
||||
expect(first_member).to include(developer.name)
|
||||
expect(second_member).to include(owner.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
|
||||
end
|
||||
|
||||
it 'sorts by oldest joined' do
|
||||
|
@ -51,7 +51,7 @@ describe 'Groups > Members > Sort members' do
|
|||
|
||||
expect(first_member).to include(owner.name)
|
||||
expect(second_member).to include(developer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
|
||||
end
|
||||
|
||||
it 'sorts by name ascending' do
|
||||
|
@ -59,7 +59,7 @@ describe 'Groups > Members > Sort members' do
|
|||
|
||||
expect(first_member).to include(owner.name)
|
||||
expect(second_member).to include(developer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
|
||||
end
|
||||
|
||||
it 'sorts by name descending' do
|
||||
|
@ -67,7 +67,7 @@ describe 'Groups > Members > Sort members' do
|
|||
|
||||
expect(first_member).to include(developer.name)
|
||||
expect(second_member).to include(owner.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
|
||||
end
|
||||
|
||||
it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
|
||||
|
@ -75,7 +75,7 @@ describe 'Groups > Members > Sort members' do
|
|||
|
||||
expect(first_member).to include(owner.name)
|
||||
expect(second_member).to include(developer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
|
||||
end
|
||||
|
||||
it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
|
||||
|
@ -83,7 +83,7 @@ describe 'Groups > Members > Sort members' do
|
|||
|
||||
expect(first_member).to include(developer.name)
|
||||
expect(second_member).to include(owner.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
|
||||
end
|
||||
|
||||
def visit_members_list(sort:)
|
||||
|
|
|
@ -52,18 +52,18 @@ describe 'Projects > Members > Groups with access list', :js do
|
|||
|
||||
context 'search in existing members (yes, this filters the groups list as well)' do
|
||||
it 'finds no results' do
|
||||
page.within '.member-search-form' do
|
||||
page.within '.user-search-form' do
|
||||
fill_in 'search', with: 'testing 123'
|
||||
find('.member-search-btn').click
|
||||
find('.user-search-btn').click
|
||||
end
|
||||
|
||||
expect(page).not_to have_selector('.group_member')
|
||||
end
|
||||
|
||||
it 'finds results' do
|
||||
page.within '.member-search-form' do
|
||||
page.within '.user-search-form' do
|
||||
fill_in 'search', with: group.name
|
||||
find('.member-search-btn').click
|
||||
find('.user-search-btn').click
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.group_member', count: 1)
|
||||
|
|
|
@ -18,7 +18,7 @@ describe 'Projects > Members > Sorting' do
|
|||
|
||||
expect(first_member).to include(maintainer.name)
|
||||
expect(second_member).to include(developer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
|
||||
end
|
||||
|
||||
it 'sorts by access level ascending' do
|
||||
|
@ -26,7 +26,7 @@ describe 'Projects > Members > Sorting' do
|
|||
|
||||
expect(first_member).to include(developer.name)
|
||||
expect(second_member).to include(maintainer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
|
||||
end
|
||||
|
||||
it 'sorts by access level descending' do
|
||||
|
@ -34,7 +34,7 @@ describe 'Projects > Members > Sorting' do
|
|||
|
||||
expect(first_member).to include(maintainer.name)
|
||||
expect(second_member).to include(developer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
|
||||
end
|
||||
|
||||
it 'sorts by last joined' do
|
||||
|
@ -42,7 +42,7 @@ describe 'Projects > Members > Sorting' do
|
|||
|
||||
expect(first_member).to include(maintainer.name)
|
||||
expect(second_member).to include(developer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
|
||||
end
|
||||
|
||||
it 'sorts by oldest joined' do
|
||||
|
@ -50,7 +50,7 @@ describe 'Projects > Members > Sorting' do
|
|||
|
||||
expect(first_member).to include(developer.name)
|
||||
expect(second_member).to include(maintainer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
|
||||
end
|
||||
|
||||
it 'sorts by name ascending' do
|
||||
|
@ -58,7 +58,7 @@ describe 'Projects > Members > Sorting' do
|
|||
|
||||
expect(first_member).to include(maintainer.name)
|
||||
expect(second_member).to include(developer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
|
||||
end
|
||||
|
||||
it 'sorts by name descending' do
|
||||
|
@ -66,7 +66,7 @@ describe 'Projects > Members > Sorting' do
|
|||
|
||||
expect(first_member).to include(developer.name)
|
||||
expect(second_member).to include(maintainer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
|
||||
end
|
||||
|
||||
it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
|
||||
|
@ -74,7 +74,7 @@ describe 'Projects > Members > Sorting' do
|
|||
|
||||
expect(first_member).to include(maintainer.name)
|
||||
expect(second_member).to include(developer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
|
||||
end
|
||||
|
||||
it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
|
||||
|
@ -82,7 +82,7 @@ describe 'Projects > Members > Sorting' do
|
|||
|
||||
expect(first_member).to include(developer.name)
|
||||
expect(second_member).to include(maintainer.name)
|
||||
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
|
||||
expect(page).to have_css('.user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
|
||||
end
|
||||
|
||||
def visit_members_list(sort:)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe StarredProjectsFinder do
|
||||
let(:project1) { create(:project, :public, :empty_repo) }
|
||||
let(:project2) { create(:project, :public, :empty_repo) }
|
||||
let(:other_project) { create(:project, :public, :empty_repo) }
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:other_user) { create(:user) }
|
||||
|
||||
before do
|
||||
user.toggle_star(project1)
|
||||
user.toggle_star(project2)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let(:finder) { described_class.new(user, params: {}, current_user: current_user) }
|
||||
|
||||
subject { finder.execute }
|
||||
|
||||
describe 'as same user' do
|
||||
let(:current_user) { user }
|
||||
|
||||
it { is_expected.to contain_exactly(project1, project2) }
|
||||
end
|
||||
|
||||
describe 'as other user' do
|
||||
let(:current_user) { other_user }
|
||||
|
||||
it { is_expected.to contain_exactly(project1, project2) }
|
||||
end
|
||||
|
||||
describe 'as no user' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to contain_exactly(project1, project2) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe UsersStarProjectsFinder do
|
||||
let(:project) { create(:project, :public, :empty_repo) }
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:private_user) { create(:user, private_profile: true) }
|
||||
let(:other_user) { create(:user) }
|
||||
|
||||
before do
|
||||
user.toggle_star(project)
|
||||
private_user.toggle_star(project)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let(:finder) { described_class.new(project, {}, current_user: current_user) }
|
||||
let(:public_stars) { user.users_star_projects }
|
||||
let(:private_stars) { private_user.users_star_projects }
|
||||
|
||||
subject { finder.execute }
|
||||
|
||||
describe 'as same user' do
|
||||
let(:current_user) { private_user }
|
||||
|
||||
it { is_expected.to match_array(private_stars + public_stars) }
|
||||
end
|
||||
|
||||
describe 'as other user' do
|
||||
let(:current_user) { other_user }
|
||||
|
||||
it { is_expected.to match_array(public_stars) }
|
||||
end
|
||||
|
||||
describe 'as no user' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to match_array(public_stars) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -27,7 +27,7 @@ describe UsersHelper do
|
|||
|
||||
context 'with public profile' do
|
||||
it 'includes all the expected tabs' do
|
||||
expect(tabs).to include(:activity, :groups, :contributed, :projects, :snippets)
|
||||
expect(tabs).to include(:activity, :groups, :contributed, :projects, :starred, :snippets)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -838,6 +838,28 @@ describe API::Projects do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /users/:user_id/starred_projects/' do
|
||||
before do
|
||||
user3.update(starred_projects: [project, project2, project3])
|
||||
end
|
||||
|
||||
it 'returns error when user not found' do
|
||||
get api('/users/9999/starred_projects/')
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
expect(json_response['message']).to eq('404 User Not Found')
|
||||
end
|
||||
|
||||
it 'returns projects filtered by user' do
|
||||
get api("/users/#{user3.id}/starred_projects/", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, project2.id, project3.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /projects/user/:id' do
|
||||
it 'creates new project without path but with name and return 201' do
|
||||
expect { post api("/projects/user/#{user.id}", admin), params: { name: 'Foo Project' } }.to change { Project.count }.by(1)
|
||||
|
@ -2148,6 +2170,85 @@ describe API::Projects do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/starrers' do
|
||||
shared_examples_for 'project starrers response' do
|
||||
it 'returns an array of starrers' do
|
||||
get api("/projects/#{public_project.id}/starrers", current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response[0]['starred_since']).to be_present
|
||||
expect(json_response[0]['user']).to be_present
|
||||
end
|
||||
|
||||
it 'returns the proper security headers' do
|
||||
get api('/projects/1/starrers', current_user)
|
||||
|
||||
expect(response).to include_security_headers
|
||||
end
|
||||
end
|
||||
|
||||
let(:public_project) { create(:project, :public) }
|
||||
let(:private_user) { create(:user, private_profile: true) }
|
||||
|
||||
before do
|
||||
user.update(starred_projects: [public_project])
|
||||
private_user.update(starred_projects: [public_project])
|
||||
end
|
||||
|
||||
it 'returns not_found(404) for not existing project' do
|
||||
get api("/projects/9999999999/starrers", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
context 'public project without user' do
|
||||
it_behaves_like 'project starrers response' do
|
||||
let(:current_user) { nil }
|
||||
end
|
||||
|
||||
it 'returns only starrers with a public profile' do
|
||||
get api("/projects/#{public_project.id}/starrers", nil)
|
||||
|
||||
user_ids = json_response.map { |s| s['user']['id'] }
|
||||
expect(user_ids).to include(user.id)
|
||||
expect(user_ids).not_to include(private_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'public project with user with private profile' do
|
||||
it_behaves_like 'project starrers response' do
|
||||
let(:current_user) { private_user }
|
||||
end
|
||||
|
||||
it 'returns current user with a private profile' do
|
||||
get api("/projects/#{public_project.id}/starrers", private_user)
|
||||
|
||||
user_ids = json_response.map { |s| s['user']['id'] }
|
||||
expect(user_ids).to include(user.id, private_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'private project' do
|
||||
context 'with unauthorized user' do
|
||||
it 'returns not_found for existing but unauthorized project' do
|
||||
get api("/projects/#{project3.id}/starrers", user3)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without user' do
|
||||
it 'returns not_found for existing but unauthorized project' do
|
||||
get api("/projects/#{project3.id}/starrers", nil)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/languages' do
|
||||
context 'with an authorized user' do
|
||||
it_behaves_like 'languages and percentages JSON response' do
|
||||
|
|
Loading…
Reference in New Issue