Files
twenty/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminConfigVariableDetails.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

150 lines
4.4 KiB
TypeScript

import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
import { ConfigVariableHelpText } from '@/settings/admin-panel/config-variables/components/ConfigVariableHelpText';
import { ConfigVariableValueInput } from '@/settings/admin-panel/config-variables/components/ConfigVariableValueInput';
import { useConfigVariableActions } from '@/settings/admin-panel/config-variables/hooks/useConfigVariableActions';
import { ConfigVariableEdit } from '@/settings/config-variables/components/ConfigVariableEdit';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPath, type ConfigVariableValue } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { useQuery } from '@apollo/client/react';
import {
ConfigSource,
GetDatabaseConfigVariableDocument,
} from '~/generated-admin/graphql';
const hasMeaningfulValue = (value: ConfigVariableValue): boolean => {
if (value === null || value === undefined) {
return false;
}
if (typeof value === 'string') {
return value.trim() !== '';
}
if (Array.isArray(value)) {
return value.length > 0;
}
return true;
};
export const SettingsAdminConfigVariableDetails = () => {
const { variableName } = useParams();
const apolloAdminClient = useApolloAdminClient();
const { t } = useLingui();
const [isEditing, setIsEditing] = useState(false);
const isConfigVariablesInDbEnabled = useAtomStateValue(
isConfigVariablesInDbEnabledState,
);
const { data: configVariableData, loading } = useQuery(
GetDatabaseConfigVariableDocument,
{
client: apolloAdminClient,
variables: { key: variableName ?? '' },
fetchPolicy: 'network-only',
},
);
const variable = configVariableData?.getDatabaseConfigVariable;
const { handleUpdateVariable, handleDeleteVariable } =
useConfigVariableActions(variable?.name ?? '');
const [value, setValue] = useState<ConfigVariableValue>(
variable?.value ?? null,
);
if (loading === true || isDefined(variable) === false) {
return <SettingsSkeletonLoader />;
}
const isEnvOnly = variable.isEnvOnly;
const isFromDatabase = variable.source === ConfigSource.DATABASE;
const hasValueChanged =
JSON.stringify(value) !== JSON.stringify(variable.value);
const isValueValid =
!isEnvOnly && hasValueChanged && hasMeaningfulValue(value);
const onSave = async () => {
await handleUpdateVariable(value, isFromDatabase);
};
const onEdit = () => {
if (variable.isSensitive) {
setValue('');
}
};
const canOpenCancelModal = isFromDatabase && !hasValueChanged;
const onCancel = () => {
setValue(variable.value);
};
const onConfirmReset = async () => {
await handleDeleteVariable();
};
return (
<SettingsPageLayout
links={[
{
children: t`Other`,
href: getSettingsPath(SettingsPath.AdminPanel),
},
{
children: t`Admin Panel - Config`,
href: getSettingsPath(
SettingsPath.AdminPanel,
undefined,
undefined,
'config-variables',
),
},
{
children: variable.name,
},
]}
>
<ConfigVariableEdit
title={variable.name}
description={variable.description}
input={
<ConfigVariableValueInput
variable={variable}
value={value}
onChange={setValue}
disabled={isEnvOnly || !isEditing}
/>
}
isEditing={isEditing}
setIsEditing={setIsEditing}
isSaveDisabled={!isValueValid}
onSave={onSave}
onCancel={onCancel}
canOpenCancelModal={canOpenCancelModal}
onEdit={onEdit}
onConfirmReset={onConfirmReset}
editDisabled={isEnvOnly || !isConfigVariablesInDbEnabled}
helpContent={
<ConfigVariableHelpText
variable={variable}
hasValueChanged={hasValueChanged}
/>
}
/>
</SettingsPageLayout>
);
};