431f6ae98f
## 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.
191 lines
6.4 KiB
TypeScript
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>
|
|
)
|
|
);
|
|
};
|