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:
committed by
GitHub
parent
5d0a4b8db4
commit
d06e687b77
+7
-12
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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}
|
||||
>
|
||||
|
||||
+52
-35
@@ -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`} />}
|
||||
|
||||
+14
-3
@@ -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
|
||||
|
||||
+6
@@ -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};
|
||||
`;
|
||||
+23
@@ -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>
|
||||
);
|
||||
+11
-4
@@ -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}
|
||||
/>
|
||||
|
||||
+13
-7
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
+98
-13
@@ -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>
|
||||
|
||||
+17
-4
@@ -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;
|
||||
|
||||
+1
-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;
|
||||
|
||||
+1
-1
@@ -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}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user