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