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.
150 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
};
|