From ed7ab572d7ab4edd51afc743d24d0bb2df59e4ce Mon Sep 17 00:00:00 2001
From: Van Anderson <van.m.anderson@gmail.com>
Date: Fri, 26 Jul 2024 14:13:42 +0000
Subject: [PATCH] Refactor users/show.haml and user_tabs.js to remove legacy
 tabs

Remove hidden legacy tabs from users/show.haml and  refactor user_tabs.js to render lazy content into a single centralized content container. This allows a significant amount of code to be removed and simplified. Light refactors of these files also remove naming references to removed legacy concepts and enhance readability.
---
 .../javascripts/pages/users/user_tabs.js      | 195 ++++--------------
 app/controllers/users_controller.rb           |   7 +
 app/helpers/users_helper.rb                   |  20 +-
 app/views/users/_legacy_tabs.html.haml        |  11 +
 app/views/users/show.html.haml                | 102 +--------
 spec/features/calendar_spec.rb                |  18 +-
 .../profiles/user_visits_profile_spec.rb      |  14 +-
 spec/features/users/overview_spec.rb          |  26 +--
 spec/features/users/show_spec.rb              | 109 +---------
 ...user_browses_projects_on_user_page_spec.rb |   4 +-
 spec/helpers/users_helper_spec.rb             |  25 ++-
 11 files changed, 125 insertions(+), 406 deletions(-)
 create mode 100644 app/views/users/_legacy_tabs.html.haml

diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index c857fd2fa2a65..670aabd633700 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -11,59 +11,6 @@ import { __ } from '~/locale';
 import ActivityCalendar from './activity_calendar';
 import UserOverviewBlock from './user_overview_block';
 
-/**
- * UserTabs
- *
- * Handles persisting and restoring the current tab selection and lazily-loading
- * content on the Users#show page.
- *
- * ### Example Markup
- *
- * <ul class="nav-links">
- *   <li class="activity-tab active">
- *     <a data-action="activity" data-target="#activity" data-toggle="tab" href="/username">
- *       Activity
- *     </a>
- *   </li>
- *   <li class="groups-tab">
- *     <a data-action="groups" data-target="#groups" data-toggle="tab" href="/users/username/groups">
- *       Groups
- *     </a>
- *   </li>
- *   <li class="contributed-tab">
- *     ...
- *   </li>
- *   <li class="projects-tab">
- *     ...
- *   </li>
- *   <li class="snippets-tab">
- *     ...
- *   </li>
- * </ul>
- *
- * <div class="tab-content">
- *   <div class="tab-pane" id="activity">
- *     Activity Content
- *   </div>
- *   <div class="tab-pane" id="groups">
- *     Groups Content
- *   </div>
- *   <div class="tab-pane" id="contributed">
- *     Contributed projects content
- *   </div>
- *   <div class="tab-pane" id="projects">
- *    Projects content
- *   </div>
- *   <div class="tab-pane" id="snippets">
- *     Snippets content
- *   </div>
- * </div>
- *
- * <div class="loading">
- *   Loading Animation
- * </div>
- */
-
 const CALENDAR_TEMPLATE = `
   <div class="calendar">
     <div class="js-contrib-calendar gl-overflow-x-auto"></div>
@@ -96,80 +43,54 @@ const CALENDAR_TEMPLATE = `
 
 const CALENDAR_PERIOD_12_MONTHS = 12;
 
+const DEFAULT_LOADER_ACTIONS = [
+  'groups',
+  'contributed',
+  'projects',
+  'starred',
+  'snippets',
+  'followers',
+  'following',
+];
+
 export default class UserTabs {
-  constructor({ defaultAction, action, parentEl }) {
-    this.loaded = {};
-    this.defaultAction = defaultAction || 'overview';
-    this.action = action || this.defaultAction;
-    this.$parentEl = $(parentEl) || $(document);
+  constructor({ parentEl }) {
+    this.$legacyTabsContainer = $('#js-legacy-tabs-container');
+    this.$parentEl = $(parentEl || document);
     this.windowLocation = window.location;
-    this.$parentEl.find('.nav-links a').each((i, navLink) => {
-      this.loaded[$(navLink).attr('data-action')] = false;
-    });
-    this.actions = Object.keys(this.loaded);
-    this.bindEvents();
 
-    // TODO: refactor to make this configurable via constructor params with a default value of 'show'
-    if (this.action === 'show') {
-      this.action = this.defaultAction;
-    }
+    const action = this.$legacyTabsContainer.data('action');
+    const endpoint = this.$legacyTabsContainer.data('endpoint');
 
-    this.activateTab(this.action);
+    this.bindPaginationEvent();
+    this.loadPage(action, endpoint);
   }
 
-  bindEvents() {
-    this.$parentEl
-      .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));
+  bindPaginationEvent() {
+    this.$parentEl.on('click', '.gl-pagination a', (event) => this.changePage(event));
   }
 
-  changeProjectsPage(e) {
+  changePage(e) {
     e.preventDefault();
 
-    $('.tab-pane.active').empty();
+    $('#js-legacy-tabs-container').empty();
     const endpoint = $(e.target).attr('href');
-    this.loadTab(this.getCurrentAction(), endpoint);
-  }
-
-  tabShown(event) {
-    const $target = $(event.target);
-    const action = $target.data('action');
-    const source = $target.attr('href');
-    const endpoint = $target.data('endpoint');
-    this.setTab(action, endpoint);
-    return this.setCurrentAction(source);
-  }
-
-  activateTab(action) {
-    return this.$parentEl.find(`.nav-links .js-${action}-tab a`).tab('show');
+    const action = this.$legacyTabsContainer.data('action');
+    this.loadPage(action, endpoint);
   }
 
-  setTab(action, endpoint) {
-    if (this.loaded[action]) {
-      return;
-    }
+  loadPage(action, endpoint) {
     if (action === 'activity') {
-      this.loadActivities();
+      // eslint-disable-next-line no-new
+      new Activities('#js-legacy-tabs-container');
     } else if (action === 'overview') {
-      this.loadOverviewTab();
-    }
-
-    const loadableActions = [
-      'groups',
-      'contributed',
-      'projects',
-      'starred',
-      'snippets',
-      'followers',
-      'following',
-    ];
-    if (loadableActions.indexOf(action) > -1) {
-      this.loadTab(action, endpoint);
+      this.loadOverviewPage();
+    } else if (DEFAULT_LOADER_ACTIONS.includes(action)) {
+      this.defaultPageLoader(action, endpoint);
     }
   }
 
-  loadTab(action, endpoint) {
+  defaultPageLoader(action, endpoint) {
     this.toggleLoading(true);
 
     const params = action === 'projects' ? { skip_namespace: true } : {};
@@ -177,10 +98,9 @@ export default class UserTabs {
     return axios
       .get(endpoint, { params })
       .then(({ data }) => {
-        const tabSelector = `div#${action}`;
-        this.$parentEl.find(tabSelector).html(data.html);
-        this.loaded[action] = true;
-        localTimeAgo(document.querySelectorAll(`${tabSelector} .js-timeago`));
+        const containerSelector = `div#js-legacy-tabs-container`;
+        this.$parentEl.find(containerSelector).html(data.html);
+        localTimeAgo(document.querySelectorAll(`${containerSelector} .js-timeago`));
 
         this.toggleLoading(false);
       })
@@ -189,35 +109,18 @@ export default class UserTabs {
       });
   }
 
-  loadActivities() {
-    if (this.loaded.activity) {
-      return;
-    }
-
-    // eslint-disable-next-line no-new
-    new Activities('#activity');
-
-    this.loaded.activity = true;
-  }
-
-  loadOverviewTab() {
-    if (this.loaded.overview) {
-      return;
-    }
-
+  loadOverviewPage() {
     initReadMore();
 
     this.loadActivityCalendar();
 
-    UserTabs.renderMostRecentBlocks('#js-overview .activities-block', {
+    UserTabs.renderMostRecentBlocks('#js-legacy-tabs-container .activities-block', {
       requestParams: { limit: 15 },
     });
 
-    UserTabs.renderMostRecentBlocks('#js-overview .projects-block', {
+    UserTabs.renderMostRecentBlocks('#js-legacy-tabs-container .projects-block', {
       requestParams: { limit: 3, skip_pagination: true, skip_namespace: true, card_mode: true },
     });
-
-    this.loaded.overview = true;
   }
 
   static renderMostRecentBlocks(container, options) {
@@ -234,7 +137,7 @@ export default class UserTabs {
   }
 
   loadActivityCalendar() {
-    const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar');
+    const $calendarWrap = this.$parentEl.find('.user-calendar');
     const calendarPath = $calendarWrap.data('calendarPath');
 
     AjaxCache.retrieve(calendarPath)
@@ -265,9 +168,9 @@ export default class UserTabs {
 
     // eslint-disable-next-line no-new
     new ActivityCalendar({
-      container: '.tab-pane.active .js-contrib-calendar',
-      activitiesContainer: '.tab-pane.active .user-calendar-activities',
-      recentActivitiesContainer: '.tab-pane.active .overview-content-list',
+      container: '#js-legacy-tabs-container .js-contrib-calendar',
+      activitiesContainer: '#js-legacy-tabs-container .user-calendar-activities',
+      recentActivitiesContainer: '#js-legacy-tabs-container .overview-content-list',
       timestamps: data,
       calendarActivitiesPath,
       utcOffset,
@@ -283,22 +186,4 @@ export default class UserTabs {
   toggleLoading(status) {
     return this.$parentEl.find('.loading').toggleClass('hide', !status);
   }
-
-  setCurrentAction(source) {
-    let newState = source;
-    newState = newState.replace(/\/+$/, '');
-    newState += this.windowLocation.search + this.windowLocation.hash;
-    window.history.replaceState(
-      {
-        url: newState,
-      },
-      document.title,
-      newState,
-    );
-    return newState;
-  }
-
-  getCurrentAction() {
-    return this.$parentEl.find('.nav-links a.active').data('action');
-  }
 }
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 55a50f5b3f55d..15a705fbf9503 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -21,6 +21,7 @@ class UsersController < ApplicationController
   skip_before_action :authenticate_user!
   prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
   before_action :user, except: [:exists]
+  before_action :set_legacy_data
   before_action :authorize_read_user_profile!, only: [
     :calendar, :calendar_activities, :groups, :projects, :contributed, :starred, :snippets, :followers, :following
   ]
@@ -307,6 +308,12 @@ def finder_params
       not_aimed_for_deletion: true
     }
   end
+
+  def set_legacy_data
+    controller_action = params[:action]
+    @action = controller_action.gsub('show', 'overview')
+    @endpoint = request.path
+  end
 end
 
 UsersController.prepend_mod_with('UsersController')
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 7dcc514ab42b7..d9ae9368513f5 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -30,12 +30,12 @@ def user_email_help_text(user)
     }) + content_tag(:p) { confirmation_link }
   end
 
-  def profile_tabs
-    @profile_tabs ||= get_profile_tabs
-  end
+  def profile_actions(user)
+    return [] unless can?(current_user, :read_user_profile, user)
+
+    return [:overview, :activity] if user.bot?
 
-  def profile_tab?(tab)
-    profile_tabs.include?(tab)
+    [:overview, :activity, :groups, :contributed, :projects, :starred, :snippets, :followers, :following]
   end
 
   def user_internal_regex_data
@@ -278,16 +278,6 @@ def blocked_user_badge(user)
     { text: s_('AdminUsers|Blocked'), variant: 'danger' }
   end
 
-  def get_profile_tabs
-    tabs = []
-
-    if can?(current_user, :read_user_profile, @user)
-      tabs += [:overview, :activity, :groups, :contributed, :projects, :starred, :snippets, :followers, :following]
-    end
-
-    tabs
-  end
-
   def get_current_user_menu_items
     items = []
 
diff --git a/app/views/users/_legacy_tabs.html.haml b/app/views/users/_legacy_tabs.html.haml
new file mode 100644
index 0000000000000..c7aa1b86bbd77
--- /dev/null
+++ b/app/views/users/_legacy_tabs.html.haml
@@ -0,0 +1,11 @@
+- if allowable_actions.include?(action.to_sym)
+  #js-legacy-tabs-container{ data: { action: action, endpoint: endpoint } }
+    - case action
+    - when 'overview'
+      = render "users/overview"
+    - when 'activity'
+      .flash-container
+      - if can?(current_user, :read_cross_project)
+        .content_list.user-activity-content{ data: { href: user_activity_path } }
+        .loading
+          = render Pajamas::SpinnerComponent.new(size: :md)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 41166503ed798..7d139d3b26ad7 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -8,6 +8,7 @@
 - add_page_specific_style 'page_bundles/projects'
 - @force_desktop_expanded_sidebar = true
 - nav "user_profile"
+- allowable_actions = profile_actions(@user)
 
 = content_for :meta_tags do
   = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
@@ -43,108 +44,19 @@
               = markdown_field(@user.status, :message)
   .user-profile{ class: @user.blocked? ? '' : 'user-profile-with-sidebar' }
     .user-profile-content
-      - 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
-          - if profile_tab?(:overview)
-            #js-overview.tab-pane.user-overview-page
-              = render "users/overview"
-
-          - 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
-
+      - if allowable_actions.any?
+        - if Feature.enabled?(:profile_tabs_vue, current_user)
+          #js-profile-tabs{ data: user_profile_tabs_app_data(@user) }
+        - else
+          = render 'users/legacy_tabs', action: @action, endpoint: @endpoint, allowable_actions: allowable_actions
         .loading.hide
           = render Pajamas::SpinnerComponent.new(size: :md)
-
-      - if profile_tabs.empty?
+      - else
         - if @user.blocked?
           = render Pajamas::EmptyStateComponent.new(svg_path: 'illustrations/empty-state/empty-access-md.svg',
             title: s_('UserProfile|This user is blocked'))
         - else
           = render Pajamas::EmptyStateComponent.new(svg_path: 'illustrations/empty-state/empty-private-md.svg',
             title: s_('UserProfile|This user has a private profile'))
-
     - unless @user.blocked?
       = render 'users/profile_sidebar'
-
-    -# TODO: Remove this with the removal of the old navigatifon.
-    -# See https://gitlab.com/gitlab-org/gitlab/-/issues/435899.
-    - if !profile_tabs.empty? && !Feature.enabled?(:profile_tabs_vue, current_user)
-      .scrolling-tabs-container.gl-display-none
-        %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
-          = sprite_icon('chevron-lg-left', size: 12)
-        %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
-          = sprite_icon('chevron-lg-right', size: 12)
-        %ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs.gl-border-b-0
-          - if profile_tab?(:overview)
-            %li.js-overview-tab
-              = link_to user_path, data: { target: 'div#js-overview', action: 'overview', toggle: 'tab' } do
-                = s_('UserProfile|Overview')
-          - if profile_tab?(:activity)
-            %li.js-activity-tab
-              = link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
-                = s_('UserProfile|Activity')
-          - if profile_tab?(:groups)
-            %li.js-groups-tab
-              = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
-                = s_('UserProfile|Groups')
-          - if profile_tab?(:contributed)
-            %li.js-contributed-tab
-              = link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
-                = s_('UserProfile|Contributed projects')
-          - if profile_tab?(:projects)
-            %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), card_mode: true } 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
-                = s_('UserProfile|Snippets')
-          - if profile_tab?(:followers)
-            %li.js-followers-tab
-              = link_to user_followers_path, data: { target: 'div#followers', action: 'followers', toggle: 'tab', endpoint: user_followers_path(format: :json) } do
-                = s_('UserProfile|Followers')
-                = gl_badge_tag @user.followers.count
-          - 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), testid: 'following_tab' } do
-                = s_('UserProfile|Following')
-                = gl_badge_tag @user.followees.count
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 501af5ade1a99..48396eddbde03 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -77,7 +77,7 @@ def note_comment_contribution
   end
 
   def selected_day_activities(visible: true)
-    find('#js-overview .user-calendar-activities', visible: visible).text
+    find('#js-legacy-tabs-container .user-calendar-activities', visible: visible).text
   end
 
   def recent_activities(visible: true)
@@ -101,11 +101,11 @@ def recent_activities(visible: true)
       include_context 'when user page is visited'
 
       it 'displays calendar' do
-        expect(find('#js-overview')).to have_css('.js-contrib-calendar')
+        expect(find('#js-legacy-tabs-container')).to have_css('.js-contrib-calendar')
       end
 
       describe 'select calendar day' do
-        let(:cells) { page.all('#js-overview .user-contrib-cell') }
+        let(:cells) { page.all('#js-legacy-tabs-container .user-contrib-cell') }
 
         before do
           cells[0].click
@@ -152,10 +152,10 @@ def recent_activities(visible: true)
         include_context 'when user page is visited'
 
         it 'displays calendar activity square for 1 contribution', :sidekiq_inline do
-          expect(find('#js-overview')).to have_selector(get_cell_level_selector(contribution_count), count: 1)
+          expect(find('#js-legacy-tabs-container')).to have_selector(get_cell_level_selector(contribution_count), count: 1)
 
           today = Date.today.strftime(date_format)
-          expect(find('#js-overview')).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
+          expect(find('#js-legacy-tabs-container')).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
         end
       end
 
@@ -179,7 +179,7 @@ def recent_activities(visible: true)
           include_context 'when user page is visited'
 
           it 'displays calendar activity log', :sidekiq_inline do
-            expect(all('#js-overview .overview-content-list .event-target-title').map(&:text)).to contain_exactly(
+            expect(all('#js-legacy-tabs-container .overview-content-list .event-target-title').map(&:text)).to contain_exactly(
               match(/#{issue_title}/),
               match(/new task/)
             )
@@ -219,17 +219,17 @@ def recent_activities(visible: true)
         include_context 'when user page is visited'
 
         it 'displays calendar activity squares for both days', :sidekiq_inline do
-          expect(find('#js-overview')).to have_selector(get_cell_level_selector(1), count: 2)
+          expect(find('#js-legacy-tabs-container')).to have_selector(get_cell_level_selector(1), count: 2)
         end
 
         it 'displays calendar activity square for yesterday', :sidekiq_inline do
           yesterday = Date.yesterday.strftime(date_format)
-          expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+          expect(find('#js-legacy-tabs-container')).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
         end
 
         it 'displays calendar activity square for today' do
           today = Date.today.strftime(date_format)
-          expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, today), count: 1)
+          expect(find('#js-legacy-tabs-container')).to have_selector(get_cell_date_selector(1, today), count: 1)
         end
       end
     end
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index b8d8d4b429786..7ea813af19da3 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -79,7 +79,7 @@
 
       it_behaves_like 'shows expected content' do
         let(:link) { 'Groups' }
-        let(:div) { '#groups' }
+        let(:div) { '#js-legacy-tabs-container' }
         let(:expected_content) { group.name }
       end
     end
@@ -98,7 +98,7 @@
 
       it_behaves_like 'shows expected content' do
         let(:link) { 'Contributed projects' }
-        let(:div) { '#contributed' }
+        let(:div) { '#js-legacy-tabs-container' }
         let(:expected_content) { project.name }
       end
     end
@@ -110,7 +110,7 @@
 
       it_behaves_like 'shows expected content' do
         let(:link) { 'Personal projects' }
-        let(:div) { '#projects' }
+        let(:div) { '#js-legacy-tabs-container' }
         let(:expected_content) { project.name }
       end
     end
@@ -124,7 +124,7 @@
 
       it_behaves_like 'shows expected content' do
         let(:link) { 'Starred projects' }
-        let(:div) { '#starred' }
+        let(:div) { '#js-legacy-tabs-container' }
         let(:expected_content) { project.name }
       end
     end
@@ -134,7 +134,7 @@
 
       it_behaves_like 'shows expected content' do
         let(:link) { 'Snippets' }
-        let(:div) { '#snippets' }
+        let(:div) { '#js-legacy-tabs-container' }
         let(:expected_content) { snippet.title }
       end
     end
@@ -148,7 +148,7 @@
 
       it_behaves_like 'shows expected content' do
         let(:link) { 'Followers' }
-        let(:div) { '#followers' }
+        let(:div) { '#js-legacy-tabs-container' }
         let(:expected_content) { fan.name }
       end
     end
@@ -162,7 +162,7 @@
 
       it_behaves_like 'shows expected content' do
         let(:link) { 'Following' }
-        let(:div) { '#following' }
+        let(:div) { '#js-legacy-tabs-container' }
         let(:expected_content) { star.name }
       end
     end
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index 4ef22f7f7b73e..b819e26f6652e 100644
--- a/spec/features/users/overview_spec.rb
+++ b/spec/features/users/overview_spec.rb
@@ -44,7 +44,7 @@ def push_code_contribution
       end
 
       it 'does not show a link to the activity list' do
-        expect(find('#js-overview .activities-block')).to have_selector('.js-view-all', visible: false)
+        expect(find('#js-legacy-tabs-container .activities-block')).to have_selector('.js-view-all', visible: false)
       end
     end
 
@@ -56,7 +56,7 @@ def push_code_contribution
       include_context 'visit overview tab'
 
       it 'display 3 entries in the list of activities' do
-        expect(find('#js-overview')).to have_selector('.event-item', count: 3)
+        expect(find('#js-legacy-tabs-container')).to have_selector('.event-item', count: 3)
       end
     end
 
@@ -68,11 +68,11 @@ def push_code_contribution
       include_context 'visit overview tab'
 
       it 'displays 15 entries in the list of activities' do
-        expect(find('#js-overview')).to have_selector('.event-item', count: 15)
+        expect(find('#js-legacy-tabs-container')).to have_selector('.event-item', count: 15)
       end
 
       it 'shows a link to the activity list' do
-        expect(find('#js-overview .activities-block')).to have_selector('.js-view-all', visible: true)
+        expect(find('#js-legacy-tabs-container .activities-block')).to have_selector('.js-view-all', visible: true)
       end
 
       it 'links to the activity tab' do
@@ -100,11 +100,11 @@ def push_code_contribution
       end
 
       it 'shows a link to the project list' do
-        expect(find('#js-overview .projects-block')).to have_selector('.js-view-all', visible: true)
+        expect(find('#js-legacy-tabs-container .projects-block')).to have_selector('.js-view-all', visible: true)
       end
 
       it 'shows projects in "card mode"' do
-        page.within('#js-overview .projects-block') do
+        page.within('#js-legacy-tabs-container .projects-block') do
           expect(find('.js-projects-list-holder')).to have_css('.gl-card')
         end
       end
@@ -126,7 +126,7 @@ def push_code_contribution
       end
 
       it 'shows a link to the project list' do
-        expect(find('#js-overview .projects-block')).to have_selector('.js-view-all', visible: true)
+        expect(find('#js-legacy-tabs-container .projects-block')).to have_selector('.js-view-all', visible: true)
       end
 
       it 'does not show pagination' do
@@ -145,7 +145,7 @@ def push_code_contribution
       end
 
       it 'shows an empty followers list with an info message' do
-        page.within('#followers') do
+        page.within('#js-legacy-tabs-container') do
           expect(page).to have_content('You do not have any followers')
           expect(page).not_to have_selector('.gl-card.gl-mb-5')
           expect(page).not_to have_selector('.gl-pagination')
@@ -163,7 +163,7 @@ def push_code_contribution
       end
 
       it 'shows followers' do
-        page.within('#followers') do
+        page.within('#js-legacy-tabs-container') do
           expect(page).to have_content(follower.name)
           expect(page).to have_selector('.gl-card.gl-mb-5')
           expect(page).not_to have_selector('.gl-pagination')
@@ -184,7 +184,7 @@ def push_code_contribution
       end
 
       it 'shows paginated followers' do
-        page.within('#followers') do
+        page.within('#js-legacy-tabs-container') do
           other_users.each_with_index do |follower, i|
             break if i == 20
 
@@ -206,7 +206,7 @@ def push_code_contribution
       end
 
       it 'shows an empty following list with an info message' do
-        page.within('#following') do
+        page.within('#js-legacy-tabs-container') do
           expect(page).to have_content('You are not following other users')
           expect(page).not_to have_selector('.gl-card.gl-mb-5')
           expect(page).not_to have_selector('.gl-pagination')
@@ -224,7 +224,7 @@ def push_code_contribution
       end
 
       it 'shows following user' do
-        page.within('#following') do
+        page.within('#js-legacy-tabs-container') do
           expect(page).to have_content(followee.name)
           expect(page).to have_selector('.gl-card.gl-mb-5')
           expect(page).not_to have_selector('.gl-pagination')
@@ -245,7 +245,7 @@ def push_code_contribution
       end
 
       it 'shows paginated following' do
-        page.within('#following') do
+        page.within('#js-legacy-tabs-container') do
           other_users.each_with_index do |followee, i|
             break if i == 20
 
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index f62a1dd67ff4e..5253034d8ca24 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -28,42 +28,6 @@
   end
 
   context 'with public profile' do
-    context 'with `profile_tabs_vue` feature flag disabled' do
-      before do
-        stub_feature_flags(profile_tabs_vue: false)
-      end
-
-      it 'shows all the tabs' do
-        subject
-
-        page.within '.nav-links' do
-          expect(page).to have_link('Overview')
-          expect(page).to have_link('Activity')
-          expect(page).to have_link('Groups')
-          expect(page).to have_link('Contributed projects')
-          expect(page).to have_link('Personal projects')
-          expect(page).to have_link('Snippets')
-          expect(page).to have_link('Followers')
-          expect(page).to have_link('Following')
-        end
-      end
-    end
-
-    it 'shows all the tabs', :js do
-      subject
-
-      page.within '[role="tablist"]' do
-        expect(page).to have_link('Overview')
-        expect(page).to have_link('Activity')
-        expect(page).to have_link('Groups')
-        expect(page).to have_link('Contributed projects')
-        expect(page).to have_link('Personal projects')
-        expect(page).to have_link('Snippets')
-        expect(page).to have_link('Followers')
-        expect(page).to have_link('Following')
-      end
-    end
-
     it 'does not show private profile message' do
       subject
 
@@ -225,11 +189,11 @@
   context 'with private profile' do
     let_it_be(:user) { create(:user, private_profile: true) }
 
-    it 'shows no tab' do
+    it 'shows no page content container' do
       subject
 
       expect(page).to have_css("div.profile-header")
-      expect(page).not_to have_css("ul.nav-links")
+      expect(page).not_to have_css("#js-legacy-tabs-container")
     end
 
     it 'shows private profile message' do
@@ -237,44 +201,6 @@
 
       expect(page).to have_content("This user has a private profile")
     end
-
-    context 'with `profile_tabs_vue` feature flag disabled' do
-      before do
-        stub_feature_flags(profile_tabs_vue: false)
-      end
-
-      it 'shows own tabs' do
-        sign_in(user)
-        subject
-
-        page.within '.nav-links' do
-          expect(page).to have_link('Overview')
-          expect(page).to have_link('Activity')
-          expect(page).to have_link('Groups')
-          expect(page).to have_link('Contributed projects')
-          expect(page).to have_link('Personal projects')
-          expect(page).to have_link('Snippets')
-          expect(page).to have_link('Followers')
-          expect(page).to have_link('Following')
-        end
-      end
-    end
-
-    it 'shows own tabs', :js do
-      sign_in(user)
-      subject
-
-      page.within '[role="tablist"]' do
-        expect(page).to have_link('Overview')
-        expect(page).to have_link('Activity')
-        expect(page).to have_link('Groups')
-        expect(page).to have_link('Contributed projects')
-        expect(page).to have_link('Personal projects')
-        expect(page).to have_link('Snippets')
-        expect(page).to have_link('Followers')
-        expect(page).to have_link('Following')
-      end
-    end
   end
 
   context 'with blocked profile' do
@@ -295,9 +221,9 @@
       visit_profile
     end
 
-    it 'shows no tab' do
+    it 'shows no content container' do
       expect(page).not_to have_css("div.profile-header")
-      expect(page).not_to have_css("ul.nav-links")
+      expect(page).not_to have_css("#js-legacy-tabs-container")
     end
 
     it 'shows no sidebar' do
@@ -350,9 +276,9 @@
         expect(page).to have_css('[data-testid="user-profile-header"]', text: 'Unconfirmed user')
       end
 
-      it 'shows no tab' do
+      it 'shows no content container' do
         expect(page).to have_css('[data-testid="user-profile-header"]')
-        expect(page).not_to have_css("ul.nav-links")
+        expect(page).not_to have_css("#js-legacy-tabs-container")
       end
 
       it 'shows no additional fields' do
@@ -460,29 +386,6 @@
     it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
   end
 
-  context 'with a bot user' do
-    let_it_be(:user) { create(:user, user_type: :security_bot) }
-
-    before do
-      stub_feature_flags(profile_tabs_vue: false)
-    end
-
-    it 'only shows Overview and Activity tabs' do
-      subject
-
-      page.within '.nav-links' do
-        expect(page).to have_link('Overview')
-        expect(page).to have_link('Activity')
-        expect(page).to have_link('Groups')
-        expect(page).to have_link('Contributed projects')
-        expect(page).to have_link('Personal projects')
-        expect(page).to have_link('Snippets')
-        expect(page).to have_link('Followers')
-        expect(page).to have_link('Following')
-      end
-    end
-  end
-
   context 'structured markup' do
     let_it_be(:user) { create(:user, website_url: 'https://gitlab.com', organization: 'GitLab', job_title: 'Frontend Engineer', email: 'public@example.com', public_email: 'public@example.com', location: 'Country', created_at: Time.zone.now, updated_at: Time.zone.now) }
 
diff --git a/spec/features/users/user_browses_projects_on_user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb
index addf9bbdcb1dd..46dee648f9ce8 100644
--- a/spec/features/users/user_browses_projects_on_user_page_spec.rb
+++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb
@@ -77,7 +77,7 @@ def click_nav_link(name)
       visit user_path(user)
       click_nav_link('Personal projects')
 
-      expect(page).to have_css('.tab-content #projects.active')
+      expect(page).to have_css('.user-profile-content #js-legacy-tabs-container')
       expect(title).to start_with(user.name)
 
       expect(page).to have_content(public_project.name)
@@ -142,7 +142,7 @@ def click_nav_link(name)
 
           click_nav_link('Contributed projects')
 
-          page.within '#contributed' do
+          page.within '#js-legacy-tabs-container' do
             expect(page).to have_content(contributed_project.name)
           end
         end
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 36c140464fb45..fc5a912f4334e 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -66,27 +66,38 @@ def filter_ee_badges(badges)
     end
   end
 
-  describe '#profile_tabs' do
-    subject(:tabs) { helper.profile_tabs }
+  describe '#profile_actions' do
+    subject(:profile_actions) { helper.profile_actions(other_user) }
+
+    let_it_be(:other_user) { create(:user) }
 
     before do
       allow(helper).to receive(:current_user).and_return(user)
-      allow(helper).to receive(:can?).and_return(true)
+      allow(user).to receive(:bot?).and_return(false)
+      allow(helper).to receive(:can?).with(user, :read_user_profile, other_user).and_return(true)
     end
 
     context 'with public profile' do
-      it 'includes all the expected tabs' do
-        expect(tabs).to include(:activity, :groups, :contributed, :projects, :starred, :snippets)
+      it 'contains all profile actions' do
+        expect(profile_actions).to match_array [:overview, :activity, :groups, :contributed, :projects, :starred, :snippets, :followers, :following]
       end
     end
 
     context 'with private profile' do
       before do
-        allow(helper).to receive(:can?).with(user, :read_user_profile, nil).and_return(false)
+        allow(helper).to receive(:can?).with(user, :read_user_profile, other_user).and_return(false)
       end
 
       it 'is empty' do
-        expect(tabs).to be_empty
+        expect(profile_actions).to match_array []
+      end
+    end
+
+    context 'with a public bot user' do
+      let_it_be(:other_user) { create(:user, :bot) }
+
+      it 'contains bot profile actions' do
+        expect(profile_actions).to match_array [:overview, :activity]
       end
     end
   end
-- 
GitLab