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:
committed by
GitHub
parent
58ebbe0394
commit
030ee2c7cc
+5
-9
@@ -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 />;
|
||||
|
||||
+3
-16
@@ -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>
|
||||
);
|
||||
|
||||
+25
-21
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user