Fix settings UI polish pass 2 (#21540)

## Summary

This PR groups the requested second UI polish pass across the app shell,
settings pages, data model, community, AI tools, and developer setup
surfaces.

### Shell and navigation polish
- Lets the app content card fill the available viewport and removes the
bottom-left radius on the main container.
- Uses an outside-looking container border treatment so the rounded
top-left edge matches the Figma frame without a double border.
- Restores the separator between the AI chat side panel and the record
container.
- Updates navigation drawer background/skeleton tones to use gray/3
consistently.
- Simplifies the full-screen backend error fallback so the white screen
fills the viewport.

### Settings page polish
- Moves the layout customization action from the settings navbar into a
settings card under the hero.
- Tightens layout page copy now that the customization surfaces are not
directly manageable yet.
- Aligns role detail title sizing and hover treatment with other
settings headers.
- Supports settings card brand icon colors, then restores Discord and X
brand logos on the Community page.
- Adds the Community discovery cover, moves community/social content to
the top, and reorganizes Partners and Features.
- Keeps setting card subtitle and separator behavior available for the
updated settings cards.

### Data model and object settings polish
- Adds a shared data-model table body wrapper so object, field, and
relation tables all keep the expected bottom border.
- Displays the object icon in the object settings navbar.
- Shares the new-field wizard parent object icon wrapper and keeps the
object icon at 64% opacity in wizard headers.
- Updates the new-field type selector icon treatment to match the wizard
header opacity behavior.

### Integrations, tools, and API polish
- Reworks the MCP setup config card with light syntax coloring and a
top-right copy icon button instead of the bottom copy section.
- Fixes email import icons to match the Figma asset shape.
- Ensures AI tool table icons and chevrons never fall back to black.

## Validation

- `npx nx lint:diff-with-main twenty-front`
- `npx tsc -p packages/twenty-front/tsconfig.json --noEmit`
- `git diff --check`
- Manual local visual pass on `apple.localhost:3001` for companies,
settings/community, data model, object settings, new-field wizard,
layout settings, account import, MCP setup, AI tools, and role detail
pages.

## Visual QA

### Shell and settings frame

<img width="1308" height="1397" alt="Shell and settings frame
before/after board"
src="https://raw.githubusercontent.com/twentyhq/twenty/pr-assets/ui-fixes-2/visual-qa/ui-fixes-2/shell-settings-frame.png"
/>

### Community and data model

<img width="1308" height="1806" alt="Community and data model
before/after board"
src="https://raw.githubusercontent.com/twentyhq/twenty/pr-assets/ui-fixes-2/visual-qa/ui-fixes-2/community-data-model.png"
/>

### Integrations and tools

<img width="1308" height="1397" alt="Integrations and tools before/after
board"
src="https://raw.githubusercontent.com/twentyhq/twenty/pr-assets/ui-fixes-2/visual-qa/ui-fixes-2/integrations-tools.png"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21540?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-14 06:15:49 +02:00
committed by GitHub
parent 5d0a4b8db4
commit d06e687b77
28 changed files with 497 additions and 195 deletions
@@ -1,6 +1,5 @@
import { AppErrorDisplay } from '@/error-handler/components/internal/AppErrorDisplay';
import { type AppErrorDisplayProps } from '@/error-handler/types/AppErrorDisplayProps';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
@@ -8,12 +7,10 @@ import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
type AppFullScreenErrorFallbackProps = AppErrorDisplayProps;
const StyledContainer = styled.div`
background: ${themeCssVariables.background.noisy};
background: ${themeCssVariables.background.primary};
box-sizing: border-box;
display: flex;
height: 100vh;
padding-left: ${themeCssVariables.spacing[3]};
padding-top: ${themeCssVariables.spacing[3]};
height: 100dvh;
width: 100vw;
`;
@@ -24,13 +21,11 @@ export const AppFullScreenErrorFallback = ({
}: AppFullScreenErrorFallbackProps) => {
return (
<StyledContainer>
<PageBody>
<AppErrorDisplay
error={error}
resetErrorBoundary={resetErrorBoundary}
title={title}
/>
</PageBody>
<AppErrorDisplay
error={error}
resetErrorBoundary={resetErrorBoundary}
title={title}
/>
</StyledContainer>
);
};
@@ -32,7 +32,7 @@ export const NavigationMenuItemSkeletonLoader = () => {
const { theme } = useContext(ThemeContext);
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
baseColor={theme.grayScale.gray3}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
@@ -9,43 +9,58 @@ type SettingsAccountsMessageFolderIconProps = {
};
const StyledCardMedia = styled.div`
align-items: stretch;
border: 2px solid ${themeCssVariables.border.color.medium};
align-items: center;
background-color: ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.sm};
color: ${themeCssVariables.font.color.light};
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing['0.5']};
height: ${themeCssVariables.spacing[8]};
height: 40px;
justify-content: center;
padding: ${themeCssVariables.spacing['0.5']};
width: ${themeCssVariables.spacing[6]};
padding: 2px;
width: 32px;
`;
const StyledCardMediaContent = styled.div`
align-items: stretch;
background-color: ${themeCssVariables.background.secondary};
border-radius: 2px;
box-sizing: border-box;
display: flex;
flex: 1;
flex-direction: column;
gap: 2px;
height: 100%;
justify-content: center;
min-width: 0;
padding: 2px;
`;
const StyledFolderRow = styled.div`
align-items: center;
display: flex;
flex: 1 0 0;
gap: 2px;
min-height: 0;
`;
const StyledFolderIcon = styled.div<{ isDisabled?: boolean }>`
background-color: ${({ isDisabled }) =>
isDisabled
? themeCssVariables.background.quaternary
: themeCssVariables.accent.accent4060};
? themeCssVariables.border.color.medium
: themeCssVariables.accent.accent7};
border-radius: 1px;
height: 5px;
width: 5px;
height: 100%;
width: 4.8px;
`;
const StyledFolderLabel = styled.div<{ isDisabled?: boolean }>`
background-color: ${({ isDisabled }) =>
isDisabled
? themeCssVariables.background.quaternary
: themeCssVariables.accent.accent4060};
? themeCssVariables.border.color.medium
: themeCssVariables.accent.accent7};
border-radius: 1px;
flex: 1;
height: 5px;
height: 100%;
`;
export const SettingsAccountsMessageFolderIcon = ({
@@ -57,26 +72,28 @@ export const SettingsAccountsMessageFolderIcon = ({
return (
<StyledCardMedia className={className}>
<StyledFolderRow>
<StyledFolderIcon isDisabled={isSelectedFolders} />
<StyledFolderLabel isDisabled={isSelectedFolders} />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon isDisabled={isSelectedFolders} />
<StyledFolderLabel isDisabled={isSelectedFolders} />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon />
<StyledFolderLabel />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon isDisabled={isSelectedFolders} />
<StyledFolderLabel isDisabled={isSelectedFolders} />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon />
<StyledFolderLabel />
</StyledFolderRow>
<StyledCardMediaContent>
<StyledFolderRow>
<StyledFolderIcon isDisabled={isSelectedFolders} />
<StyledFolderLabel isDisabled={isSelectedFolders} />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon isDisabled={isSelectedFolders} />
<StyledFolderLabel isDisabled={isSelectedFolders} />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon />
<StyledFolderLabel />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon isDisabled={isSelectedFolders} />
<StyledFolderLabel isDisabled={isSelectedFolders} />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon />
<StyledFolderLabel />
</StyledFolderRow>
</StyledCardMediaContent>
</StyledCardMedia>
);
};
@@ -16,6 +16,7 @@ type SettingsCardProps = {
disabled?: boolean;
soon?: boolean;
Icon: ReactNode;
iconColor?: string;
onClick?: () => void;
title: string;
className?: string;
@@ -82,8 +83,15 @@ const StyledDescription = styled.div`
padding-left: ${themeCssVariables.spacing[7]};
`;
const StyledIconContainer = styled.div`
const StyledIconContainer = styled.div<{
disabled?: boolean;
iconColor?: string;
}>`
align-items: center;
color: ${({ disabled, iconColor }) =>
disabled
? themeCssVariables.font.color.extraLight
: (iconColor ?? 'inherit')};
display: flex;
height: 24px;
justify-content: center;
@@ -95,6 +103,7 @@ export const SettingsCard = ({
soon,
disabled = soon,
Icon,
iconColor,
onClick,
title,
className,
@@ -112,7 +121,9 @@ export const SettingsCard = ({
<StyledCardContentContainer>
<CardContent>
<StyledHeader>
<StyledIconContainer>{Icon}</StyledIconContainer>
<StyledIconContainer disabled={disabled} iconColor={iconColor}>
{Icon}
</StyledIconContainer>
<StyledTitle disabled={disabled}>
{title}
{soon && <Pill label={t`Soon`} />}
@@ -7,7 +7,7 @@ import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { type ReactNode, useContext } from 'react';
import { Card } from 'twenty-ui-deprecated/layout';
import {
ThemeContext,
@@ -43,11 +43,17 @@ const StyledOverlay = styled.div`
position: absolute;
`;
const StyledFooter = styled.div`
background: ${themeCssVariables.background.secondary};
border-top: 1px solid ${themeCssVariables.border.color.medium};
`;
type SettingsDiscoveryHeroCardProps = {
lightSrc: string;
darkSrc: string;
instanceIdPrefix: string;
tabs: SettingsCustomizeVideoModalTab[];
footer?: ReactNode;
playButtonAriaLabel?: string;
};
@@ -56,6 +62,7 @@ export const SettingsDiscoveryHeroCard = ({
darkSrc,
instanceIdPrefix,
tabs,
footer,
playButtonAriaLabel,
}: SettingsDiscoveryHeroCardProps) => {
const { t } = useLingui();
@@ -64,6 +71,7 @@ export const SettingsDiscoveryHeroCard = ({
const isDiscoveryVideoEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_SETTINGS_DISCOVERY_HERO_ENABLED,
);
const shouldDisplayVideo = isDiscoveryVideoEnabled && tabs.length > 0;
const modalInstanceId = `${instanceIdPrefix}-modal`;
const tabsInstanceId = `${instanceIdPrefix}-tabs`;
@@ -75,7 +83,7 @@ export const SettingsDiscoveryHeroCard = ({
<Card rounded>
<StyledCoverContainer>
<StyledImage src={src} alt="" aria-hidden />
{isDiscoveryVideoEnabled && (
{shouldDisplayVideo && (
<StyledOverlay>
<HeroPlayButton
onClick={() => openModal(modalInstanceId)}
@@ -84,8 +92,11 @@ export const SettingsDiscoveryHeroCard = ({
</StyledOverlay>
)}
</StyledCoverContainer>
{footer !== undefined && footer !== null && (
<StyledFooter>{footer}</StyledFooter>
)}
</Card>
{isDiscoveryVideoEnabled && (
{shouldDisplayVideo && (
<SettingsCustomizeVideoModal
modalInstanceId={modalInstanceId}
tabsInstanceId={tabsInstanceId}
@@ -8,6 +8,7 @@ import { isDefined } from 'twenty-shared/utils';
type SettingsPageLayoutProps = {
links: BreadcrumbProps['links'];
title?: ReactNode;
icon?: ReactNode;
actionButton?: ReactNode;
secondaryBar?: ReactNode;
children: ReactNode;
@@ -18,6 +19,7 @@ type SettingsPageLayoutProps = {
export const SettingsPageLayout = ({
links,
title,
icon,
actionButton,
secondaryBar,
children,
@@ -29,6 +31,7 @@ export const SettingsPageLayout = ({
<PageCardHeader
links={links}
title={title}
icon={icon}
tag={tag}
actionButton={actionButton}
centerTitle
@@ -0,0 +1,6 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
export const StyledSettingsDataModelTableBodyContainer = styled.div`
border-bottom: 1px solid ${themeCssVariables.border.color.light};
`;
@@ -0,0 +1,23 @@
import {
ObjectMetadataIcon,
type ObjectMetadataIconInput,
} from '@/object-metadata/components/ObjectMetadataIcon';
import { styled } from '@linaria/react';
const StyledObjectIconContainer = styled.div`
display: flex;
flex-shrink: 0;
opacity: 0.64;
`;
type SettingsObjectNewFieldHeaderIconProps = {
objectMetadataItem: ObjectMetadataIconInput;
};
export const SettingsObjectNewFieldHeaderIcon = ({
objectMetadataItem,
}: SettingsObjectNewFieldHeaderIconProps) => (
<StyledObjectIconContainer>
<ObjectMetadataIcon objectMetadataItem={objectMetadataItem} />
</StyledObjectIconContainer>
);
@@ -63,6 +63,11 @@ const StyledSearchInputContainer = styled.div`
width: 100%;
`;
const StyledFieldTypeIconContainer = styled.span`
display: flex;
opacity: 0.64;
`;
export const SettingsObjectNewFieldSelector = ({
excludedFieldTypes = [],
objectNamePlural,
@@ -164,10 +169,12 @@ export const SettingsObjectNewFieldSelector = ({
<SettingsCard
key={key}
Icon={
<config.Icon
size={theme.icon.size.xl}
stroke={theme.icon.stroke.sm}
/>
<StyledFieldTypeIconContainer>
<config.Icon
size={theme.icon.size.xl}
stroke={theme.icon.stroke.sm}
/>
</StyledFieldTypeIconContainer>
}
title={config.label}
/>
@@ -7,11 +7,13 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
import { type TableMetadata } from '@/ui/layout/table/types/TableMetadata';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { StyledSettingsDataModelTableBodyContainer } from '@/settings/data-model/components/SettingsDataModelTableBodyContainer';
import { styled } from '@linaria/react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
@@ -182,13 +184,17 @@ export const SettingsObjectRelationsTable = ({
))}
<TableHeader></TableHeader>
</TableRow>
{filteredRelationFields.map((fieldMetadataItem) => (
<SettingsObjectRelationItemTableRow
key={fieldMetadataItem.id}
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
))}
<StyledSettingsDataModelTableBodyContainer>
<TableBody>
{filteredRelationFields.map((fieldMetadataItem) => (
<SettingsObjectRelationItemTableRow
key={fieldMetadataItem.id}
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
))}
</TableBody>
</StyledSettingsDataModelTableBodyContainer>
</Table>
</>
);
@@ -1,17 +1,44 @@
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { type ReactNode } from 'react';
import { H2Title, IconCopy } from 'twenty-ui-deprecated/display';
import { Button } from 'twenty-ui-deprecated/input';
import { LightIconButton } from 'twenty-ui-deprecated/input';
import { Card, CardContent, Section } from 'twenty-ui-deprecated/layout';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
const StyledMcpConfigContainer = styled.div`
position: relative;
`;
const StyledCopyButtonContainer = styled.div`
position: absolute;
right: 0;
top: 0;
`;
const StyledPre = styled.pre`
color: ${themeCssVariables.font.color.secondary};
font-family: monospace;
line-height: 1.5;
margin: 0;
overflow-x: auto;
white-space: pre;
`;
const StyledJsonKey = styled.span`
color: ${themeCssVariables.color.blue};
`;
const StyledJsonString = styled.span`
color: ${themeCssVariables.color.green};
`;
const StyledJsonPlaceholder = styled.span`
color: ${themeCssVariables.color.orange};
`;
const buildMcpConfig = (serverUrl: string) =>
`{
"mcpServers": {
@@ -24,6 +51,61 @@ const buildMcpConfig = (serverUrl: string) =>
}
}`;
const renderJsonString = (token: string, key: string) => {
const apiKeyPlaceholder = '<YOUR_API_KEY>';
if (!token.includes(apiKeyPlaceholder)) {
return <StyledJsonString key={key}>{token}</StyledJsonString>;
}
const [beforePlaceholder, afterPlaceholder] = token.split(apiKeyPlaceholder);
return (
<span key={key}>
<StyledJsonString>{beforePlaceholder}</StyledJsonString>
<StyledJsonPlaceholder>{apiKeyPlaceholder}</StyledJsonPlaceholder>
<StyledJsonString>{afterPlaceholder}</StyledJsonString>
</span>
);
};
const getHighlightedMcpConfig = (mcpConfig: string) => {
const jsonStringTokenRegex = /("(?:[^"\\]|\\.)*")(\s*:)?/g;
const tokens: ReactNode[] = [];
let previousMatchEndIndex = 0;
let tokenIndex = 0;
let match: RegExpExecArray | null;
while ((match = jsonStringTokenRegex.exec(mcpConfig)) !== null) {
const [fullMatch, quotedString, trailingColon] = match;
const matchIndex = match.index;
if (matchIndex > previousMatchEndIndex) {
tokens.push(mcpConfig.slice(previousMatchEndIndex, matchIndex));
}
if (trailingColon) {
tokens.push(
<StyledJsonKey key={`json-key-${tokenIndex}`}>
{quotedString}
</StyledJsonKey>,
);
tokens.push(trailingColon);
} else {
tokens.push(renderJsonString(quotedString, `json-string-${tokenIndex}`));
}
previousMatchEndIndex = matchIndex + fullMatch.length;
tokenIndex += 1;
}
if (previousMatchEndIndex < mcpConfig.length) {
tokens.push(mcpConfig.slice(previousMatchEndIndex));
}
return tokens;
};
export const SettingsMcpSetup = () => {
const { t } = useLingui();
const { copyToClipboard } = useCopyToClipboard();
@@ -36,19 +118,22 @@ export const SettingsMcpSetup = () => {
description={t`Add Twenty as a Model Context Protocol (MCP) server. Paste this config into Claude Desktop, Cursor, Cline, Continue, Zed, or any other MCP-aware client.`}
/>
<Card rounded>
<CardContent divider>
<StyledPre>{mcpConfig}</StyledPre>
</CardContent>
<CardContent>
<Button
title={t`Copy config`}
Icon={IconCopy}
size="small"
variant="secondary"
onClick={() =>
copyToClipboard(mcpConfig, t`MCP config copied to clipboard`)
}
/>
<StyledMcpConfigContainer>
<StyledPre>{getHighlightedMcpConfig(mcpConfig)}</StyledPre>
<StyledCopyButtonContainer>
<LightIconButton
Icon={IconCopy}
size="small"
accent="tertiary"
aria-label={t`Copy MCP config`}
title={t`Copy MCP config`}
onClick={() =>
copyToClipboard(mcpConfig, t`MCP config copied to clipboard`)
}
/>
</StyledCopyButtonContainer>
</StyledMcpConfigContainer>
</CardContent>
</Card>
</Section>
@@ -7,11 +7,24 @@ import { t } from '@lingui/core/macro';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
const StyledHeaderTitle = styled.div`
color: ${themeCssVariables.font.color.primary};
font-size: ${themeCssVariables.font.size.lg};
font-weight: ${themeCssVariables.font.weight.semiBold};
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};
}
@@ -45,7 +58,7 @@ export const SettingsRoleLabelContainer = ({
<TitleInput
instanceId="role-label-input"
disabled={!settingsDraftRole.isEditable}
sizeVariant="md"
sizeVariant="sm"
value={settingsDraftRole.label}
onChange={handleChange}
placeholder={t`Role name`}
@@ -37,6 +37,7 @@ const StyledSidePanelWrapper = styled.div<{
const StyledSidePanel = styled.aside`
background: ${themeCssVariables.background.primary};
border-left: 1px solid ${themeCssVariables.border.color.medium};
box-sizing: border-box;
display: flex;
flex-direction: column;
@@ -25,7 +25,7 @@ import { AnimatePresence, LayoutGroup } from 'framer-motion';
import { Outlet } from 'react-router-dom';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
const StyledLayout = styled.div`
background: ${themeCssVariables.background.tertiary};
background: ${themeCssVariables.grayScale.gray3};
display: flex;
flex-direction: column;
height: 100dvh;
@@ -19,17 +19,18 @@ const StyledRoot = styled.div`
`;
const StyledMainCardWrapper = styled.div`
box-sizing: border-box;
display: flex;
flex: 1 1 0;
min-width: 0;
padding-left: 1px;
width: 0;
`;
const StyledCard = styled.div`
background: ${themeCssVariables.background.primary};
border-left: 1px solid ${themeCssVariables.border.color.medium};
border-radius: 12px 0 0 12px;
border-right: 1px solid ${themeCssVariables.border.color.medium};
border-radius: 16px 0 0 0;
box-shadow: 0 0 0 1px ${themeCssVariables.border.color.medium};
box-sizing: border-box;
display: flex;
flex: 1;
@@ -205,7 +205,7 @@ const StyledIcon = styled.div`
const StyledIconBackgroundTile = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.tertiary};
background-color: ${themeCssVariables.grayScale.gray3};
border-radius: ${themeCssVariables.border.radius.md};
display: flex;
flex-shrink: 0;
@@ -16,7 +16,7 @@ export const NavigationDrawerSectionTitleSkeletonLoader = () => {
const { theme } = useContext(ThemeContext);
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
baseColor={theme.grayScale.gray3}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
@@ -97,6 +97,7 @@ export const SettingsAgentToolsTable = ({
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
}
link={getToolLink(tool)}
@@ -151,7 +151,7 @@ export const SettingsToolIcon = ({
<OperationIcon
size="12px"
stroke={theme.icon.stroke.md}
color={themeCssVariables.grayScale.gray10}
color={theme.font.color.tertiary}
/>
</StyledOperationOverlay>
</StyledCompositeContainer>
@@ -175,5 +175,11 @@ export const SettingsToolIcon = ({
);
}
return <MainIcon size={16} />;
return (
<MainIcon
size={16}
stroke={theme.icon.stroke.md}
color={theme.font.color.tertiary}
/>
);
};
@@ -1,4 +1,5 @@
import { SettingsCard } from '@/settings/components/SettingsCard';
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsLabContent } from '@/settings/lab/components/SettingsLabContent';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
@@ -7,23 +8,50 @@ import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import {
H2Title,
IconBrandX,
IconBriefcase,
IconTransform,
type IconComponent,
useIcons,
} from 'twenty-ui-deprecated/display';
import { Section } from 'twenty-ui-deprecated/layout';
import { ThemeContext } from 'twenty-ui-deprecated/theme-constants';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import {
MOBILE_VIEWPORT,
ThemeContext,
themeCssVariables,
} from 'twenty-ui-deprecated/theme-constants';
import coverDark from '~/pages/settings/community/assets/cover-dark.png';
import coverLight from '~/pages/settings/community/assets/cover-light.png';
const SETTINGS_COMMUNITY_HERO_INSTANCE_ID_PREFIX = 'settings-community-hero';
const StyledCardLink = styled.a`
display: block;
min-width: 0;
text-decoration: none;
`;
const StyledCardsGrid = styled.div`
display: grid;
gap: ${themeCssVariables.spacing[2]};
grid-template-columns: repeat(2, minmax(0, 1fr));
@media (max-width: ${MOBILE_VIEWPORT}px) {
grid-template-columns: 1fr;
}
`;
const StyledFeaturesContent = styled.div`
display: grid;
gap: ${themeCssVariables.spacing[4]};
`;
type SettingsCommunityLink = {
title: string;
description: string;
href: string;
Icon: IconComponent;
iconColor: string;
cardTitle: string;
};
@@ -32,63 +60,115 @@ export const SettingsCommunity = () => {
const { getIcon } = useIcons();
const IconBrandDiscord = getIcon('IconBrandDiscord');
const communityLinks: SettingsCommunityLink[] = [
const socialLinks: SettingsCommunityLink[] = [
{
title: t`Partners`,
description: t`Hire a partner to help you implement and customize Twenty.`,
href: 'https://twenty.com/partners/list',
Icon: IconBriefcase,
cardTitle: t`Browse partners`,
},
{
title: t`Discord`,
description: t`Join our community to get help and share feedback.`,
href: 'https://discord.com/invite/cx5n4Jzs57',
Icon: IconBrandDiscord,
iconColor: themeCssVariables.color.blue9,
cardTitle: t`Join our Discord`,
},
{
title: t`Releases`,
description: t`Check out our latest releases`,
href: 'https://twenty.com/releases',
Icon: IconTransform,
cardTitle: t`Read changelog`,
href: 'https://x.com/twentycrm',
Icon: IconBrandX,
iconColor: themeCssVariables.font.color.primary,
cardTitle: t`Follow us on X`,
},
];
return (
<SettingsPageLayout
title={t`Community`}
links={[{ children: t`Other` }, { children: t`Community` }]}
links={[
{
children: t`Other`,
href: getSettingsPath(SettingsPath.Community),
},
{ children: t`Community` },
]}
>
<SettingsPageContainer>
{communityLinks.map(({ title, description, href, Icon, cardTitle }) => (
<Section key={href}>
<H2Title title={title} description={description} />
<Section>
<SettingsDiscoveryHeroCard
lightSrc={coverLight}
darkSrc={coverDark}
instanceIdPrefix={SETTINGS_COMMUNITY_HERO_INSTANCE_ID_PREFIX}
tabs={[]}
/>
</Section>
<Section>
<H2Title
title={t`Join the community`}
description={t`Stay up to date with product news and community updates.`}
/>
<StyledCardsGrid>
{socialLinks.map(({ href, Icon, iconColor, cardTitle }) => (
<StyledCardLink
key={href}
href={href}
target="_blank"
rel="noopener noreferrer"
>
<SettingsCard
Icon={
<Icon
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
}
iconColor={iconColor}
title={cardTitle}
/>
</StyledCardLink>
))}
</StyledCardsGrid>
</Section>
<Section>
<H2Title
title={t`Partners`}
description={t`Hire a partner to help you implement and customize Twenty.`}
/>
<StyledCardLink
href="https://twenty.com/partners/list"
target="_blank"
rel="noopener noreferrer"
>
<SettingsCard
Icon={
<IconBriefcase
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
}
title={t`Browse partners`}
/>
</StyledCardLink>
</Section>
<Section>
<H2Title
title={t`Features`}
description={t`Try our upcoming features. Note they are still in beta. Please bear with us and report any issues you find.`}
/>
<StyledFeaturesContent>
<SettingsLabContent />
<StyledCardLink
href={href}
href="https://twenty.com/releases"
target="_blank"
rel="noopener noreferrer"
>
<SettingsCard
Icon={
<Icon
<IconTransform
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
}
title={cardTitle}
title={t`Read changelog`}
/>
</StyledCardLink>
</Section>
))}
<Section>
<H2Title
title={t`Early access`}
description={t`Try our upcoming features. Note they are still in beta. Please bear with us and report any issues you find.`}
/>
<SettingsLabContent />
</StyledFeaturesContent>
</Section>
</SettingsPageContainer>
</SettingsPageLayout>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ObjectMetadataIcon } from '@/object-metadata/components/ObjectMetadataIcon';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { ObjectFields } from '@/settings/data-model/object-details/components/tabs/ObjectFields';
@@ -135,6 +136,7 @@ export const SettingsObjectDetailPage = () => {
return (
<SettingsPageLayout
title={objectMetadataItem.labelPlural}
icon={<ObjectMetadataIcon objectMetadataItem={objectMetadataItem} />}
links={[
{
children: t`Workspace`,
@@ -5,12 +5,14 @@ import {
OBJECT_FIELD_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
SettingsObjectFieldItemTableRow,
} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
import { StyledSettingsDataModelTableBodyContainer } from '@/settings/data-model/components/SettingsDataModelTableBodyContainer';
import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
import { type TableMetadata } from '@/ui/layout/table/types/TableMetadata';
@@ -204,20 +206,24 @@ export const SettingsObjectFieldTable = ({
))}
<TableHeader></TableHeader>
</TableRow>
{filteredItems.map((objectSettingsDetailItem) => {
const status = objectSettingsDetailItem.fieldMetadataItem.isActive
? 'active'
: 'disabled';
<StyledSettingsDataModelTableBodyContainer>
<TableBody>
{filteredItems.map((objectSettingsDetailItem) => {
const status = objectSettingsDetailItem.fieldMetadataItem.isActive
? 'active'
: 'disabled';
return (
<SettingsObjectFieldItemTableRow
key={objectSettingsDetailItem.fieldMetadataItem.id}
settingsObjectDetailTableItem={objectSettingsDetailItem}
status={status}
mode={mode}
/>
);
})}
return (
<SettingsObjectFieldItemTableRow
key={objectSettingsDetailItem.fieldMetadataItem.id}
settingsObjectDetailTableItem={objectSettingsDetailItem}
status={status}
mode={mode}
/>
);
})}
</TableBody>
</StyledSettingsDataModelTableBodyContainer>
</Table>
</>
);
@@ -5,8 +5,10 @@ import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdat
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
import { StyledSettingsDataModelTableBodyContainer } from '@/settings/data-model/components/SettingsDataModelTableBodyContainer';
import { SettingsObjectMetadataItemTableRow } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import {
SETTINGS_OBJECT_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
SETTINGS_OBJECT_TABLE_ROW_MOBILE_MIN_WIDTH,
@@ -236,56 +238,62 @@ export const SettingsObjectTable = ({
)}
<TableHeader></TableHeader>
</TableRow>
{filteredObjectSettingsItems.map((objectSettingsItem) => {
const isActive = objectSettingsItem.objectMetadataItem.isActive;
<StyledSettingsDataModelTableBodyContainer>
<TableBody>
{filteredObjectSettingsItems.map((objectSettingsItem) => {
const isActive =
objectSettingsItem.objectMetadataItem.isActive;
return (
<SettingsObjectMetadataItemTableRow
key={objectSettingsItem.objectMetadataItem.namePlural}
objectMetadataItem={objectSettingsItem.objectMetadataItem}
totalObjectCount={objectSettingsItem.totalObjectCount}
action={
isActive ? (
<StyledIconChevronRightContainer>
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</StyledIconChevronRightContainer>
) : isDDLLocked ? null : (
<SettingsObjectInactiveMenuDropDown
isCustomObject={
objectSettingsItem.objectMetadataItem.isCustom
}
objectMetadataItemNamePlural={
objectSettingsItem.objectMetadataItem.namePlural
}
onActivate={() =>
updateOneObjectMetadataItem({
idToUpdate:
objectSettingsItem.objectMetadataItem.id,
updatePayload: { isActive: true },
})
}
onDelete={() =>
deleteOneObjectMetadataItem(
objectSettingsItem.objectMetadataItem.id,
)
}
/>
)
}
link={
isActive
? getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural:
objectSettingsItem.objectMetadataItem.namePlural,
})
: undefined
}
/>
);
})}
return (
<SettingsObjectMetadataItemTableRow
key={objectSettingsItem.objectMetadataItem.namePlural}
objectMetadataItem={objectSettingsItem.objectMetadataItem}
totalObjectCount={objectSettingsItem.totalObjectCount}
action={
isActive ? (
<StyledIconChevronRightContainer>
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</StyledIconChevronRightContainer>
) : isDDLLocked ? null : (
<SettingsObjectInactiveMenuDropDown
isCustomObject={
objectSettingsItem.objectMetadataItem.isCustom
}
objectMetadataItemNamePlural={
objectSettingsItem.objectMetadataItem.namePlural
}
onActivate={() =>
updateOneObjectMetadataItem({
idToUpdate:
objectSettingsItem.objectMetadataItem.id,
updatePayload: { isActive: true },
})
}
onDelete={() =>
deleteOneObjectMetadataItem(
objectSettingsItem.objectMetadataItem.id,
)
}
/>
)
}
link={
isActive
? getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural:
objectSettingsItem.objectMetadataItem
.namePlural,
})
: undefined
}
/>
);
})}
</TableBody>
</StyledSettingsDataModelTableBodyContainer>
</Table>
</StyledScrollableContent>
</StyledScrollWrapper>
@@ -4,6 +4,7 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilte
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsWizardStepBar } from '@/settings/components/layout/SettingsWizardStepBar';
import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength';
import { SettingsObjectNewFieldHeaderIcon } from '@/settings/data-model/fields/components/SettingsObjectNewFieldHeaderIcon';
import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm';
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
@@ -183,6 +184,11 @@ export const SettingsObjectNewFieldConfigure = () => {
>
<SettingsPageLayout
title={activeObjectMetadataItem.labelPlural}
icon={
<SettingsObjectNewFieldHeaderIcon
objectMetadataItem={activeObjectMetadataItem}
/>
}
titleColor={themeCssVariables.font.color.tertiary}
links={[
{
@@ -3,6 +3,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsWizardStepBar } from '@/settings/components/layout/SettingsWizardStepBar';
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
import { SettingsObjectNewFieldHeaderIcon } from '@/settings/data-model/fields/components/SettingsObjectNewFieldHeaderIcon';
import { SettingsObjectNewFieldSelector } from '@/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector';
import { type FieldType } from '@/settings/data-model/types/FieldType';
import { type SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
@@ -67,6 +68,11 @@ export const SettingsObjectNewFieldSelect = () => {
>
<SettingsPageLayout
title={activeObjectMetadataItem.labelPlural}
icon={
<SettingsObjectNewFieldHeaderIcon
objectMetadataItem={activeObjectMetadataItem}
/>
}
titleColor={themeCssVariables.font.color.tertiary}
links={[
{
@@ -1,5 +1,6 @@
import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsLayoutItemsStats } from '@/settings/layout/components/SettingsLayoutItemsStats';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
@@ -70,16 +71,6 @@ export const SettingsLayout = () => {
return (
<SettingsPageLayout
title={t`Layout`}
actionButton={
<Button
title={t`Customize`}
variant="primary"
accent="blue"
size="small"
Icon={IconPencil}
onClick={handleCustomize}
/>
}
links={[
{
children: t`Workspace`,
@@ -96,6 +87,23 @@ export const SettingsLayout = () => {
instanceIdPrefix={SETTINGS_LAYOUT_HERO_INSTANCE_ID_PREFIX}
tabs={heroTabs}
playButtonAriaLabel={t`Watch customization demo`}
footer={
<SettingsOptionCardContentButton
Icon={IconLayoutDashboard}
title={t`Customize layout`}
description={t`Customize how your workspace looks.`}
Button={
<Button
title={t`Customize`}
variant="primary"
accent="blue"
size="small"
Icon={IconPencil}
onClick={handleCustomize}
/>
}
/>
}
/>
</Section>
<Section>