Files
twenty/packages/twenty-front/src/pages/settings/logic-functions/SettingsLogicFunctionDetail.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

191 lines
6.4 KiB
TypeScript

import { useNavigate, useParams } from 'react-router-dom';
import { useLogicFunctionForm } from '@/logic-functions/hooks/useLogicFunctionForm';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsLogicFunctionLabelContainer } from '@/settings/logic-functions/components/SettingsLogicFunctionLabelContainer';
import { SettingsLogicFunctionSettingsTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionSettingsTab';
import { SettingsLogicFunctionTestTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionTestTab';
import { SettingsLogicFunctionTriggersTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionTriggersTab';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import {
IconBolt,
IconCode,
IconPlayerPlay,
IconSettings,
} from 'twenty-ui/display';
import { useQuery } from '@apollo/client/react';
import { FindOneApplicationDocument } from '~/generated-metadata/graphql';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsLogicFunctionCodeEditorTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionCodeEditorTab';
import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction';
const LOGIC_FUNCTION_DETAIL_ID = 'logic-function-detail';
export const SettingsLogicFunctionDetail = () => {
const { logicFunctionId = '', applicationId } = useParams();
const navigate = useNavigate();
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const { data, loading: applicationLoading } = useQuery(
FindOneApplicationDocument,
{
variables: { id: applicationId ?? '' },
skip: !isDefined(applicationId),
},
);
const applicationName = data?.findOneApplication?.name;
const applicationVariableKeys =
data?.findOneApplication?.applicationVariables?.map(
(variable) => variable.key,
) ?? [];
const workspaceCustomApplicationId =
currentWorkspace?.workspaceCustomApplication?.id;
const isReadonly =
isDefined(applicationId) && applicationId !== workspaceCustomApplicationId;
const instanceId = `${LOGIC_FUNCTION_DETAIL_ID}-${logicFunctionId}`;
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
instanceId,
);
const { formValues, logicFunction, loading, onChange } = useLogicFunctionForm(
{ logicFunctionId },
);
const { executeLogicFunction, isExecuting } = useExecuteLogicFunction({
logicFunctionId,
});
const handleTestFunction = async () => {
navigate('#test');
await executeLogicFunction();
};
const tabs = [
{
id: 'editor',
title: t`Editor`,
Icon: IconCode,
disabled: isReadonly,
hide: isReadonly,
},
{ id: 'settings', title: t`Settings`, Icon: IconSettings },
{ id: 'test', title: t`Test`, Icon: IconPlayerPlay },
{ id: 'triggers', title: t`Triggers`, Icon: IconBolt },
];
const isEditorTab = activeTabId === 'editor';
const isTriggersTab = activeTabId === 'triggers';
const isSettingsTab = activeTabId === 'settings';
const isTestTab = activeTabId === 'test';
const breadcrumbLinks = isDefined(applicationId)
? (() => {
const applicationContentHref = getSettingsPath(
SettingsPath.ApplicationDetail,
{ applicationId },
undefined,
'content',
);
return [
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.General),
},
{
children: t`Applications`,
href: getSettingsPath(SettingsPath.Applications),
},
{ children: applicationName ?? '', href: applicationContentHref },
{ children: t`Logic functions`, href: applicationContentHref },
{ children: logicFunction?.name ?? '' },
];
})()
: [
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.General),
},
{
children: t`AI`,
href: getSettingsPath(SettingsPath.AI),
},
{ children: t`Logic functions` },
{ children: logicFunction?.name ?? '' },
];
const files = [
{
path: 'index.ts',
content: formValues.sourceHandlerCode,
language: 'typescript',
},
];
return (
!loading &&
!applicationLoading && (
<SettingsPageLayout
title={
<SettingsLogicFunctionLabelContainer
value={formValues.name}
onChange={onChange('name')}
readonly={isReadonly}
/>
}
links={breadcrumbLinks}
>
<SettingsPageContainer>
<TabList tabs={tabs} componentInstanceId={instanceId} />
{isEditorTab && (
<SettingsLogicFunctionCodeEditorTab
files={files}
handleExecute={handleTestFunction}
onChange={onChange('sourceHandlerCode')}
isTesting={isExecuting}
applicationVariableKeys={applicationVariableKeys}
/>
)}
{isTriggersTab && (
<SettingsLogicFunctionTriggersTab
formValues={formValues}
onChange={onChange}
readonly={isReadonly}
applicationName={applicationName}
/>
)}
{isSettingsTab && (
<SettingsLogicFunctionSettingsTab
formValues={formValues}
onChange={onChange}
readonly={isReadonly}
/>
)}
{isTestTab && (
<SettingsLogicFunctionTestTab
handleExecute={executeLogicFunction}
logicFunctionId={logicFunctionId}
formValues={formValues}
isTesting={isExecuting}
/>
)}
</SettingsPageContainer>
</SettingsPageLayout>
)
);
};