Files
twenty/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx
T
Félix Malfait 431f6ae98f feat(settings): move settings chrome into a single rounded card (#21131)
## What

Replaces `SubMenuTopBarContainer` with a settings-specific
`SettingsPageLayout` that puts the whole page chrome — breadcrumb,
centered title, actions, an optional secondary bar (tabs or wizard
step), and the 760px body — inside **one rounded card**, with
`SidePanelForDesktop` as a sibling. Title, tabs and body content share
one centered vertical axis at every card width.

Supersedes #21122. One PR, no feature flag.

## New components (`@/settings/components/layout/`)

- **SettingsPageLayout** — owns the rounded card + side-panel sibling,
`useCommandMenuHotKeys`, mobile command menu
- **SettingsPageHeader** — breadcrumb · centered title · actions in a
symmetric `1fr auto 1fr` grid (symmetric padding throughout)
- **SettingsSecondaryBar** — the secondary row, bracketed by top +
bottom borders
- **SettingsTabBar** — centered tabs reusing `activeTabIdComponentState`
+ `TabListFromUrlOptionalEffect` for URL-hash sync (does not touch the
shared `TabList`)
- **SettingsWizardStepBar** — back arrow · "N. Label" · optional
trailing slot

## Migrations

- Bulk rename across ~80 call sites (`SubMenuTopBarContainer` →
`SettingsPageLayout`); old component deleted.
- 5 tab pages (AI, APIs & Webhooks, Applications, Members, Role) + the
Data Model object-detail page render their tabs in `secondaryBar`
(object-detail keeps "See records" / "New Field" in the header actions).
- The 2 role object-level steps render the wizard step bar with working
back navigation.
- Accounts consolidated into **General / Emails / Calendars** tabs;
standalone `SettingsAccountsEmails` / `SettingsAccountsCalendars` pages
+ routes + stories removed. `SettingsPath.AccountsEmails` /
`AccountsCalendars` now resolve to `accounts#emails` /
`accounts#calendars`, so existing `getSettingsPath()` links deep-link to
the right tab via the existing hash sync — no call-site changes.

## Verification

- `nx typecheck twenty-front` and `nx lint twenty-front` both clean.
- Browser (logged-in workspace): title / tab / body / card centers align
on a single axis at multiple widths — width-invariant, so alignment
holds when the AI side panel (a sibling) shrinks the card. Rounded card
with even gaps on all four sides; tab row bracketed by two 1px lines;
no-tab pages render header → body with no lines; wizard back navigation
works; `…/accounts#emails` opens the Emails tab.

The shared `PageHeader` and `TabList` are untouched. The settings side
panel itself isn't wired to open yet — that's a follow-up PR.
2026-06-02 14:22:57 +02:00

228 lines
6.8 KiB
TypeScript

import { FeatureFlagKey, SettingsPath } from 'twenty-shared/types';
import { useAuth } from '@/auth/hooks/useAuth';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { billingState } from '@/client-config/states/billingState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
import {
type NavigationDrawerItemIndentationLevel,
type NavigationDrawerItemModifier,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import {
IconApi,
// IconApps, // TODO: Re-enable when integrations page is ready
IconAt,
IconCalendarEvent,
IconColorSwatch,
type IconComponent,
IconCurrencyDollar,
IconDoorEnter,
IconHelpCircle,
IconHierarchy2,
IconLayout,
IconMail,
IconMessage,
IconPlug,
IconServer,
IconSettings,
IconSparkles,
IconUserCircle,
IconUsers,
} from 'twenty-ui/display';
import { PermissionFlagType } from '~/generated-metadata/graphql';
export type SettingsNavigationSection = {
label: string;
items: SettingsNavigationItem[];
isAdvanced?: boolean;
};
export type SettingsNavigationItem = {
label: string;
path?: SettingsPath;
onClick?: () => void;
Icon: IconComponent;
indentationLevel?: NavigationDrawerItemIndentationLevel;
matchSubPages?: boolean;
isHidden?: boolean;
subItems?: SettingsNavigationItem[];
isAdvanced?: boolean;
modifier?: NavigationDrawerItemModifier;
};
const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
const billing = useAtomStateValue(billingState);
const { signOut } = useAuth();
const supportChat = useAtomStateValue(supportChatState);
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const isBillingEnabled = billing?.isBillingEnabled ?? false;
const currentUser = useAtomStateValue(currentUserState);
const isAdminEnabled =
(currentUser?.canImpersonate || currentUser?.canAccessFullAdminPanel) ??
false;
const isSupportChatConfigured =
supportChat?.supportDriver === 'FRONT' &&
isNonEmptyString(supportChat.supportFrontChatId);
const permissionMap = usePermissionFlagMap();
const isEmailGroupFeatureEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_EMAIL_GROUP_ENABLED,
);
return [
{
label: t`User`,
items: [
{
label: t`Profile`,
path: SettingsPath.ProfilePage,
Icon: IconUserCircle,
},
{
label: t`Experience`,
path: SettingsPath.Experience,
Icon: IconColorSwatch,
},
{
label: t`Accounts`,
path: SettingsPath.Accounts,
Icon: IconAt,
isHidden: !permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS],
subItems: [
{
label: t`Emails`,
path: SettingsPath.AccountsEmails,
Icon: IconMail,
isHidden: !permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS],
indentationLevel: 2,
},
{
label: t`Calendars`,
path: SettingsPath.AccountsCalendars,
Icon: IconCalendarEvent,
isHidden: !permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS],
indentationLevel: 2,
},
],
},
],
},
{
label: t`Workspace`,
items: [
{
label: t`General`,
path: SettingsPath.General,
Icon: IconSettings,
isHidden: !permissionMap[PermissionFlagType.WORKSPACE],
},
{
label: t`Data model`,
path: SettingsPath.Objects,
Icon: IconHierarchy2,
isHidden: !permissionMap[PermissionFlagType.DATA_MODEL],
},
{
label: t`Layout`,
path: SettingsPath.Layout,
Icon: IconLayout,
isHidden: !permissionMap[PermissionFlagType.LAYOUTS],
},
{
label: t`Members`,
path: SettingsPath.WorkspaceMembersPage,
Icon: IconUsers,
isHidden: !permissionMap[PermissionFlagType.WORKSPACE_MEMBERS],
},
{
label: t`Billing`,
path: SettingsPath.Billing,
Icon: IconCurrencyDollar,
isHidden:
!isBillingEnabled || !permissionMap[PermissionFlagType.WORKSPACE],
},
{
label: t`APIs & Webhooks`,
path: SettingsPath.ApiWebhooks,
Icon: IconApi,
isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS],
},
// TODO: Re-enable when integrations page is ready
// {
// label: t`Integrations`,
// path: SettingsPath.Integrations,
// Icon: IconApps,
// isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS],
// },
{
label: t`Apps`,
path: SettingsPath.Applications,
Icon: IconPlug,
isHidden: !permissionMap[PermissionFlagType.APPLICATIONS],
},
{
label: t`AI`,
path: SettingsPath.AI,
Icon: IconSparkles,
isHidden: !permissionMap[PermissionFlagType.AI],
},
{
label: t`Email`,
path: SettingsPath.WorkspaceEmail,
Icon: IconMail,
isHidden:
!isEmailGroupFeatureEnabled ||
!permissionMap[PermissionFlagType.WORKSPACE],
},
],
},
{
label: t`Other`,
items: [
{
label: t`Admin Panel`,
path: SettingsPath.AdminPanel,
Icon: IconServer,
isHidden: !isAdminEnabled,
},
{
label: t`Community`,
path: SettingsPath.Community,
Icon: IconUsers,
isHidden: !permissionMap[PermissionFlagType.WORKSPACE],
},
{
label: t`Support`,
onClick: () => window.FrontChat?.('show'),
Icon: IconMessage,
isHidden: !isSupportChatConfigured,
},
{
label: t`Documentation`,
onClick: () =>
window.open(
getDocumentationUrl({ locale: currentWorkspaceMember?.locale }),
'_blank',
),
Icon: IconHelpCircle,
},
{
label: t`Logout`,
onClick: signOut,
Icon: IconDoorEnter,
matchSubPages: false,
},
],
},
];
};
export { useSettingsNavigationItems };