Polish settings page titles and admin tables (#22305)

## Summary

- Reuse the shared settings title presentation for read-only and
editable settings page titles.
- Polish settings AI/app icons, breadcrumb cropping, and admin detail
title icons.
- Align admin panel table/card typography and spacing with existing
settings tables.

## Before/After

<img width="1524" height="2214" alt="Settings pages before and after"
src="https://github.com/user-attachments/assets/91037b25-b442-4eb2-b244-1d8280ce2cd9"
/>

<img width="2200" height="3268" alt="Additional settings UI before and
after"
src="https://github.com/user-attachments/assets/f10d7283-7031-4958-8539-649c3067cd31"
/>

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22305?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Thomas des Francs
2026-06-29 17:39:30 +02:00
committed by GitHub
parent b199dde840
commit 2bb7fb2e9b
23 changed files with 347 additions and 258 deletions
@@ -1,5 +1,6 @@
import { AppChip } from '@/applications/components/AppChip';
import { ApplicationDisplay } from '@/applications/components/ApplicationDisplay';
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
import { StyledNameTableCell } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -209,20 +210,14 @@ const SettingsAdminAppsTableRow = ({
mobileGridAutoColumns={TABLE_GRID_MOBILE}
isClickable
>
<TableCell
color={themeCssVariables.font.color.primary}
gap={themeCssVariables.spacing[2]}
minWidth="0"
overflow="hidden"
>
<AppChip
size="md"
fallbackApplicationData={{
logo: registration.logoUrl,
<StyledNameTableCell minWidth="0" overflow="hidden">
<ApplicationDisplay
application={{
name: registration.name,
logo: registration.logoUrl,
}}
/>
</TableCell>
</StyledNameTableCell>
<TableCell overflow="hidden" align="right">
{getFormattedSource(registration)}
</TableCell>
@@ -36,6 +36,10 @@ const StyledEmptyState = styled.div`
padding: ${themeCssVariables.spacing[4]} 0;
`;
const StyledSearchInputContainer = styled.div`
padding-bottom: ${themeCssVariables.spacing[2]};
`;
const RECENT_USERS_GRID_TEMPLATE_COLUMNS = '1fr 2fr 1fr 36px';
const TOP_WORKSPACES_GRID_TEMPLATE_COLUMNS = '2fr 1fr 36px';
@@ -99,13 +103,15 @@ export const SettingsAdminGeneral = () => {
: t`Last 10 users created. Click to impersonate.`
}
/>
<SettingsTextInput
instanceId="admin-panel-user-search"
value={userSearchTerm}
onChange={setUserSearchTerm}
placeholder={t`Search by name, email, or user ID...`}
fullWidth
/>
<StyledSearchInputContainer>
<SettingsTextInput
instanceId="admin-panel-user-search"
value={userSearchTerm}
onChange={setUserSearchTerm}
placeholder={t`Search by name, email, or user ID...`}
fullWidth
/>
</StyledSearchInputContainer>
{isLoadingUsers ? (
<SettingsSectionSkeletonLoader />
) : recentUsers.length === 0 ? (
@@ -114,15 +120,15 @@ export const SettingsAdminGeneral = () => {
</StyledEmptyState>
) : (
<Table>
<TableRow
gridTemplateColumns={RECENT_USERS_GRID_TEMPLATE_COLUMNS}
>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Email`}</TableHeader>
<TableHeader>{t`Workspace`}</TableHeader>
<TableHeader />
</TableRow>
<TableBody>
<TableRow
gridTemplateColumns={RECENT_USERS_GRID_TEMPLATE_COLUMNS}
>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Email`}</TableHeader>
<TableHeader>{t`Workspace`}</TableHeader>
<TableHeader />
</TableRow>
{recentUsers.map((user) => (
<TableRow
key={user.id}
@@ -195,13 +201,15 @@ export const SettingsAdminGeneral = () => {
title={t`Top Workspaces`}
description={t`Top 10 workspaces by number of users`}
/>
<SettingsTextInput
instanceId="admin-panel-workspace-search"
value={workspaceSearchTerm}
onChange={setWorkspaceSearchTerm}
placeholder={t`Search by workspace name, subdomain, or ID...`}
fullWidth
/>
<StyledSearchInputContainer>
<SettingsTextInput
instanceId="admin-panel-workspace-search"
value={workspaceSearchTerm}
onChange={setWorkspaceSearchTerm}
placeholder={t`Search by workspace name, subdomain, or ID...`}
fullWidth
/>
</StyledSearchInputContainer>
{isLoadingWorkspaces ? (
<SettingsSectionSkeletonLoader />
) : topWorkspaces.length === 0 ? (
@@ -210,14 +218,14 @@ export const SettingsAdminGeneral = () => {
</StyledEmptyState>
) : (
<Table>
<TableRow
gridTemplateColumns={TOP_WORKSPACES_GRID_TEMPLATE_COLUMNS}
>
<TableHeader>{t`Workspace`}</TableHeader>
<TableHeader align="right">{t`Users`}</TableHeader>
<TableHeader />
</TableRow>
<TableBody>
<TableRow
gridTemplateColumns={TOP_WORKSPACES_GRID_TEMPLATE_COLUMNS}
>
<TableHeader>{t`Workspace`}</TableHeader>
<TableHeader align="right">{t`Users`}</TableHeader>
<TableHeader />
</TableRow>
{topWorkspaces.map((workspace) => (
<TableRow
key={workspace.id}
@@ -18,7 +18,7 @@ import { Section } from 'twenty-ui/layout';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { GetServerAdminsDocument } from '~/generated-admin/graphql';
const SERVER_ADMINS_GRID_TEMPLATE_COLUMNS = '2fr 1fr 1fr 36px';
const SERVER_ADMINS_GRID_TEMPLATE_COLUMNS = '1fr 2fr 1fr 36px';
const StyledEmptyState = styled.div`
color: ${themeCssVariables.font.color.tertiary};
@@ -49,13 +49,13 @@ export const SettingsAdminServerAdmins = () => {
<StyledEmptyState>{t`No server administrators found.`}</StyledEmptyState>
) : (
<Table>
<TableRow gridTemplateColumns={SERVER_ADMINS_GRID_TEMPLATE_COLUMNS}>
<TableHeader>{t`Administrator`}</TableHeader>
<TableHeader>{t`Admin panel`}</TableHeader>
<TableHeader>{t`Impersonation`}</TableHeader>
<TableHeader />
</TableRow>
<TableBody>
<TableRow gridTemplateColumns={SERVER_ADMINS_GRID_TEMPLATE_COLUMNS}>
<TableHeader>{t`Administrator`}</TableHeader>
<TableHeader>{t`Admin panel`}</TableHeader>
<TableHeader>{t`Impersonation`}</TableHeader>
<TableHeader />
</TableRow>
{serverAdmins.map((admin) => {
const adminLabel =
`${admin.firstName || ''} ${admin.lastName || ''}`.trim() ||
@@ -0,0 +1,23 @@
import {
TitleInput,
type TitleInputProps,
} from '@/ui/input/components/TitleInput';
import { styled } from '@linaria/react';
const StyledTitleInputContainer = styled.div`
max-width: 420px;
min-width: 0;
width: fit-content;
`;
export type SettingsEditableTitleProps = TitleInputProps;
export const SettingsEditableTitle = (props: SettingsEditableTitleProps) => (
<StyledTitleInputContainer>
<TitleInput
// oxlint-disable-next-line react/jsx-props-no-spreading
{...props}
sizeVariant={props.sizeVariant ?? 'sm'}
/>
</StyledTitleInputContainer>
);
@@ -9,11 +9,12 @@ import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledRowContainer = styled.div`
> * {
align-items: center;
box-sizing: border-box;
display: flex;
font-size: ${themeCssVariables.font.size.sm};
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.medium};
gap: ${themeCssVariables.spacing[2]};
min-height: ${themeCssVariables.spacing[6]};
height: ${themeCssVariables.spacing[10]};
padding: ${themeCssVariables.spacing[2]};
padding-left: ${themeCssVariables.spacing[3]};
}
@@ -1,8 +1,9 @@
import { SettingsEditableTitle } from '@/settings/components/SettingsEditableTitle';
import { SettingsSecondaryBar } from '@/settings/components/layout/SettingsSecondaryBar';
import { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
import { type BreadcrumbProps } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { type JSX, type ReactNode } from 'react';
import { useId, type JSX, type ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
type SettingsPageLayoutProps = {
@@ -25,25 +26,41 @@ export const SettingsPageLayout = ({
children,
tag,
titleColor,
}: SettingsPageLayoutProps) => (
<PageCardLayout
header={
<PageCardHeader
links={links}
title={title}
icon={icon}
tag={tag}
actionButton={actionButton}
centerTitle
titleColor={titleColor}
}: SettingsPageLayoutProps) => {
const titleInstanceId = useId();
const formattedTitle =
typeof title === 'string' ? (
<SettingsEditableTitle
disabled
instanceId={`settings-page-layout-title-${titleInstanceId}`}
textColor={titleColor}
value={title}
/>
}
secondaryBar={
isDefined(secondaryBar) ? (
<SettingsSecondaryBar>{secondaryBar}</SettingsSecondaryBar>
) : undefined
}
>
{children}
</PageCardLayout>
);
) : (
title
);
return (
<PageCardLayout
header={
<PageCardHeader
links={links}
title={formattedTitle}
icon={icon}
tag={tag}
actionButton={actionButton}
centerTitle
titleColor={titleColor}
/>
}
secondaryBar={
isDefined(secondaryBar) ? (
<SettingsSecondaryBar>{secondaryBar}</SettingsSecondaryBar>
) : undefined
}
>
{children}
</PageCardLayout>
);
};
@@ -1,18 +1,5 @@
import { TitleInput } from '@/ui/input/components/TitleInput';
import { styled } from '@linaria/react';
import { SettingsEditableTitle } from '@/settings/components/SettingsEditableTitle';
import { t } from '@lingui/core/macro';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledHeaderTitle = styled.div`
color: ${themeCssVariables.font.color.primary};
font-size: ${themeCssVariables.font.size.lg};
font-weight: ${themeCssVariables.font.weight.semiBold};
max-width: 420px;
width: fit-content;
& > input:disabled {
color: ${themeCssVariables.font.color.primary};
}
`;
type SettingsLogicFunctionLabelContainerProps = {
value: string;
@@ -26,15 +13,12 @@ export const SettingsLogicFunctionLabelContainer = ({
readonly = false,
}: SettingsLogicFunctionLabelContainerProps) => {
return (
<StyledHeaderTitle>
<TitleInput
instanceId="logic-function-name-input"
sizeVariant="md"
value={value}
onChange={onChange}
placeholder={t`Function name`}
disabled={readonly}
/>
</StyledHeaderTitle>
<SettingsEditableTitle
instanceId="logic-function-name-input"
value={value}
onChange={onChange}
placeholder={t`Function name`}
disabled={readonly}
/>
);
};
@@ -1,34 +1,8 @@
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SettingsEditableTitle } from '@/settings/components/SettingsEditableTitle';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
import { TitleInput } from '@/ui/input/components/TitleInput';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledHeaderTitle = styled.div`
border-radius: ${themeCssVariables.border.radius.sm};
max-width: 420px;
padding-left: ${themeCssVariables.spacing[1]};
padding-right: ${themeCssVariables.spacing[1]};
width: fit-content;
&:hover {
background: ${themeCssVariables.background.transparent.light};
}
& > div:hover {
background: transparent;
}
& > div :hover {
background: transparent;
}
& > input:disabled {
color: ${themeCssVariables.font.color.primary};
}
`;
type SettingsRoleLabelContainerProps = {
roleId: string;
@@ -54,15 +28,12 @@ export const SettingsRoleLabelContainer = ({
};
return (
<StyledHeaderTitle>
<TitleInput
instanceId="role-label-input"
disabled={!settingsDraftRole.isEditable}
sizeVariant="sm"
value={settingsDraftRole.label}
onChange={handleChange}
placeholder={t`Role name`}
/>
</StyledHeaderTitle>
<SettingsEditableTitle
instanceId="role-label-input"
disabled={!settingsDraftRole.isEditable}
value={settingsDraftRole.label}
onChange={handleChange}
placeholder={t`Role name`}
/>
);
};