Commit e8627751 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'ee-51944-redesign-project-lists-ui' into 'master'

(EE port) Resolve "Redesign project lists UI"

See merge request gitlab-org/gitlab-ee!8825
parents 4e877960 a551e547
import ProjectsList from '~/projects_list'; import ProjectsList from '~/projects_list';
import Star from '../../../star';
document.addEventListener('DOMContentLoaded', () => new ProjectsList()); document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
new Star('.project-row'); // eslint-disable-line no-new
});
// if the "projects dashboard" is a user's default dashboard, when they visit the
// instance root index, the dashboard will be served by the root controller instead
// of a dashboard controller. The root index redirects for all other default dashboards.
import '../dashboard/projects/index';
...@@ -151,8 +151,10 @@ export default class UserTabs { ...@@ -151,8 +151,10 @@ export default class UserTabs {
loadTab(action, endpoint) { loadTab(action, endpoint) {
this.toggleLoading(true); this.toggleLoading(true);
const params = action === 'projects' ? { skip_namespace: true } : {};
return axios return axios
.get(endpoint) .get(endpoint, { params })
.then(({ data }) => { .then(({ data }) => {
const tabSelector = `div#${action}`; const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html); this.$parentEl.find(tabSelector).html(data.html);
...@@ -188,7 +190,7 @@ export default class UserTabs { ...@@ -188,7 +190,7 @@ export default class UserTabs {
requestParams: { limit: 10 }, requestParams: { limit: 10 },
}); });
UserTabs.renderMostRecentBlocks('#js-overview .projects-block', { UserTabs.renderMostRecentBlocks('#js-overview .projects-block', {
requestParams: { limit: 10, skip_pagination: true }, requestParams: { limit: 10, skip_pagination: true, skip_namespace: true, compact_mode: true },
}); });
this.loaded.overview = true; this.loaded.overview = true;
......
...@@ -5,11 +5,12 @@ import { spriteIcon } from './lib/utils/common_utils'; ...@@ -5,11 +5,12 @@ import { spriteIcon } from './lib/utils/common_utils';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
export default class Star { export default class Star {
constructor() { constructor(container = '.project-home-panel') {
$('.project-home-panel .toggle-star').on('click', function toggleStarClickCallback() { $(`${container} .toggle-star`).on('click', function toggleStarClickCallback() {
const $this = $(this); const $this = $(this);
const $starSpan = $this.find('span'); const $starSpan = $this.find('span');
const $startIcon = $this.find('svg'); const $starIcon = $this.find('svg');
const iconClasses = $starIcon.attr('class').split(' ');
axios axios
.post($this.data('endpoint')) .post($this.data('endpoint'))
...@@ -22,12 +23,12 @@ export default class Star { ...@@ -22,12 +23,12 @@ export default class Star {
if (isStarred) { if (isStarred) {
$starSpan.removeClass('starred').text(s__('StarProject|Star')); $starSpan.removeClass('starred').text(s__('StarProject|Star'));
$startIcon.remove(); $starIcon.remove();
$this.prepend(spriteIcon('star-o', 'icon')); $this.prepend(spriteIcon('star-o', iconClasses));
} else { } else {
$starSpan.addClass('starred').text(__('Unstar')); $starSpan.addClass('starred').text(__('Unstar'));
$startIcon.remove(); $starIcon.remove();
$this.prepend(spriteIcon('star', 'icon')); $this.prepend(spriteIcon('star', iconClasses));
} }
}) })
.catch(() => Flash('Star toggle failed. Try again later.')); .catch(() => Flash('Star toggle failed. Try again later.'));
......
...@@ -112,6 +112,7 @@ ...@@ -112,6 +112,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
text-decoration: none;
} }
.avatar { .avatar {
...@@ -124,6 +125,7 @@ ...@@ -124,6 +125,7 @@
} }
&.s40 { min-width: 40px; min-height: 40px; } &.s40 { min-width: 40px; min-height: 40px; }
&.s64 { min-width: 64px; min-height: 64px; }
} }
.avatar-counter { .avatar-counter {
......
...@@ -199,6 +199,7 @@ $well-light-text-color: #5b6169; ...@@ -199,6 +199,7 @@ $well-light-text-color: #5b6169;
$gl-font-size: 14px; $gl-font-size: 14px;
$gl-font-size-xs: 11px; $gl-font-size-xs: 11px;
$gl-font-size-small: 12px; $gl-font-size-small: 12px;
$gl-font-size-medium: 1.43rem;
$gl-font-size-large: 16px; $gl-font-size-large: 16px;
$gl-font-weight-normal: 400; $gl-font-weight-normal: 400;
$gl-font-weight-bold: 600; $gl-font-weight-bold: 600;
...@@ -277,6 +278,7 @@ $project-title-row-height: 64px; ...@@ -277,6 +278,7 @@ $project-title-row-height: 64px;
$project-avatar-mobile-size: 24px; $project-avatar-mobile-size: 24px;
$gl-line-height: 16px; $gl-line-height: 16px;
$gl-line-height-24: 24px; $gl-line-height-24: 24px;
$gl-line-height-14: 14px;
// EE-only CSS variables START // EE-only CSS variables START
$system-header-height: 35px; $system-header-height: 35px;
......
...@@ -969,34 +969,73 @@ pre.light-well { ...@@ -969,34 +969,73 @@ pre.light-well {
@include basic-list-stats; @include basic-list-stats;
display: flex; display: flex;
align-items: center; align-items: center;
color: $gl-text-color-secondary;
padding: $gl-padding 0;
@include media-breakpoint-up(lg) {
padding: $gl-padding-24 0;
} }
h3 { &.no-description {
font-size: $gl-font-size; @include media-breakpoint-up(sm) {
.avatar-container {
align-self: center;
} }
.avatar-container, .metadata-info {
.controls { margin-bottom: 0;
flex: 0 0 auto; }
}
}
}
h2 {
font-size: $gl-font-size-medium;
font-weight: $gl-font-weight-bold;
margin-bottom: 0;
@include media-breakpoint-up(sm) {
.namespace-name {
font-weight: $gl-font-weight-normal;
}
}
} }
.avatar-container { .avatar-container {
flex: 0 0 auto;
align-self: flex-start; align-self: flex-start;
} }
.project-details { .project-details {
min-width: 0; min-width: 0;
line-height: $gl-line-height;
.flex-wrapper {
min-width: 0;
margin-top: -$gl-padding-8; // negative margin required for flex-wrap
}
p, p,
.commit-row-message { .commit-row-message {
@include str-truncated(100%); @include str-truncated(100%);
margin-bottom: 0; margin-bottom: 0;
} }
.user-access-role {
margin: 0;
} }
.controls { @include media-breakpoint-up(md) {
margin-left: auto; .description {
text-align: right; color: $gl-text-color;
}
}
@include media-breakpoint-down(md) {
.user-access-role {
line-height: $gl-line-height-14;
}
}
} }
.ci-status-link { .ci-status-link {
...@@ -1008,6 +1047,149 @@ pre.light-well { ...@@ -1008,6 +1047,149 @@ pre.light-well {
text-decoration: none; text-decoration: none;
} }
} }
.controls {
margin-top: $gl-padding;
@include media-breakpoint-down(md) {
margin-top: 0;
}
@include media-breakpoint-down(xs) {
margin-top: $gl-padding-8;
}
.icon-wrapper {
color: inherit;
margin-right: $gl-padding;
@include media-breakpoint-down(md) {
margin-right: 0;
margin-left: $gl-padding-8;
}
@include media-breakpoint-down(xs) {
&:first-child {
margin-left: 0;
}
}
}
.ci-status-link {
display: inline-flex;
}
}
.star-button {
.icon {
top: 0;
}
}
.icon-container {
@include media-breakpoint-down(xs) {
margin-right: $gl-padding-8;
}
}
&.compact {
.project-row {
padding: $gl-padding 0;
}
h2 {
font-size: $gl-font-size;
}
.avatar-container {
@include avatar-size(40px, 10px);
min-height: 40px;
min-width: 40px;
.identicon.s64 {
font-size: 16px;
}
}
.controls {
@include media-breakpoint-up(sm) {
margin-top: 0;
}
}
.updated-note {
@include media-breakpoint-up(sm) {
margin-top: $gl-padding-8;
}
}
.icon-wrapper {
margin-left: $gl-padding-8;
margin-right: 0;
@include media-breakpoint-down(xs) {
&:first-child {
margin-left: 0;
}
}
}
.user-access-role {
line-height: $gl-line-height-14;
}
}
@include media-breakpoint-down(md) {
h2 {
font-size: $gl-font-size;
}
.avatar-container {
@include avatar-size(40px, 10px);
min-height: 40px;
min-width: 40px;
.identicon.s64 {
font-size: 16px;
}
}
}
@include media-breakpoint-down(md) {
.updated-note {
margin-top: $gl-padding-8;
text-align: right;
}
}
.forks,
.pipeline-status,
.updated-note {
display: flex;
}
@include media-breakpoint-down(md) {
&:not(.explore) {
.forks {
display: none;
}
}
&.explore {
.pipeline-status,
.updated-note {
display: none !important;
}
}
}
@include media-breakpoint-down(xs) {
.updated-note {
margin-top: 0;
text-align: left;
}
}
} }
.card .projects-list li { .card .projects-list li {
......
...@@ -57,11 +57,13 @@ class UsersController < ApplicationController ...@@ -57,11 +57,13 @@ class UsersController < ApplicationController
load_projects load_projects
skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination]) 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| respond_to do |format|
format.html { render 'show' } format.html { render 'show' }
format.json do format.json do
pager_json("shared/projects/_list", @projects.count, projects: @projects, skip_pagination: skip_pagination) pager_json("shared/projects/_list", @projects.count, projects: @projects, skip_pagination: skip_pagination, skip_namespace: skip_namespace, compact_mode: compact_mode)
end end
end end
end end
......
...@@ -517,6 +517,20 @@ module ProjectsHelper ...@@ -517,6 +517,20 @@ module ProjectsHelper
end end
end end
def explore_projects_tab?
current_page?(explore_projects_path) ||
current_page?(trending_explore_projects_path) ||
current_page?(starred_explore_projects_path)
end
def show_merge_request_count?(merge_requests, compact_mode)
merge_requests && !compact_mode && Feature.enabled?(:project_list_show_mr_count, default_enabled: true)
end
def show_issue_count?(issues, compact_mode)
issues && !compact_mode && Feature.enabled?(:project_list_show_issue_count, default_enabled: true)
end
def sidebar_projects_paths def sidebar_projects_paths
%w[ %w[
projects#show projects#show
......
...@@ -2,24 +2,29 @@ ...@@ -2,24 +2,29 @@
- avatar = true unless local_assigns[:avatar] == false - avatar = true unless local_assigns[:avatar] == false
- use_creator_avatar = false unless local_assigns[:use_creator_avatar] == true - use_creator_avatar = false unless local_assigns[:use_creator_avatar] == true
- stars = true unless local_assigns[:stars] == false - stars = true unless local_assigns[:stars] == false
- forks = false unless local_assigns[:forks] == true - forks = true unless local_assigns[:forks] == false
- merge_requests = true unless local_assigns[:merge_requests] == false
- issues = true unless local_assigns[:issues] == false
- pipeline_status = true unless local_assigns[:pipeline_status] == false
- ci = false unless local_assigns[:ci] == true - ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user] - user = local_assigns[:user]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true - remote = false unless local_assigns[:remote] == true
- skip_pagination = false unless local_assigns[:skip_pagination] == true - skip_pagination = false unless local_assigns[:skip_pagination] == true
- compact_mode = false unless local_assigns[:compact_mode] == true
- css_classes = "#{'compact' if compact_mode} #{'explore' if explore_projects_tab?}"
.js-projects-list-holder .js-projects-list-holder
- if any_projects?(projects) - if any_projects?(projects)
- load_pipeline_status(projects) - load_pipeline_status(projects)
%ul.projects-list{ class: css_classes }
%ul.projects-list
- projects.each_with_index do |project, i| - projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace, = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user, merge_requests: merge_requests,
issues: issues, pipeline_status: pipeline_status, compact_mode: compact_mode
- if @private_forks_count && @private_forks_count > 0 - if @private_forks_count && @private_forks_count > 0
%li.project-row.private-forks-notice %li.project-row.private-forks-notice
......
- avatar = true unless local_assigns[:avatar] == false - avatar = true unless local_assigns[:avatar] == false
- stars = true unless local_assigns[:stars] == false - stars = true unless local_assigns[:stars] == false
- forks = false unless local_assigns[:forks] == true - forks = true unless local_assigns[:forks] == false
- merge_requests = true unless local_assigns[:merge_requests] == false
- issues = true unless local_assigns[:issues] == false
- pipeline_status = true unless local_assigns[:pipeline_status] == false
- skip_namespace = false unless local_assigns[:skip_namespace] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true
- access = max_project_member_access(project) - access = max_project_member_access(project)
- css_class = '' unless local_assigns[:css_class] - compact_mode = false unless local_assigns[:compact_mode] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project) - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project) - cache_key = project_list_cache_key(project)
- updated_tooltip = time_ago_with_tooltip(project.last_activity_date) - updated_tooltip = time_ago_with_tooltip(project.last_activity_date)
- css_details_class = compact_mode ? "d-flex flex-column flex-sm-row flex-md-row align-items-sm-center" : "align-items-center flex-md-fill flex-lg-column d-sm-flex d-lg-block"
- css_controls_class = compact_mode ? "" : "align-items-md-end align-items-lg-center flex-lg-row"
%li.project-row{ class: css_class } %li.project-row.d-flex{ class: css_class }
= cache(cache_key) do = cache(cache_key) do
- if avatar - if avatar
.avatar-container.s40 .avatar-container.s64.flex-grow-0.flex-shrink-0
= link_to project_path(project), class: dom_class(project) do = link_to project_path(project), class: dom_class(project) do
- if project.creator && use_creator_avatar - if project.creator && use_creator_avatar
= image_tag avatar_icon_for_user(project.creator, 40), class: "avatar s40", alt:'' = image_tag avatar_icon_for_user(project.creator, 64), class: "avatar s65", alt:''
- else - else
= project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40) = project_icon(project, alt: '', class: 'avatar project-avatar s64', width: 64, height: 64)
.project-details .project-details.flex-sm-fill{ class: css_details_class }
%h3.prepend-top-0.append-bottom-0 .flex-wrapper.flex-fill
.d-flex.align-items-center.flex-wrap
%h2.d-flex.prepend-top-8
= link_to project_path(project), class: 'text-plain' do = link_to project_path(project), class: 'text-plain' do
%span.project-full-name>< %span.project-full-name.append-right-8><
%span.namespace-name %span.namespace-name
- if project.namespace && !skip_namespace - if project.namespace && !skip_namespace
= project.namespace.human_name = project.namespace.human_name
...@@ -29,34 +37,71 @@ ...@@ -29,34 +37,71 @@
%span.project-name< %span.project-name<
= project.name = project.name
- if access&.nonzero? %span.metadata-info.visibility-icon.append-right-10.prepend-top-8.has-tooltip{ data: { container: 'body', placement: 'top' }, title: visibility_icon_description(project) }
= visibility_level_icon(project.visibility_level, fw: true)
- if explore_projects_tab? && project.repository.license
%span.metadata-info.d-inline-flex.align-items-center.append-right-10.prepend-top-8
= sprite_icon('scale', size: 14, css_class: 'append-right-4')
= project.repository.license.name
- if !explore_projects_tab? && access&.nonzero?
-# haml-lint:disable UnnecessaryStringOutput -# haml-lint:disable UnnecessaryStringOutput
= ' ' # prevent haml from eating the space between elements = ' ' # prevent haml from eating the space between elements
%span.user-access-role= Gitlab::Access.human_access(access) .metadata-info.prepend-top-8
%span.user-access-role.d-block= Gitlab::Access.human_access(access)
- if show_last_commit_as_description - if show_last_commit_as_description
.description.prepend-top-5 .description.d-none.d-sm-block.prepend-top-8.append-right-default
= link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message") = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
- elsif project.description.present? - elsif project.description.present?
.description.prepend-top-5 .description.d-none.d-sm-block.prepend-top-8.append-right-default
= markdown_field(project, :description) = markdown_field(project, :description)
.controls .controls.d-flex.flex-row.flex-sm-column.flex-md-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class }
.prepend-top-0 .icon-container.d-flex.align-items-center
- if project.archived - if project.archived
%span.prepend-left-10.badge.badge-warning archived %span.d-flex.icon-wrapper.badge.badge-warning archived
- if can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
%span.prepend-left-10
= render_project_pipeline_status(project.pipeline_status)
- if forks
%span.prepend-left-10
= sprite_icon('fork', size: 12)
= number_with_delimiter(project.forks_count)
- if stars - if stars
%span.prepend-left-10 %span.d-flex.align-items-center.icon-wrapper.stars.has-tooltip{ data: { container: 'body', placement: 'top' }, title: _('Stars') }
= icon('star') = sprite_icon('star', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.star_count) = number_with_delimiter(project.star_count)
%span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } - if forks
= visibility_level_icon(project.visibility_level, fw: true) = link_to project_forks_path(project),
.prepend-top-0 class: "align-items-center icon-wrapper forks has-tooltip",
updated #{updated_tooltip} title: _('Forks'), data: { container: 'body', placement: 'top' } do
= sprite_icon('fork', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.forks_count)
- if show_merge_request_count?(merge_requests, compact_mode)
= link_to project_merge_requests_path(project),
class: "d-none d-lg-flex align-items-center icon-wrapper merge-requests has-tooltip",
title: _('Merge Requests'), data: { container: 'body', placement: 'top' } do
= sprite_icon('git-merge', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.open_merge_requests_count)
- if show_issue_count?(issues, compact_mode)
= link_to project_issues_path(project),
class: "d-none d-lg-flex align-items-center icon-wrapper issues has-tooltip",
title: _('Issues'), data: { container: 'body', placement: 'top' } do
= sprite_icon('issues', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.open_issues_count)
- if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
%span.icon-wrapper.pipeline-status
= render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top')
.updated-note
%span Updated #{updated_tooltip}
.d-none.d-lg-flex.align-item-stretch
- unless compact_mode
- if current_user
%button.star-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(project, :json) } }
- if current_user.starred?(project)
= sprite_icon('star', { css_class: 'icon' })
%span.starred= s_('ProjectOverview|Unstar')
- else
= sprite_icon('star-o', { css_class: 'icon' })
%span= s_('ProjectOverview|Star')
- else
= link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
= sprite_icon('star-o', { css_class: 'icon' })
%span= s_('ProjectOverview|Star')
---
title: Redesign project lists UI
merge_request: 22682
author:
type: other
...@@ -3771,6 +3771,9 @@ msgstr "" ...@@ -3771,6 +3771,9 @@ msgstr ""
msgid "Forking in progress" msgid "Forking in progress"
msgstr "" msgstr ""
msgid "Forks"
msgstr ""
msgid "Format" msgid "Format"
msgstr "" msgstr ""
...@@ -8187,6 +8190,9 @@ msgstr "" ...@@ -8187,6 +8190,9 @@ msgstr ""
msgid "Starred projects" msgid "Starred projects"
msgstr "" msgstr ""
msgid "Stars"
msgstr ""
msgid "Start Web Terminal" msgid "Start Web Terminal"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment