From 932172b45c445dca7675256394a0efd33ddfb8e3 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 14 Nov 2023 13:16:31 -0700 Subject: [PATCH 01/19] Reorganize profile page layout - Create sidebar with user info - Group info types - Move other content to right column - Add border to readme and activity calendar - Move activity directly under activity graph Changelog: changed --- .../stylesheets/page_bundles/profile.scss | 19 ++ app/views/users/_overview.html.haml | 69 ++-- .../users/_view_user_in_admin_area.html.haml | 2 +- app/views/users/show.html.haml | 317 +++++++++--------- locale/gitlab.pot | 12 + .../profiles/user_visits_profile_spec.rb | 2 +- spec/features/users/overview_spec.rb | 18 +- spec/features/users/rss_spec.rb | 4 +- spec/features/users/show_spec.rb | 8 +- 9 files changed, 234 insertions(+), 217 deletions(-) diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss index 912f0145bf1dcf..c7ab8cf4713fb8 100644 --- a/app/assets/stylesheets/page_bundles/profile.scss +++ b/app/assets/stylesheets/page_bundles/profile.scss @@ -164,6 +164,8 @@ .user-profile { .profile-header { + margin: 0 $gl-padding; + .avatar-holder { margin: 0 auto 10px; } @@ -268,3 +270,20 @@ border: 0; } } + +.user-profile { + @include media-breakpoint-up(lg) { + display: grid; + grid-template-columns: minmax(240px, 1fr) 3fr; + gap: 3rem; + } +} + +.user-profile-sidebar { + z-index: 2; +} + +.user-profile-sidebar, +.user-profile-content { + min-width: 1px; // grid overflow fix +} diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml index 3a073436aa327e..b0162297498c89 100644 --- a/app/views/users/_overview.html.haml +++ b/app/views/users/_overview.html.haml @@ -1,16 +1,6 @@ -- activity_pane_class = Feature.enabled?(:security_auto_fix) && @user.bot? ? "col-12" : "col-md-12 col-lg-6 gl-align-self-start" - -.row.d-none.d-sm-flex - .col-12.calendar-block.gl-my-3 - .user-calendar.light{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: local_timezone_instance(@user.timezone).now.utc_offset } } - = gl_loading_icon(size: 'md', css_class: 'gl-my-8') - .user-calendar-error.invisible - = _('There was an error loading users activity calendar.') - %a.js-retry-load{ href: '#' } - = s_('UserProfile|Retry') -- if @user.user_readme&.rich_viewer - .row.justify-content-center - .col-12.col-md-10.col-lg-8.gl-my-6 +- if can?(current_user, :read_cross_project) && @user.user_readme&.rich_viewer + .profile-readme-wrapper.gl-relative.gl-overflow-hidden.gl-mb-5 + .profile-readme.read-more-container.gl-relative.justify-content-center.gl-border.gl-rounded-base.gl-py-5.gl-px-6.gl-overflow-hidden .gl-display-flex = render Pajamas::BreadcrumbComponent.new(class: 'gl-mb-4') do |c| - c.with_item(text: @user.username, href: project_path(@user.user_project)) @@ -18,29 +8,34 @@ - if current_user == @user .gl-ml-auto - = link_to _('Edit'), edit_blob_path(@user.user_project, @user.user_project.default_branch, @user.user_readme.path) + = link_to _('Edit file'), edit_blob_path(@user.user_project, @user.user_project.default_branch, @user.user_readme.path) = render 'projects/blob/viewer', viewer: @user.user_readme.rich_viewer, load_async: false -.row - .col-12.user-calendar-activities -.row - %div{ class: activity_pane_class } - - if can?(current_user, :read_cross_project) - .activities-block - .gl-mt-5 - .gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid - %h4.gl-flex-grow-1 - = Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity') - = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all" - .overview-content-list.user-activity-content{ data: { href: user_activity_path, testid: 'user-activity-content' } } - = gl_loading_icon(size: 'md', css_class: 'loading') - - unless Feature.enabled?(:security_auto_fix) && @user.bot? - .col-md-12.col-lg-6 - .projects-block - .gl-mt-5 - .gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid - %h4.gl-flex-grow-1 - = s_('UserProfile|Personal projects') - = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all" - .overview-content-list{ data: { href: user_projects_path } } - = gl_loading_icon(size: 'md', css_class: 'loading') +- unless Feature.enabled?(:security_auto_fix) && @user.bot? + - if @user.personal_projects.any? + .projects-block + .gl-display-flex.gl-align-items-baseline + %h4.gl-font-lg.gl-flex-grow-1 + = s_('UserProfile|Personal projects') + = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all" + .overview-content-list{ data: { href: user_projects_path } } + = gl_loading_icon(size: 'md', css_class: 'loading') + +- if can?(current_user, :read_cross_project) + .gl-align-self-start + .activities-block + .user-calendar.gl-border.light.gl-rounded-base.gl-px-3.gl-pt-4.gl-mb-5{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: local_timezone_instance(@user.timezone).now.utc_offset } } + = gl_loading_icon(size: 'md', css_class: 'gl-my-8') + .user-calendar-error.invisible + = _('There was an error loading users activity calendar.') + = render Pajamas::ButtonComponent.new(variant: :link, href: '#', button_options: { class: 'js-retry-load' }) do + = s_('UserProfile|Retry') + + .gl-display-flex.gl-align-items-baseline + %h4.gl-font-lg.gl-flex-grow-1 + = Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity') + = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all" + + .user-calendar-activities + .overview-content-list.user-activity-content.gl-mb-5{ data: { href: user_activity_path, testid: 'user-activity-content' } } + = gl_loading_icon(size: 'md', css_class: 'loading') diff --git a/app/views/users/_view_user_in_admin_area.html.haml b/app/views/users/_view_user_in_admin_area.html.haml index 36b3c33d8ab362..c8265ac9a9aeaf 100644 --- a/app/views/users/_view_user_in_admin_area.html.haml +++ b/app/views/users/_view_user_in_admin_area.html.haml @@ -1,4 +1,4 @@ - if current_user && current_user.admin? = render Pajamas::ButtonComponent.new(href: [:admin, @user], icon: 'user', - button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|View user in admin area'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }) + button_options: { class: 'has-tooltip', title: s_('UserProfile|View user in admin area'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0f6d08dee8b9d7..fe5f6339b67383 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -12,105 +12,103 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") -.user-profile - .cover-block.user-cover-block.gl-border-t.gl-border-b.gl-mt-n1 - %div{ class: container_class } - .cover-controls.gl-display-flex.gl-gap-3.gl-pb-4 - = 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: user_settings_profile_path, - button_options: { class: 'gl-flex-grow-1', 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) } - - .profile-header.gl-mx-5.gl-mb-4{ class: [('gl-mb-6' if profile_tabs.empty?)] } - .gl-display-inline-block.gl-mx-8.gl-vertical-align-top - .avatar-holder - = link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer' do - = render Pajamas::AvatarComponent.new(@user, alt: "", size: 96, avatar_options: { itemprop: "image" }) - - if @user.achievements_enabled && Ability.allowed?(current_user, :read_user_profile, @user) - #js-user-achievements{ data: { root_url: root_url, user_id: @user.id } } - .gl-display-inline-block.gl-vertical-align-top.gl-text-left.gl-max-w-80 - - if @user.blocked? || !@user.confirmed? - .user-info - %h1.cover-title.gl-my-0 - = user_display_name(@user) - = render "users/profile_basic_info" - - else - .user-info - %h1.cover-title.gl-my-0{ itemprop: 'name' } - = @user.name - - if @user.pronouns.present? - %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle - = "(#{@user.pronouns})" - - if @user.status&.busy? - = render Pajamas::BadgeComponent.new(s_('UserProfile|Busy'), size: 'sm', variant: 'warning', class: 'gl-vertical-align-middle') - - - if @user.pronunciation.present? - .gl-align-items-center - %p.gl-mb-4.gl-text-gray-500= s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation } - - - if @user.status&.customized? - .cover-status.gl-display-inline-flex.gl-align-items-baseline.gl-mb-3 - = emoji_icon(@user.status.emoji, class: 'gl-mr-2') - = markdown_field(@user.status, :message) - = render "users/profile_basic_info" - - user_local_time = local_time(@user.timezone) - - if @user.location.present? || user_local_time.present? || work_information(@user).present? - .gl-text-gray-900 +%div{ class: container_class } + .user-profile + .user-profile-sidebar + .profile-header.gl-py-6.gl-overflow-y-auto.gl-sm-pr-4 + + = 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" }) + + .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.achievements_enabled && Ability.allowed?(current_user, :read_user_profile, @user) + #js-user-achievements{ data: { root_url: root_url, user_id: @user.id } } + + - user_local_time = local_time(@user.timezone) + %div{ itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' } + %h3.h5.gl-mb-2= s_('UserProfile|Info') + - if work_information(@user).present? + .gl-mb-2 + = sprite_icon('work', css_class: 'fgray') + %span.gl-ml-1 + = work_information(@user, with_schema_markup: true) - if @user.location.present? - = render 'middle_dot_divider', stacking: true, itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' do + .gl-mb-2 = sprite_icon('location', css_class: 'fgray') - %span{ itemprop: 'addressLocality' } + %span.gl-ml-1{ itemprop: 'addressLocality' } = @user.location - if user_local_time.present? - = render 'middle_dot_divider', stacking: true, data: { testid: 'user-local-time' } do + .gl-mb-2{ data: { testid: 'user-local-time' } } = sprite_icon('clock', css_class: 'fgray') - %span + %span.gl-ml-1 = user_local_time - - if work_information(@user).present? - = render 'middle_dot_divider', stacking: true do - = sprite_icon('work', css_class: 'fgray') - %span - = work_information(@user, with_schema_markup: true) - .gl-text-gray-900 - - if @user.skype.present? - = render 'middle_dot_divider' do - = link_to "skype:#{@user.skype}", class: 'gl-hover-text-decoration-none', title: "Skype" do - = sprite_icon('skype', css_class: 'skype-icon') - - if @user.linkedin.present? - = render 'middle_dot_divider' do - = link_to linkedin_url(@user), class: 'gl-hover-text-decoration-none', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do - = sprite_icon('linkedin', css_class: 'linkedin-icon') - - if @user.twitter.present? - = render 'middle_dot_divider', breakpoint: 'sm' do - = link_to twitter_url(@user), class: 'gl-hover-text-decoration-none', title: _("X (formerly Twitter)"), target: '_blank', rel: 'noopener noreferrer nofollow' do - = sprite_icon('x', css_class: 'x-icon') - - if @user.discord.present? - = render 'middle_dot_divider', breakpoint: 'sm' do - = link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do - = sprite_icon('discord', css_class: 'discord-icon') - - if @user.mastodon.present? - = render 'middle_dot_divider', breakpoint: 'sm' do - = link_to mastodon_url(@user), class: 'gl-hover-text-decoration-none', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow' do - = sprite_icon('mastodon', css_class: 'mastodon-icon') - - if @user.website_url.present? - = render 'middle_dot_divider', stacking: true do - - if Feature.enabled?(:security_auto_fix) && @user.bot? - = sprite_icon('question-o', css_class: 'gl-text-blue-500') - = link_to @user.short_website_url, @user.full_website_url, target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url' - - if display_public_email?(@user) - = render 'middle_dot_divider', stacking: true do - = link_to @user.public_email, "mailto:#{@user.public_email}", itemprop: 'email' - - -# Ensure this stays indented one level less than the social links - -# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118314 - - if @user.bio.present? && @user.confirmed? && !@user.blocked? - %p.profile-user-bio.gl-mb-3 - = @user.bio + = sprite_icon('calendar', css_class: 'fgray') + %span.gl-ml-1= s_('Member since %{date}') % { date: l(@user.created_at.to_date, format: :long) } + + .gl-text-gray-900 + - if @user.website_url.present? || display_public_email?(@user) || @user.skype.present? || @user.linkedin.present? || @user.twitter.present? || (Feature.enabled?(:mastodon_social_ui, @user) && @user.mastodon.present?) || @user.discord.present? + %h3.h5.gl-mb-2= s_('UserProfile|Contact') + - if @user.website_url.present? + .gl-mb-2 + - if Feature.enabled?(:security_auto_fix) && @user.bot? + = sprite_icon('question-o', css_class: 'gl-text-blue-500') + = sprite_icon('earth', css_class: 'fgray') + = link_to @user.short_website_url, @user.full_website_url, class: 'gl-text-gray-900 gl-ml-1', target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url' + - if display_public_email?(@user) + .gl-mb-2 + = sprite_icon('mail', css_class: 'fgray') + = link_to @user.public_email, "mailto:#{@user.public_email}", class: 'gl-text-gray-900 gl-ml-1', itemprop: 'email' + - if @user.skype.present? + .gl-mb-2 + = sprite_icon('skype', css_class: 'fgray') + = link_to @user.skype, "skype:#{@user.skype}", class: 'gl-text-gray-900 gl-ml-1', title: "Skype" + - if @user.linkedin.present? + .gl-mb-2 + = sprite_icon('linkedin', css_class: 'fgray') + = link_to @user.linkedin, linkedin_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' + - if @user.twitter.present? + .gl-mb-2 + = sprite_icon('twitter', css_class: 'fgray') + = link_to @user.twitter, twitter_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: _("X (formerly Twitter)"), target: '_blank', rel: 'noopener noreferrer nofollow' + - if @user.mastodon.present? + .gl-mb-2 + = sprite_icon('mastodon', css_class: 'fgray') + = link_to @user.mastodon, mastodon_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow' + - if @user.discord.present? + .gl-mb-2 + = sprite_icon('discord', css_class: 'fgray') + = link_to @user.discord, discord_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' -# TODO: Remove this with the removal of the old navigation. -# See https://gitlab.com/gitlab-org/gitlab/-/issues/435899. @@ -157,67 +155,76 @@ = gl_badge_tag @user.followers.count, size: :sm - if profile_tab?(:following) %li.js-following-tab - = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do + = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json), testid: 'following_tab' } do = s_('UserProfile|Following') = gl_badge_tag @user.followees.count, size: :sm - - if !profile_tabs.empty? && Feature.enabled?(:profile_tabs_vue, current_user) - #js-profile-tabs{ data: user_profile_tabs_app_data(@user) } - %div{ class: container_class } - - unless Feature.enabled?(:profile_tabs_vue, current_user) - .tab-content - - if profile_tab?(:overview) - #js-overview.tab-pane - = render "users/overview" - - - if profile_tab?(:activity) - #activity.tab-pane - .row - .col-12 - .flash-container - - if can?(current_user, :read_cross_project) - %h4.prepend-top-20 - = 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') + + .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 108921089fe4ef..64255904716bd0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18594,6 +18594,9 @@ msgstr "" msgid "Edit environment" msgstr "" +msgid "Edit file" +msgstr "" + msgid "Edit files in the editor and commit changes here" msgstr "" @@ -53977,6 +53980,9 @@ msgstr "" msgid "UserProfile|Busy" msgstr "" +msgid "UserProfile|Contact" +msgstr "" + msgid "UserProfile|Contributed projects" msgstr "" @@ -54007,6 +54013,9 @@ msgstr "" msgid "UserProfile|Groups are the best way to manage projects and members." msgstr "" +msgid "UserProfile|Info" +msgstr "" + msgid "UserProfile|Join or create a group to start contributing by commenting on issues or submitting merge requests!" msgstr "" @@ -54025,6 +54034,9 @@ msgstr "" msgid "UserProfile|Pronounced as: %{pronunciation}" msgstr "" +msgid "UserProfile|Pronouns: %{pronouns}" +msgstr "" + msgid "UserProfile|Retry" msgstr "" diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb index 7edfb542594ffe..a0d0bd73d3cf0e 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 ".cover-block" do + page.within ".user-info" do expect(page).to have_content user.name expect(page).to have_content user.username end diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb index 1da61ecb86861d..f776b16713d109 100644 --- a/spec/features/users/overview_spec.rb +++ b/spec/features/users/overview_spec.rb @@ -86,22 +86,6 @@ def push_code_contribution end describe 'projects section' do - describe 'user has no personal projects' do - include_context 'visit overview tab' - - it 'shows an empty project list with an info message' do - page.within('.projects-block') do - expect(page).to have_selector('.loading', visible: false) - expect(page).to have_content('You haven\'t created any personal projects.') - expect(page).not_to have_selector('.project-row') - end - end - - it 'does not show a link to the project list' do - expect(find('#js-overview .projects-block')).to have_selector('.js-view-all', visible: false) - end - end - describe 'user has a personal project' do before do create(:project, :private, namespace: user.namespace, creator: user) { |p| p.add_maintainer(user) } @@ -315,7 +299,7 @@ def push_code_contribution end it 'shows projects panel' do - expect(page).to have_selector('.projects-block') + expect(page).not_to have_selector('.projects-block') end end end diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb index 730c31df899f8e..5fcaee0211d529 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-cover-block') do + page.within('.user-profile-content') 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-cover-block') do + page.within('.user-profile-content') 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 c56b261fe282c2..e0c41155c2b42a 100644 --- a/spec/features/users/show_spec.rb +++ b/spec/features/users/show_spec.rb @@ -12,7 +12,7 @@ it 'shows copy user id action in the dropdown', :js do subject - page.within('.user-cover-block') do + page.within('.cover-controls') do find_by_testid('base-dropdown-toggle').click end @@ -305,7 +305,7 @@ end it 'shows user name as blocked' do - expect(page).to have_css(".cover-title", text: 'Blocked user') + expect(page).to have_css(".user-info", 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(".cover-title", text: 'Unconfirmed user') + expect(page).to have_css(".user-info", text: 'Unconfirmed user') end it 'shows no tab' do @@ -393,7 +393,7 @@ subject - expect(page).to have_content("(they/them)") + expect(page).to have_content("Pronouns: they/them") end it 'shows the pronunctiation of the user if there was one' do -- GitLab From 3838badbb18692b78033ed7fd7129c2abb1b4b50 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 14 Nov 2023 20:05:31 -0700 Subject: [PATCH 02/19] Fix calendar alignment and add achievements section - Show full 12 month calendar on all screen sizes - Scroll calendar when out of room - Add achievements section Changelog: changed --- .../pages/users/activity_calendar.js | 22 +-- .../javascripts/pages/users/user_tabs.js | 53 ++++--- .../profile/components/activity_calendar.vue | 35 +---- .../profile/components/overview_tab.vue | 8 +- .../profile/components/user_achievements.vue | 13 +- .../stylesheets/page_bundles/profile.scss | 147 +++++------------- locale/gitlab.pot | 3 + spec/features/calendar_spec.rb | 36 ----- .../components/activity_calendar_spec.js | 30 ---- 9 files changed, 96 insertions(+), 251 deletions(-) diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 13bba06d425bbf..dbb0fe585791f7 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -72,7 +72,7 @@ export default class ActivityCalendar { this.clickDay = this.clickDay.bind(this); this.currentSelectedDate = ''; this.daySpace = 1; - this.daySize = 15; + this.daySize = 14; this.daySizeWithSpace = this.daySize + this.daySpace * 2; this.monthNames = [ __('Jan'), @@ -131,7 +131,6 @@ export default class ActivityCalendar { this.renderDays(); this.renderMonths(); this.renderDayTitles(); - this.renderKey(); } // Add extra padding for the last month label if it is also the last column @@ -257,25 +256,6 @@ export default class ActivityCalendar { .text((date) => this.monthNames[date.month]); } - renderKey() { - this.svg - .append('g') - .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`) - .selectAll('rect') - .data(CONTRIB_LEGENDS) - .enter() - .append('rect') - .attr('width', this.daySize) - .attr('height', this.daySize) - .attr('x', (_, i) => this.daySizeWithSpace * i) - .attr('y', 0) - .attr('data-level', (_, i) => i) - .attr('class', 'user-contrib-cell has-tooltip contrib-legend') - .attr('title', (x) => x.title) - .attr('data-container', 'body') - .attr('data-html', true); - } - clickDay(stamp) { if (this.currentSelectedDate !== stamp.date) { this.currentSelectedDate = stamp.date; diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index f9e22808b0d9c7..bce040b99df504 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -1,7 +1,6 @@ // TODO: Remove this with the removal of the old navigation. // See https://gitlab.com/gitlab-org/gitlab/-/issues/435899. -import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import $ from 'jquery'; import Activities from '~/activities'; import AjaxCache from '~/lib/utils/ajax_cache'; @@ -67,17 +66,32 @@ import UserOverviewBlock from './user_overview_block'; const CALENDAR_TEMPLATE = `
-
+
+ + + + + + + + + +
+
`; -const CALENDAR_PERIOD_6_MONTHS = 6; const CALENDAR_PERIOD_12_MONTHS = 12; -/* computation based on - * width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group); - * (see activity_calendar.js) - */ -const OVERVIEW_CALENDAR_BREAKPOINT = 918; export default class UserTabs { constructor({ defaultAction, action, parentEl }) { @@ -105,12 +119,6 @@ export default class UserTabs { .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', (event) => this.tabShown(event)) .on('click', '.gl-pagination a', (event) => this.changeProjectsPage(event)); - - window.addEventListener('resize', () => this.onResize()); - } - - onResize() { - this.loadActivityCalendar(); } changeProjectsPage(e) { @@ -199,6 +207,7 @@ export default class UserTabs { UserTabs.renderMostRecentBlocks('#js-overview .activities-block', { requestParams: { limit: 15 }, }); + UserTabs.renderMostRecentBlocks('#js-overview .projects-block', { requestParams: { limit: 10, skip_pagination: true, skip_namespace: true, compact_mode: true }, }); @@ -218,8 +227,6 @@ export default class UserTabs { loadActivityCalendar() { const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar'); - if (!$calendarWrap.length || bp.getBreakpointSize() === 'xs') return; - const calendarPath = $calendarWrap.data('calendarPath'); AjaxCache.retrieve(calendarPath) @@ -240,7 +247,6 @@ export default class UserTabs { } static renderActivityCalendar(data, $calendarWrap) { - const monthsAgo = UserTabs.getVisibleCalendarPeriod($calendarWrap); const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath'); const utcOffset = $calendarWrap.data('utcOffset'); const calendarHint = __('Issues, merge requests, pushes, and comments.'); @@ -257,8 +263,12 @@ export default class UserTabs { calendarActivitiesPath, utcOffset, firstDayOfWeek: gon.first_day_of_week, - monthsAgo, + CALENDAR_PERIOD_12_MONTHS, }); + + // Scroll to end + const calendarContainer = document.querySelector('.js-contrib-calendar'); + calendarContainer.scrollLeft = calendarContainer.scrollWidth; } toggleLoading(status) { @@ -282,11 +292,4 @@ export default class UserTabs { getCurrentAction() { return this.$parentEl.find('.nav-links a.active').data('action'); } - - static getVisibleCalendarPeriod($calendarWrap) { - const width = $calendarWrap.width(); - return width < OVERVIEW_CALENDAR_BREAKPOINT - ? CALENDAR_PERIOD_6_MONTHS - : CALENDAR_PERIOD_12_MONTHS; - } } diff --git a/app/assets/javascripts/profile/components/activity_calendar.vue b/app/assets/javascripts/profile/components/activity_calendar.vue index d359b478d35c05..07a137e7fef4a7 100644 --- a/app/assets/javascripts/profile/components/activity_calendar.vue +++ b/app/assets/javascripts/profile/components/activity_calendar.vue @@ -1,12 +1,8 @@ diff --git a/app/assets/javascripts/profile/components/overview_tab.vue b/app/assets/javascripts/profile/components/overview_tab.vue index 8cfa3fb3eea188..ab8a2871a41c1c 100644 --- a/app/assets/javascripts/profile/components/overview_tab.vue +++ b/app/assets/javascripts/profile/components/overview_tab.vue @@ -54,19 +54,19 @@ export default {