Move settings tabs below page headers (#23519)

## Summary

- Position the Admin Panel tabs directly below the settings header.
- Position the Communication tabs directly below the settings header.
- Reuse the shared settings tab bar while preserving permissions,
disabled states, and hash navigation.
- Keep the responsive tab overflow menu available on narrow settings
cards.

## After

<img width="3456" height="2008" alt="Admin Panel tabs positioned below
the settings header"
src="https://github.com/user-attachments/assets/c8ff965c-d4b2-4700-85e1-0763f9f0852d"
/>
This commit is contained in:
Thomas des Francs
2026-07-29 19:37:01 +02:00
committed by GitHub
parent 58ebbe0394
commit 030ee2c7cc
7 changed files with 58 additions and 100 deletions
@@ -3,11 +3,8 @@ import { SettingsAdminApps } from '@/settings/admin-panel/apps/components/Settin
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables';
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus';
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { lazy, Suspense } from 'react';
const SettingsEnterprise = lazy(() =>
@@ -15,12 +12,11 @@ const SettingsEnterprise = lazy(() =>
default: module.SettingsEnterprise,
})),
);
export const SettingsAdminTabContent = () => {
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
SETTINGS_ADMIN_TABS_ID,
);
export const SettingsAdminTabContent = ({
activeTabId,
}: {
activeTabId: string | null;
}) => {
switch (activeTabId) {
case SETTINGS_ADMIN_TABS.GENERAL:
return <SettingsAdminGeneral />;
@@ -1,9 +1,6 @@
import { currentUserState } from '@/auth/states/currentUserState';
import { billingState } from '@/client-config/states/billingState';
import { SettingsAdminTabContent } from '@/settings/admin-panel/components/SettingsAdminTabContent';
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { t } from '@lingui/core/macro';
import {
@@ -15,14 +12,15 @@ import {
IconVariable,
} from 'twenty-ui/icon';
export const SettingsAdminContent = () => {
export const useSettingsAdminTabs = () => {
const currentUser = useAtomStateValue(currentUserState);
const billing = useAtomStateValue(billingState);
const canAccessFullAdminPanel = currentUser?.canAccessFullAdminPanel;
const canImpersonate = currentUser?.canImpersonate;
const isBillingEnabled = billing?.isBillingEnabled;
const tabs = [
return [
{
id: SETTINGS_ADMIN_TABS.GENERAL,
title: t`General`,
@@ -64,15 +62,4 @@ export const SettingsAdminContent = () => {
]
: []),
];
return (
<>
<TabList
tabs={tabs}
behaveAsLinks={true}
componentInstanceId={SETTINGS_ADMIN_TABS_ID}
/>
<SettingsAdminTabContent />
</>
);
};
@@ -1,64 +1,16 @@
import { useSettingsActiveTabId } from '@/settings/components/layout/useSettingsActiveTabId';
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab-list/components/TabListFromUrlOptionalEffect';
import { TAB_LIST_GAP } from '@/ui/layout/tab-list/constants/TabListGap';
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { styled } from '@linaria/react';
import { TabButton } from 'twenty-ui/input';
type SettingsTabBarProps = {
tabs: SingleTabProps[];
componentInstanceId: string;
};
const StyledTabBar = styled.div`
display: flex;
flex: 1;
gap: ${TAB_LIST_GAP}px;
justify-content: center;
min-width: 0;
`;
export const SettingsTabBar = ({
tabs,
componentInstanceId,
}: SettingsTabBarProps) => {
const visibleTabs = tabs.filter((tab) => !tab.hide);
const visibleTabIds = visibleTabs.map((tab) => tab.id);
const activeTabId = useSettingsActiveTabId(
componentInstanceId,
visibleTabIds,
);
if (visibleTabs.length === 0) {
return null;
}
return (
<TabListComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<TabListFromUrlOptionalEffect
isInSidePanel={false}
tabListIds={visibleTabIds}
/>
<StyledTabBar>
{visibleTabs.map((tab) => (
<TabButton
key={tab.id}
id={tab.id}
title={tab.title}
LeftIcon={tab.Icon}
logo={tab.logo}
active={tab.id === activeTabId}
disabled={tab.disabled}
pill={tab.pill}
to={`#${tab.id}`}
tooltipContent={tab.tooltipContent}
/>
))}
</StyledTabBar>
</TabListComponentInstanceContext.Provider>
<TabList tabs={tabs} componentInstanceId={componentInstanceId} centerTabs />
);
};
@@ -36,9 +36,11 @@ const StyledContainer = styled.div`
}
`;
const StyledInnerContainer = styled.div`
const StyledInnerContainer = styled.div<{ $centerTabs: boolean }>`
display: flex;
flex: 1;
justify-content: ${({ $centerTabs }) =>
$centerTabs ? 'center' : 'flex-start'};
min-width: 0;
`;
@@ -76,6 +78,7 @@ export const TabList = ({
componentInstanceId,
onChangeTab,
rightComponent,
centerTabs = false,
}: TabListProps) => {
const visibleTabs = tabs.filter((tab) => !tab.hide);
const navigate = useNavigate();
@@ -160,7 +163,7 @@ export const TabList = ({
<StyledContainer className={className}>
<StyledNodeDimension onDimensionChange={onContainerWidthChange}>
<StyledInnerContainer>
<StyledInnerContainer $centerTabs={centerTabs}>
<StyledTabContainer>
{visibleTabs.slice(0, visibleTabCount).map((tab) => (
<TabButton
@@ -11,4 +11,5 @@ export type TabListProps = {
componentInstanceId: string;
onChangeTab?: (tabId: string) => void;
rightComponent?: ReactNode;
centerTabs?: boolean;
};
@@ -1,16 +1,31 @@
import { SettingsAdminContent } from '@/settings/admin-panel/components/SettingsAdminContent';
import { SettingsAdminTabContent } from '@/settings/admin-panel/components/SettingsAdminTabContent';
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { useSettingsAdminTabs } from '@/settings/admin-panel/hooks/useSettingsAdminTabs';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsTabBar } from '@/settings/components/layout/SettingsTabBar';
import { useSettingsActiveTabId } from '@/settings/components/layout/useSettingsActiveTabId';
import { useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
export const SettingsAdmin = () => {
const { t } = useLingui();
const tabs = useSettingsAdminTabs();
const activeTabId = useSettingsActiveTabId(
SETTINGS_ADMIN_TABS_ID,
tabs.map((tab) => tab.id),
);
return (
<SettingsPageLayout
title={t`Admin Panel`}
secondaryBar={
<SettingsTabBar
tabs={tabs}
componentInstanceId={SETTINGS_ADMIN_TABS_ID}
/>
}
links={[
{
children: t`Other`,
@@ -20,7 +35,7 @@ export const SettingsAdmin = () => {
]}
>
<SettingsPageContainer>
<SettingsAdminContent />
<SettingsAdminTabContent activeTabId={activeTabId} />
</SettingsPageContainer>
</SettingsPageLayout>
);
@@ -5,7 +5,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { SettingsWorkspaceEmailGroupSection } from '@/settings/workspace/components/SettingsWorkspaceEmailGroupSection';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { SettingsTabBar } from '@/settings/components/layout/SettingsTabBar';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
@@ -43,6 +43,24 @@ export const SettingsWorkspaceCommunications = () => {
FeatureFlagKey.IS_EMAIL_GROUP_ENABLED,
);
const tabs = [
{ id: 'emails', title: t`Emails`, Icon: IconMail },
{
id: 'whatsapp',
title: t`Whatsapp`,
Icon: IconBrandWhatsapp,
disabled: true,
pill: t`Soon`,
},
{
id: 'calls',
title: t`Calls`,
Icon: IconPhone,
disabled: true,
pill: t`Soon`,
},
];
if (!isEmailGroupFeatureEnabled) {
return null;
}
@@ -50,6 +68,12 @@ export const SettingsWorkspaceCommunications = () => {
return (
<SettingsPageLayout
title={t`Communication`}
secondaryBar={
<SettingsTabBar
tabs={tabs}
componentInstanceId={COMMUNICATIONS_TABS_INSTANCE_ID}
/>
}
links={[
{
children: t`Workspace`,
@@ -59,26 +83,6 @@ export const SettingsWorkspaceCommunications = () => {
]}
>
<SettingsPageContainer>
<TabList
componentInstanceId={COMMUNICATIONS_TABS_INSTANCE_ID}
tabs={[
{ id: 'emails', title: t`Emails`, Icon: IconMail },
{
id: 'whatsapp',
title: t`Whatsapp`,
Icon: IconBrandWhatsapp,
disabled: true,
pill: t`Soon`,
},
{
id: 'calls',
title: t`Calls`,
Icon: IconPhone,
disabled: true,
pill: t`Soon`,
},
]}
/>
<Section>
<SettingsDiscoveryHeroCard
lightSrc={coverLight}