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.
This commit is contained in:
+350
@@ -0,0 +1,350 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { isClickHouseConfiguredState } from '@/client-config/states/isClickHouseConfiguredState';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { Separator } from '@/settings/components/Separator';
|
||||
import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard';
|
||||
import { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton';
|
||||
import { SettingsOptionCardContentCounter } from '@/settings/components/SettingsOptions/SettingsOptionCardContentCounter';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { SettingsRoleDefaultRole } from '@/settings/roles/components/SettingsRolesDefaultRole';
|
||||
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
|
||||
import { useSettingsAllRoles } from '@/settings/roles/hooks/useSettingsAllRoles';
|
||||
import { SettingsSSOIdentitiesProvidersListCard } from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCard';
|
||||
import { SettingsSecurityAuthBypassOptionsList } from '@/settings/security/components/SettingsSecurityAuthBypassOptionsList';
|
||||
import { SettingsSecurityAuthProvidersOptionsList } from '@/settings/security/components/SettingsSecurityAuthProvidersOptionsList';
|
||||
import { SettingsSecurityEditableProfileFields } from '@/settings/security/components/SettingsSecurityEditableProfileFields';
|
||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
|
||||
import { ToggleImpersonate } from '@/settings/workspace/components/ToggleImpersonate';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import {
|
||||
H2Title,
|
||||
IconClockHour8,
|
||||
IconHistory,
|
||||
IconLock,
|
||||
IconMail,
|
||||
IconTrash,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { UpdateWorkspaceDocument } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledMainContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[10]};
|
||||
min-height: 200px;
|
||||
`;
|
||||
|
||||
const StyledSectionContainer = styled.div`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const StyledLinkContainer = styled.div`
|
||||
> a {
|
||||
text-decoration: none;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SettingsSecuritySettings = () => {
|
||||
const { t } = useLingui();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const isMultiWorkspaceEnabled = useAtomStateValue(
|
||||
isMultiWorkspaceEnabledState,
|
||||
);
|
||||
const isClickHouseConfigured = useAtomStateValue(isClickHouseConfiguredState);
|
||||
const authProviders = useAtomStateValue(authProvidersState);
|
||||
const SSOIdentitiesProviders = useAtomStateValue(SSOIdentitiesProvidersState);
|
||||
const [currentWorkspace, setCurrentWorkspace] = useAtomState(
|
||||
currentWorkspaceState,
|
||||
);
|
||||
const [updateWorkspace] = useMutation(UpdateWorkspaceDocument);
|
||||
|
||||
const saveTrashRetention = useDebouncedCallback(async (value: number) => {
|
||||
try {
|
||||
await updateWorkspace({
|
||||
variables: {
|
||||
input: {
|
||||
trashRetentionDays: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: CombinedGraphQLErrors.is(err) ? err : undefined,
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const saveEventLogRetention = useDebouncedCallback(async (value: number) => {
|
||||
try {
|
||||
await updateWorkspace({
|
||||
variables: {
|
||||
input: {
|
||||
eventLogRetentionDays: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: CombinedGraphQLErrors.is(err) ? err : undefined,
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const handleTrashRetentionDaysChange = (value: number) => {
|
||||
if (!currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === currentWorkspace.trashRetentionDays) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentWorkspace({
|
||||
...currentWorkspace,
|
||||
trashRetentionDays: value,
|
||||
});
|
||||
|
||||
saveTrashRetention(value);
|
||||
};
|
||||
|
||||
const handleSyncInternalEmailsChange = (value: boolean) => {
|
||||
if (!currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === currentWorkspace.isInternalMessagesImportEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentWorkspace({
|
||||
...currentWorkspace,
|
||||
isInternalMessagesImportEnabled: value,
|
||||
});
|
||||
|
||||
updateWorkspace({
|
||||
variables: {
|
||||
input: {
|
||||
isInternalMessagesImportEnabled: value,
|
||||
},
|
||||
},
|
||||
}).catch((err) => {
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: CombinedGraphQLErrors.is(err) ? err : undefined,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleEventLogRetentionDaysChange = (value: number) => {
|
||||
if (!currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === currentWorkspace.eventLogRetentionDays) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentWorkspace({
|
||||
...currentWorkspace,
|
||||
eventLogRetentionDays: value,
|
||||
});
|
||||
|
||||
saveEventLogRetention(value);
|
||||
};
|
||||
|
||||
const roles = useSettingsAllRoles();
|
||||
|
||||
const hasSsoIdentityProviders = SSOIdentitiesProviders.length > 0;
|
||||
const hasDirectAuthEnabled =
|
||||
currentWorkspace?.isGoogleAuthEnabled ||
|
||||
currentWorkspace?.isMicrosoftAuthEnabled ||
|
||||
currentWorkspace?.isPasswordAuthEnabled;
|
||||
const hasBypassProviderAvailable =
|
||||
authProviders?.google ||
|
||||
authProviders?.microsoft ||
|
||||
authProviders?.password;
|
||||
const shouldShowBypassSection =
|
||||
hasSsoIdentityProviders &&
|
||||
!hasDirectAuthEnabled &&
|
||||
hasBypassProviderAvailable;
|
||||
|
||||
const hasEnterpriseAccess =
|
||||
currentWorkspace?.hasValidSignedEnterpriseKey === true;
|
||||
const isEventLogsEnabled = hasEnterpriseAccess && isClickHouseConfigured;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsRolesQueryEffect />
|
||||
<StyledMainContent>
|
||||
<StyledSectionContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`SSO`}
|
||||
description={t`Configure an SSO connection`}
|
||||
adornment={
|
||||
<Tag
|
||||
text={t`Enterprise`}
|
||||
color="transparent"
|
||||
Icon={IconLock}
|
||||
variant="border"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingsSSOIdentitiesProvidersListCard />
|
||||
</Section>
|
||||
</StyledSectionContainer>
|
||||
|
||||
<Section>
|
||||
<StyledContainer>
|
||||
<H2Title
|
||||
title={t`Authentication`}
|
||||
description={t`Customize your workspace security`}
|
||||
/>
|
||||
<SettingsSecurityAuthProvidersOptionsList />
|
||||
</StyledContainer>
|
||||
</Section>
|
||||
<Section>
|
||||
<StyledContainer>
|
||||
<H2Title
|
||||
title={t`Editable Profile Fields`}
|
||||
description={t`Choose which profile fields users with the Edit Profile permission can modify`}
|
||||
/>
|
||||
<SettingsSecurityEditableProfileFields />
|
||||
</StyledContainer>
|
||||
</Section>
|
||||
<SettingsRoleDefaultRole roles={roles} />
|
||||
{shouldShowBypassSection && (
|
||||
<Section>
|
||||
<StyledContainer>
|
||||
<H2Title
|
||||
title={t`SSO Bypass`}
|
||||
description={t`Configure fallback login methods for users with SSO bypass permissions`}
|
||||
/>
|
||||
<SettingsSecurityAuthBypassOptionsList />
|
||||
</StyledContainer>
|
||||
</Section>
|
||||
)}
|
||||
{isMultiWorkspaceEnabled && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Support`}
|
||||
description={t`Manage support access settings`}
|
||||
/>
|
||||
<ToggleImpersonate />
|
||||
</Section>
|
||||
)}
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Audit Logs`}
|
||||
description={t`View workspace activity logs`}
|
||||
adornment={
|
||||
<Tag
|
||||
text={t`Enterprise`}
|
||||
color="transparent"
|
||||
Icon={IconLock}
|
||||
variant="border"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{hasEnterpriseAccess ? (
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentButton
|
||||
Icon={IconHistory}
|
||||
title={t`Workspace Events`}
|
||||
description={
|
||||
!isClickHouseConfigured
|
||||
? t`ClickHouse is required for audit logs. Contact your administrator.`
|
||||
: t`View and filter events, page views, object changes`
|
||||
}
|
||||
Button={
|
||||
<StyledLinkContainer>
|
||||
<Link
|
||||
to={getSettingsPath(SettingsPath.EventLogs)}
|
||||
data-disabled={!isEventLogsEnabled}
|
||||
>
|
||||
<Button
|
||||
title={t`View Logs`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={!isEventLogsEnabled}
|
||||
/>
|
||||
</Link>
|
||||
</StyledLinkContainer>
|
||||
}
|
||||
/>
|
||||
{isEventLogsEnabled && (
|
||||
<>
|
||||
<Separator />
|
||||
<SettingsOptionCardContentCounter
|
||||
Icon={IconClockHour8}
|
||||
title={t`Log retention`}
|
||||
description={t`Number of days to retain audit logs (30-1095 days)`}
|
||||
value={currentWorkspace?.eventLogRetentionDays ?? 90}
|
||||
onChange={handleEventLogRetentionDaysChange}
|
||||
minValue={30}
|
||||
maxValue={1095}
|
||||
showButtons={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
) : (
|
||||
<SettingsEnterpriseFeatureGateCard
|
||||
title={t`Enterprise feature`}
|
||||
description={t`Upgrade to Enterprise to access audit logs.`}
|
||||
buttonTitle={t`Activate`}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title title={t`Other`} description={t`Other security settings`} />
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentCounter
|
||||
Icon={IconTrash}
|
||||
title={t`Erasure of soft-deleted records`}
|
||||
description={t`Permanent deletion. Enter the number of days.`}
|
||||
value={currentWorkspace?.trashRetentionDays ?? 14}
|
||||
onChange={handleTrashRetentionDaysChange}
|
||||
minValue={0}
|
||||
showButtons={false}
|
||||
/>
|
||||
<Separator />
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconMail}
|
||||
title={t`Sync Internal Emails`}
|
||||
description={t`Include emails where all participants share the same domain.`}
|
||||
checked={
|
||||
currentWorkspace?.isInternalMessagesImportEnabled ?? false
|
||||
}
|
||||
onChange={handleSyncInternalEmailsChange}
|
||||
advancedMode
|
||||
/>
|
||||
</Card>
|
||||
</Section>
|
||||
</StyledMainContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user