diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index cc75a4ad8fc9f10396a9c1076413e69d9cef6fdf..8e314ef053148e64c584a682d0a96e24634123d5 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -56,8 +56,8 @@
.user-profile-image {
.gl-avatar {
@include media-breakpoint-up(md) {
- height: 7.5rem;
- width: 7.5rem;
+ height: 6.5rem;
+ width: 6.5rem;
}
}
}
@@ -134,16 +134,6 @@
}
}
- .cover-controls {
- position: absolute;
- top: $gl-padding-24;
- right: -$gl-padding-8;
-
- @include media-breakpoint-up(lg) {
- position: static;
- }
- }
-
.user-profile-nav {
font-size: 0;
}
@@ -235,8 +225,8 @@
@include media-breakpoint-up(lg) {
display: grid;
- grid-template-columns: minmax(240px, 1fr) 3fr;
- gap: 3rem;
+ grid-template-columns: 1fr $right-sidebar-width;
+ gap: 2rem;
}
}
@@ -253,3 +243,26 @@
// make contrib calendar scrollable
overflow-x: auto;
}
+
+// Home panel show profile sidebar
+// information on top
+.user-profile {
+ @include media-breakpoint-down(md) {
+ display: flex;
+ flex-direction: column;
+
+ .user-overview-page {
+ display: flex;
+ flex-wrap: wrap;
+
+ .user-profile-content {
+ flex-basis: 100%;
+ }
+ }
+
+ .user-profile-sidebar {
+ order: -1;
+ flex-basis: 100%;
+ }
+ }
+}
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index e6fd5618582c7145f80a07c221b6f1ae0319c61d..8388ee16bfbc2e7ebd486efa85a6c50a92d57bdb 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -13,44 +13,110 @@
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
%div{ class: container_class }
+ .user-profile-header.gl-display-flex.gl-justify-content-space-between.gl-flex-direction-column.gl-sm-flex-direction-row-reverse.gl-mt-5.gl-mb-2
+ %div
+ .cover-controls.gl-display-flex.gl-gap-3.gl-mb-4.gl-sm-justify-content-end
+ = render 'users/follow_user'
+ -# The following edit button is mutually exclusive to the follow user button, they won't be shown together
+ - if @user == current_user
+ = render Pajamas::ButtonComponent.new(href: profile_path,
+ button_options: { title: s_('UserProfile|Edit profile') }) do
+ = s_("UserProfile|Edit profile")
+ = render 'users/view_gpg_keys'
+ = render 'users/view_user_in_admin_area'
+ .js-user-profile-actions{ data: user_profile_actions_data(@user) }
+ .gl-display-flex.gl-flex-direction-row.gl-md-align-items-center.gl-column-gap-5.gl-mt-2.gl-sm-mt-0
+ .user-image.gl-relative.gl-py-2
+ = link_to avatar_icon_for_user(@user, 400, current_user: current_user), class: "user-profile-image", target: '_blank', rel: 'noopener noreferrer', title: s_('UserProfile|View large avatar') do
+ = render Pajamas::AvatarComponent.new(@user, alt: s_('UserProfile|User profile picture'), size: 64, avatar_options: { itemprop: "image" })
+ - if @user.status&.busy?
+ = render Pajamas::BadgeComponent.new(s_('UserProfile|Busy'), size: 'sm', variant: 'warning', class: 'gl-absolute gl-display-flex gl-justify-content-center gl-align-items-center gl-bottom-0 gl-left-50p gl-bg-gray-50 gl-border gl-border-white gl-translate-x-n50')
+ %div
+ %h1.gl-font-size-h1.gl-line-height-1.gl-mb-0.gl-mt-0.gl-mr-2{ itemprop: 'name' }
+ = user_display_name(@user)
+ .gl-font-size-h2.gl-text-gray-600.gl-font-weight-normal.gl-my-0
+ = @user.to_reference
+ - if !@user.blocked? && @user.confirmed? && @user.status&.customized?
+ .gl-my-2.cover-status.gl-font-sm.gl-border-l.gl-border-3.gl-px-3.gl-py-2.gl-display-flex.gl-flex-direction-column
+ .gl-display-inline-flex.gl-gap-3.gl-align-items-center
+ = emoji_icon(@user.status.emoji)
+ = markdown_field(@user.status, :message)
.user-profile
- .user-profile-sidebar
- .profile-header.gl-py-6.gl-overflow-y-auto.gl-sm-pr-4
+ .user-profile-content.gl-pt-5
+ - if !profile_tabs.empty? && Feature.enabled?(:profile_tabs_vue, current_user)
+ #js-profile-tabs{ data: user_profile_tabs_app_data(@user) }
+ - unless Feature.enabled?(:profile_tabs_vue, current_user)
+ .tab-content.gl-overflow-hidden
+ - if profile_tab?(:overview)
+ #js-overview.tab-pane.user-overview-page
+ = render "users/overview"
- = link_to avatar_icon_for_user(@user, 400, current_user: current_user), class: "user-profile-image", target: '_blank', rel: 'noopener noreferrer', alt: 'View large avatar' do
- = render Pajamas::AvatarComponent.new(@user, alt: "", size: 96, avatar_options: { itemprop: "image" })
+ - if profile_tab?(:activity)
+ #activity.tab-pane
+ .flash-container
+ - if can?(current_user, :read_cross_project)
+ .content_list.user-activity-content{ data: { href: user_activity_path } }
+ .loading
+ = gl_loading_icon(size: 'md')
+ - unless @user.bot?
+ - if profile_tab?(:groups)
+ #groups.tab-pane
+ -# This tab is always loaded via AJAX
+
+ - if profile_tab?(:contributed)
+ #contributed.tab-pane
+ -# This tab is always loaded via AJAX
+ - if profile_tab?(:projects)
+ #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
+
+ - if profile_tab?(:followers)
+ #followers.tab-pane
+ -# This tab is always loaded via AJAX
+
+ - if profile_tab?(:following)
+ #following.tab-pane
+ -# This tab is always loaded via AJAX
+
+ .loading.hide
+ .gl-spinner.gl-spinner-md
+
+ - if profile_tabs.empty?
+ .svg-content
+ = image_tag 'illustrations/profile_private_mode.svg'
+ .text-content.text-center
+ %h4
+ - if @user.blocked?
+ = s_('UserProfile|This user is blocked')
+ - else
+ = s_('UserProfile|This user has a private profile')
+ .user-profile-sidebar
+ .profile-header.gl-py-5.gl-overflow-y-auto.gl-sm-pr-4
.gl-vertical-align-top.gl-text-left.gl-max-w-80.gl-overflow-wrap-anywhere
.user-info
- %h1.gl-font-size-h1.gl-line-height-1.gl-mb-1{ itemprop: 'name' }
- = user_display_name(@user)
- %h2.gl-font-size-h2.gl-text-gray-600.gl-font-weight-normal.gl-mt-0
- = @user.to_reference
-
- if !@user.blocked? && @user.confirmed?
.gl-display-flex.gl-gap-4.gl-flex-direction-column
- - if @user.pronouns.present? || @user.pronunciation.present?
- .gl-font-sm
- - if @user.pronunciation.present?
- %p.gl-mb-0
- = s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation }
- - if @user.pronouns.present?
- %p.gl-mb-0
- = s_("UserProfile|Pronouns: %{pronouns}") % { pronouns: @user.pronouns }
-
- - if @user.status&.customized? || @user.status&.busy?
- .cover-status.gl-font-sm.gl-border-l.gl-border-3.gl-px-3.gl-py-2.gl-bg-gray-10.gl-display-flex.gl-flex-direction-column.gl-gap-2
- - if @user.status&.busy?
- %div
- = render Pajamas::BadgeComponent.new(s_('UserProfile|Busy'), size: 'sm', variant: 'warning', class: 'gl-vertical-align-middle')
- - if @user.status&.customized?
- .gl-display-inline-flex.gl-gap-3.gl-align-items-baseline
- = emoji_icon(@user.status.emoji)
- = markdown_field(@user.status, :message)
-
- - if @user.bio.present? && @user.confirmed? && !@user.blocked?
- %p.profile-user-bio.gl-mb-0
- = @user.bio
+ - if @user.pronouns.present? || @user.pronunciation.present? || @user.bio.present?
+ %div
+ %h3.h5.gl-mb-2= s_('UserProfile|About')
+ - if @user.pronouns.present? || @user.pronunciation.present?
+ .gl-mt-2.gl-mb-4
+ - if @user.pronunciation.present?
+ = sprintf(s_("UserProfile|Pronounced as: %{div_start}%{pronunciation}%{div_end}"), { pronunciation: @user.pronunciation, div_start: '
', div_end: '
' }).html_safe
+ - if @user.pronouns.present?
+ = sprintf(s_("UserProfile|Pronouns: %{div_start}%{pronouns}%{div_end}"), { pronouns: @user.pronouns, div_start: '', div_end: '
' }).html_safe
+ - if @user.bio.present?
+ %p.profile-user-bio.gl-mb-0
+ = @user.bio
- if @user.achievements_enabled && Ability.allowed?(current_user, :read_user_profile, @user)
#js-user-achievements{ data: { root_url: root_url, user_id: @user.id } }
@@ -159,72 +225,3 @@
= s_('UserProfile|Following')
= gl_badge_tag @user.followees.count, size: :sm
- .user-profile-content.gl-pt-6
- .cover-controls.gl-display-flex.gl-gap-3.gl-mb-4.gl-md-justify-content-end
- = render 'users/follow_user'
- -# The following edit button is mutually exclusive to the follow user button, they won't be shown together
- - if @user == current_user
- = render Pajamas::ButtonComponent.new(href: profile_path,
- button_options: { title: s_('UserProfile|Edit profile') }) do
- = s_("UserProfile|Edit profile")
- = render 'users/view_gpg_keys'
- = render 'users/view_user_in_admin_area'
- .js-user-profile-actions{ data: user_profile_actions_data(@user) }
- - if !profile_tabs.empty? && Feature.enabled?(:profile_tabs_vue, current_user)
- #js-profile-tabs{ data: user_profile_tabs_app_data(@user) }
- - unless Feature.enabled?(:profile_tabs_vue, current_user)
- .tab-content.gl-overflow-hidden
- - if profile_tab?(:overview)
- #js-overview.tab-pane
- = render "users/overview"
-
- - if profile_tab?(:activity)
- #activity.tab-pane
- .flash-container
- - if can?(current_user, :read_cross_project)
- %h4.gl-mt-0
- = s_('UserProfile|Most Recent Activity')
- .content_list.user-activity-content{ data: { href: user_activity_path } }
- .loading
- = gl_loading_icon(size: 'md')
- - unless @user.bot?
- - if profile_tab?(:groups)
- #groups.tab-pane
- -# This tab is always loaded via AJAX
-
- - if profile_tab?(:contributed)
- #contributed.tab-pane
- -# This tab is always loaded via AJAX
-
- - if profile_tab?(:projects)
- #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
-
- - if profile_tab?(:followers)
- #followers.tab-pane
- -# This tab is always loaded via AJAX
-
- - if profile_tab?(:following)
- #following.tab-pane
- -# This tab is always loaded via AJAX
-
- .loading.hide
- .gl-spinner.gl-spinner-md
-
- - if profile_tabs.empty?
- .svg-content
- = image_tag 'illustrations/profile_private_mode.svg'
- .text-content.text-center
- %h4
- - if @user.blocked?
- = s_('UserProfile|This user is blocked')
- - else
- = s_('UserProfile|This user has a private profile')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ce1a285a417acfc81e5e57d40c59a414445bf771..f5e9048fdeb8e9183612018a79b284d61ea2c4ec 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -52806,6 +52806,9 @@ msgstr ""
msgid "UserProfile|%{id} ยท created %{created} by %{author}"
msgstr ""
+msgid "UserProfile|About"
+msgstr ""
+
msgid "UserProfile|Activity"
msgstr ""
@@ -52869,9 +52872,6 @@ msgstr ""
msgid "UserProfile|Join or create a group to start contributing by commenting on issues or submitting merge requests!"
msgstr ""
-msgid "UserProfile|Most Recent Activity"
-msgstr ""
-
msgid "UserProfile|No snippets found."
msgstr ""
@@ -52881,10 +52881,10 @@ msgstr ""
msgid "UserProfile|Personal projects"
msgstr ""
-msgid "UserProfile|Pronounced as: %{pronunciation}"
+msgid "UserProfile|Pronounced as: %{div_start}%{pronunciation}%{div_end}"
msgstr ""
-msgid "UserProfile|Pronouns: %{pronouns}"
+msgid "UserProfile|Pronouns: %{div_start}%{pronouns}%{div_end}"
msgstr ""
msgid "UserProfile|Retry"
@@ -52947,9 +52947,15 @@ msgstr ""
msgid "UserProfile|User profile navigation"
msgstr ""
+msgid "UserProfile|User profile picture"
+msgstr ""
+
msgid "UserProfile|View all"
msgstr ""
+msgid "UserProfile|View large avatar"
+msgstr ""
+
msgid "UserProfile|View user in admin area"
msgstr ""
diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
index 7e4308106be6359a401a284c6690b533d4336e9f..08d9ec5f3a9f364b681f8d407323c3f064ff5142 100644
--- a/spec/features/profiles/account_spec.rb
+++ b/spec/features/profiles/account_spec.rb
@@ -51,14 +51,14 @@
update_username(new_username)
visit new_user_path
expect(page).to have_current_path(new_user_path, ignore_query: true)
- expect(find('.user-info')).to have_content(new_username)
+ expect(find('.user-profile-header')).to have_content(new_username)
end
it 'the old user path redirects to the new path' do
update_username(new_username)
visit old_user_path
expect(page).to have_current_path(new_user_path, ignore_query: true)
- expect(find('.user-info')).to have_content(new_username)
+ expect(find('.user-profile-header')).to have_content(new_username)
end
context 'with a project' do
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index 675c68da22fe18a99cc22c55b65ab7b53ef98b07..9460cf1264416aad83f80ebbac9c185c4911732c 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -48,7 +48,7 @@
it 'shows expected content', :js do
visit(user_path(user))
- page.within ".user-info" do
+ page.within ".user-profile-header" do
expect(page).to have_content user.name
expect(page).to have_content user.username
end
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 5d121d9eeba9c9b06e4a4be21cb2d4bdc2c9aa2a..bdad7781701b21e6fe6f990f02acf532c39b125f 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -51,7 +51,7 @@
visit user_path(user)
- expect(page).to have_selector(%(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=96"]))
+ expect(page).to have_selector(%(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=64"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
index 5fcaee0211d529f1d29177be605e636f5050bd09..f0ca0715e145d6896b824d04b3d138f31d8dee54 100644
--- a/spec/features/users/rss_spec.rb
+++ b/spec/features/users/rss_spec.rb
@@ -13,7 +13,7 @@
end
it 'shows the RSS link with overflow menu', :js do
- page.within('.user-profile-content') do
+ page.within('.user-profile-header') do
find_by_testid('base-dropdown-toggle').click
end
@@ -27,7 +27,7 @@
end
it 'has an RSS without a feed token', :js do
- page.within('.user-profile-content') do
+ page.within('.user-profile-header') do
find_by_testid('base-dropdown-toggle').click
end
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index e0c41155c2b42a71e8a9bc105d38882ec45b59d6..754de6ed67f280859bcc5ec96a4f7ce441ca55d1 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -305,7 +305,7 @@
end
it 'shows user name as blocked' do
- expect(page).to have_css(".user-info", text: 'Blocked user')
+ expect(page).to have_css(".user-profile-header", text: 'Blocked user')
end
it 'shows no additional fields' do
@@ -343,7 +343,7 @@
end
it 'shows user name as unconfirmed' do
- expect(page).to have_css(".user-info", text: 'Unconfirmed user')
+ expect(page).to have_css(".user-profile-header", text: 'Unconfirmed user')
end
it 'shows no tab' do
@@ -435,12 +435,6 @@
stub_feature_flags(profile_tabs_vue: false)
end
- it 'shows the most recent activity' do
- subject
-
- expect(page).to have_content('Most Recent Activity')
- end
-
context 'when external authorization is enabled' do
before do
enable_external_authorization_service_check