From 25ed5dde889824a7c5051e44555e9659c64fd029 Mon Sep 17 00:00:00 2001 From: Peter Hegman Date: Wed, 15 Oct 2025 13:00:18 -0700 Subject: [PATCH 1/2] Hide organization switcher for users with one organization To avoid confusion and make the navigation less cluttered --- .../super_sidebar/components/super_topbar.vue | 5 +- .../super_sidebar/components/user_bar.vue | 5 +- app/helpers/sidebars_helper.rb | 3 +- app/models/user.rb | 4 + .../components/super_topbar_spec.js | 62 ++++++++++--- .../super_sidebar/components/user_bar_spec.js | 91 ++++++++++--------- spec/frontend/super_sidebar/mock_data.js | 1 + spec/helpers/sidebars_helper_spec.rb | 3 +- spec/models/user_spec.rb | 21 +++++ 9 files changed, 135 insertions(+), 60 deletions(-) diff --git a/app/assets/javascripts/super_sidebar/components/super_topbar.vue b/app/assets/javascripts/super_sidebar/components/super_topbar.vue index 303afb75de1018..9d83f548b699ae 100644 --- a/app/assets/javascripts/super_sidebar/components/super_topbar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_topbar.vue @@ -58,7 +58,10 @@ export default { }, shouldShowOrganizationSwitcher() { return ( - this.glFeatures.uiForOrganizations && this.isLoggedIn && window.gon.current_organization + this.glFeatures.uiForOrganizations && + this.isLoggedIn && + window.gon.current_organization && + this.sidebarData.has_multiple_organizations ); }, showAdminButton() { diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue index a2899ae7751363..fb2906315fa6ae 100644 --- a/app/assets/javascripts/super_sidebar/components/user_bar.vue +++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue @@ -65,7 +65,10 @@ export default { computed: { shouldShowOrganizationSwitcher() { return ( - this.glFeatures.uiForOrganizations && this.isLoggedIn && window.gon.current_organization + this.glFeatures.uiForOrganizations && + this.isLoggedIn && + window.gon.current_organization && + this.sidebarData.has_multiple_organizations ); }, }, diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 0d4dd17895ccd3..2c114cd2eb7291 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -112,7 +112,8 @@ def super_sidebar_logged_in_context(user, group:, project:, panel:, panel_type:) stop_impersonation_path: admin_impersonation_path, shortcut_links: shortcut_links(user: user, project: project), track_visits_path: track_namespace_visits_path, - work_items: work_items_modal_data(group, project) + work_items: work_items_modal_data(group, project), + has_multiple_organizations: user.has_multiple_organizations? }) end diff --git a/app/models/user.rb b/app/models/user.rb index b4403061ddafd6..78931a5184dabf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1830,6 +1830,10 @@ def solo_owned_organizations .where_exists(counts) end + def has_multiple_organizations? + organizations.limit(2).count > 1 + end + def can_leave_project?(project) project.namespace != namespace && project.member(self) diff --git a/spec/frontend/super_sidebar/components/super_topbar_spec.js b/spec/frontend/super_sidebar/components/super_topbar_spec.js index 13156dcd596320..76b362168169b1 100644 --- a/spec/frontend/super_sidebar/components/super_topbar_spec.js +++ b/spec/frontend/super_sidebar/components/super_topbar_spec.js @@ -11,7 +11,7 @@ import UserMenu from '~/super_sidebar/components/user_menu.vue'; import PromoMenu from '~/super_sidebar/components/promo_menu.vue'; import waitForPromises from 'helpers/wait_for_promises'; import { stubComponent } from 'helpers/stub_component'; -import { defaultOrganization as currentOrganization } from 'jest/organizations/mock_data'; +import { defaultOrganization as mockCurrentOrganization } from 'jest/organizations/mock_data'; import { sidebarData as mockSidebarData } from '../mock_data'; describe('SuperTopbar', () => { @@ -83,21 +83,53 @@ describe('SuperTopbar', () => { }); describe('Organization switcher', () => { - it('does not render the organization switcher', () => { - expect(findOrganizationSwitcher().exists()).toBe(false); - }); - - describe('when `ui_for_organizations` feature flag is enabled, user is logged in and current organization is set', () => { - beforeEach(async () => { - window.gon.current_organization = currentOrganization; - createComponent({ is_logged_in: true }, { glFeatures: { uiForOrganizations: true } }); - await waitForPromises(); - }); + describe.each` + isFeatureFlagEnabled | isLoggedIn | currentOrganization | hasMultipleOrganizations | expected + ${false} | ${false} | ${undefined} | ${false} | ${false} + ${false} | ${false} | ${undefined} | ${true} | ${false} + ${false} | ${false} | ${mockCurrentOrganization} | ${false} | ${false} + ${false} | ${false} | ${mockCurrentOrganization} | ${true} | ${false} + ${false} | ${true} | ${undefined} | ${false} | ${false} + ${false} | ${true} | ${undefined} | ${true} | ${false} + ${false} | ${true} | ${mockCurrentOrganization} | ${false} | ${false} + ${false} | ${true} | ${mockCurrentOrganization} | ${true} | ${false} + ${true} | ${false} | ${undefined} | ${false} | ${false} + ${true} | ${false} | ${undefined} | ${true} | ${false} + ${true} | ${false} | ${mockCurrentOrganization} | ${false} | ${false} + ${true} | ${false} | ${mockCurrentOrganization} | ${true} | ${false} + ${true} | ${true} | ${undefined} | ${false} | ${false} + ${true} | ${true} | ${undefined} | ${true} | ${false} + ${true} | ${true} | ${mockCurrentOrganization} | ${false} | ${false} + ${true} | ${true} | ${mockCurrentOrganization} | ${true} | ${true} + `( + 'when `ui_for_organizations` feature flag is $isFeatureFlagEnabled, logged in state is $isLoggedIn, current organization $currentOrganization, and has_multiple_organizations is $hasMultipleOrganizations', + ({ + isFeatureFlagEnabled, + isLoggedIn, + currentOrganization, + hasMultipleOrganizations, + expected, + }) => { + beforeEach(async () => { + window.gon.current_organization = currentOrganization; + createComponent( + { + sidebarData: { + ...mockSidebarData, + is_logged_in: isLoggedIn, + has_multiple_organizations: hasMultipleOrganizations, + }, + }, + { glFeatures: { uiForOrganizations: isFeatureFlagEnabled } }, + ); + await waitForPromises(); + }); - it('renders the organization switcher', () => { - expect(findOrganizationSwitcher().exists()).toBe(true); - }); - }); + it(`expects organization switcher existence to be ${expected}`, () => { + expect(findOrganizationSwitcher().exists()).toBe(expected); + }); + }, + ); }); describe('Search', () => { diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js index 0360798125b3cc..8fb5230afac8b0 100644 --- a/spec/frontend/super_sidebar/components/user_bar_spec.js +++ b/spec/frontend/super_sidebar/components/user_bar_spec.js @@ -12,9 +12,9 @@ import OrganizationSwitcher from '~/super_sidebar/components/organization_switch import UserBar from '~/super_sidebar/components/user_bar.vue'; import UserCounts from '~/super_sidebar/components/user_counts.vue'; import waitForPromises from 'helpers/wait_for_promises'; -import { isLoggedIn } from '~/lib/utils/common_utils'; import { stubComponent } from 'helpers/stub_component'; -import { defaultOrganization as currentOrganization } from 'jest/organizations/mock_data'; +import { isLoggedIn as isLoggedInUtil } from '~/lib/utils/common_utils'; +import { defaultOrganization as mockCurrentOrganization } from 'jest/organizations/mock_data'; import { sidebarData as mockSidebarData, loggedOutSidebarData } from '../mock_data'; import { MOCK_DEFAULT_SEARCH_OPTIONS, @@ -221,46 +221,55 @@ describe('UserBar component', () => { }); }); - describe('when `ui_for_organizations` feature flag is enabled, user is logged in and current organization is set', () => { - beforeEach(async () => { - window.gon.current_organization = currentOrganization; - isLoggedIn.mockReturnValue(true); - createWrapper({ provideOverrides: { glFeatures: { uiForOrganizations: true } } }); - await waitForPromises(); - }); - - it('renders `OrganizationSwitcher component', () => { - expect(findOrganizationSwitcher().exists()).toBe(true); - }); - }); - - describe.each` - featureFlagEnabled | isLoggedInValue | currentOrganizationValue - ${true} | ${true} | ${undefined} - ${true} | ${false} | ${currentOrganization} - ${true} | ${false} | ${undefined} - ${false} | ${true} | ${currentOrganization} - ${false} | ${false} | ${currentOrganization} - ${false} | ${false} | ${undefined} - `( - 'when `ui_for_organizations` feature flag is $featureFlagEnabled, isLoggedIn is $isLoggedInValue, and current organization is $currentOrganizationValue', - ({ featureFlagEnabled, isLoggedInValue, currentOrganizationValue }) => { - beforeEach(async () => { - window.gon.current_organization = currentOrganizationValue; - isLoggedIn.mockReturnValue(isLoggedInValue); - createWrapper({ - provideOverrides: { - glFeatures: { - uiForOrganizations: featureFlagEnabled, + describe('Organization switcher', () => { + describe.each` + isFeatureFlagEnabled | isLoggedIn | currentOrganization | hasMultipleOrganizations | expected + ${false} | ${false} | ${undefined} | ${false} | ${false} + ${false} | ${false} | ${undefined} | ${true} | ${false} + ${false} | ${false} | ${mockCurrentOrganization} | ${false} | ${false} + ${false} | ${false} | ${mockCurrentOrganization} | ${true} | ${false} + ${false} | ${true} | ${undefined} | ${false} | ${false} + ${false} | ${true} | ${undefined} | ${true} | ${false} + ${false} | ${true} | ${mockCurrentOrganization} | ${false} | ${false} + ${false} | ${true} | ${mockCurrentOrganization} | ${true} | ${false} + ${true} | ${false} | ${undefined} | ${false} | ${false} + ${true} | ${false} | ${undefined} | ${true} | ${false} + ${true} | ${false} | ${mockCurrentOrganization} | ${false} | ${false} + ${true} | ${false} | ${mockCurrentOrganization} | ${true} | ${false} + ${true} | ${true} | ${undefined} | ${false} | ${false} + ${true} | ${true} | ${undefined} | ${true} | ${false} + ${true} | ${true} | ${mockCurrentOrganization} | ${false} | ${false} + ${true} | ${true} | ${mockCurrentOrganization} | ${true} | ${true} + `( + 'when `ui_for_organizations` feature flag is $isFeatureFlagEnabled, logged in state is $isLoggedIn, current organization $currentOrganization, and has_multiple_organizations is $hasMultipleOrganizations', + ({ + isFeatureFlagEnabled, + isLoggedIn, + currentOrganization, + hasMultipleOrganizations, + expected, + }) => { + beforeEach(async () => { + window.gon.current_organization = currentOrganization; + isLoggedInUtil.mockReturnValue(isLoggedIn); + createWrapper({ + sidebarData: { + ...mockSidebarData, + has_multiple_organizations: hasMultipleOrganizations, }, - }, + provideOverrides: { + glFeatures: { + uiForOrganizations: isFeatureFlagEnabled, + }, + }, + }); + await waitForPromises(); }); - await waitForPromises(); - }); - it('does not render `OrganizationSwitcher component', () => { - expect(findOrganizationSwitcher().exists()).toBe(false); - }); - }, - ); + it(`expects organization switcher existence to be ${expected}`, () => { + expect(findOrganizationSwitcher().exists()).toBe(expected); + }); + }, + ); + }); }); diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js index 1add14c751364b..a9b3dc2040d081 100644 --- a/spec/frontend/super_sidebar/mock_data.js +++ b/spec/frontend/super_sidebar/mock_data.js @@ -215,6 +215,7 @@ export const sidebarData = { sign_in_path: '/sign_in', allow_signup: true, new_user_registration_path: '/sign_up', + has_multiple_organizations: false, }; export const loggedOutSidebarData = { diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index d9cdfb692679b7..dfc61c2c7ae569 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -148,7 +148,8 @@ update_pins_url: pins_path, shortcut_links: global_shortcut_links, track_visits_path: track_namespace_visits_path, - work_items: nil + work_items: nil, + has_multiple_organizations: false }) end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4c4bfdbccc5bd5..3a2a57c83d11dc 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -5596,6 +5596,27 @@ def login_method(login) it_behaves_like 'resolves user solo-owned organizations' end + describe '#has_multiple_organizations?' do + let_it_be(:organization) { create(:organization) } + + context 'when user has multiple organizations' do + let_it_be(:organization_2) { create(:organization) } + let_it_be(:user) { create(:user, organizations: [organization, organization_2]) } + + it 'returns true' do + expect(user.has_multiple_organizations?).to eq(true) + end + end + + context 'when user has one organization' do + let_it_be(:user) { create(:user, organizations: [organization]) } + + it 'returns false' do + expect(user.has_multiple_organizations?).to eq(false) + end + end + end + describe '#can_remove_self?' do let(:user) { create(:user) } -- GitLab From 57c51bb4b22404a06d19f4d3f5dc230b6f014c93 Mon Sep 17 00:00:00 2001 From: Peter Hegman Date: Fri, 17 Oct 2025 10:44:52 -0700 Subject: [PATCH 2/2] Switch to many? Per backend reviewer suggestion --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 78931a5184dabf..53da8e1a3a88f9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1831,7 +1831,7 @@ def solo_owned_organizations end def has_multiple_organizations? - organizations.limit(2).count > 1 + organizations.many? end def can_leave_project?(project) -- GitLab