Files
twenty/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.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

120 lines
3.6 KiB
TypeScript

import { isDDLLockedState } from '@/client-config/states/isDDLLockedState';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import {
H2Title,
IconEye,
IconHierarchy2,
IconLink,
IconList,
IconPlus,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { UndecoratedLink } from 'twenty-ui/navigation';
import DarkCoverImage from '@/settings/data-model/assets/cover-dark.png';
import LightCoverImage from '@/settings/data-model/assets/cover-light.png';
import { SettingsObjectTable } from '~/pages/settings/data-model/SettingsObjectTable';
const SETTINGS_DATA_MODEL_HERO_INSTANCE_ID_PREFIX = 'settings-data-model-hero';
export const SettingsObjects = () => {
const { t } = useLingui();
const { objectMetadataItems } = useFilteredObjectMetadataItems();
const isDDLLocked = useAtomStateValue(isDDLLockedState);
const heroTabs = [
{
id: 'objects',
title: t`Objects`,
Icon: IconHierarchy2,
vimeoId: '926288174',
},
{
id: 'fields',
title: t`Fields`,
Icon: IconList,
vimeoId: '927628219',
},
{
id: 'relations',
title: t`Relations`,
Icon: IconLink,
vimeoId: '1185511827',
},
];
return (
<SettingsPageLayout
title={t`Data model`}
actionButton={
isDDLLocked ? (
<Button
Icon={IconPlus}
title={t`Add object`}
accent="blue"
size="small"
disabled
/>
) : (
<UndecoratedLink to={getSettingsPath(SettingsPath.NewObject)}>
<Button
Icon={IconPlus}
title={t`Add object`}
accent="blue"
size="small"
/>
</UndecoratedLink>
)
}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.General),
},
{ children: t`Objects` },
]}
>
<SettingsPageContainer>
<Section>
<SettingsDiscoveryHeroCard
lightSrc={LightCoverImage}
darkSrc={DarkCoverImage}
instanceIdPrefix={SETTINGS_DATA_MODEL_HERO_INSTANCE_ID_PREFIX}
tabs={heroTabs}
playButtonAriaLabel={t`Watch data model demo`}
/>
</Section>
<Section>
<H2Title
title={t`Existing objects`}
description={t`Manage objects, fields and relationships`}
/>
<SettingsObjectTable objectMetadataItems={objectMetadataItems} />
</Section>
<Section>
<H2Title
title={t`Visualize data model`}
description={t`See your data structure as an interactive diagram`}
/>
<UndecoratedLink to={getSettingsPath(SettingsPath.ObjectOverview)}>
<Button
title={t`Visualize`}
variant="secondary"
size="small"
Icon={IconEye}
/>
</UndecoratedLink>
</Section>
</SettingsPageContainer>
</SettingsPageLayout>
);
};