Update settings application illustrations and app metadata previews (#19964)

## Summary
- Refresh the settings application visuals with new light/dark PNG
covers for the data model card
- Replace the custom and standard application carousel assets with the
new provided illustrations
- Align app chips, type tags, and application detail previews with the
updated icon and description treatment
- Keep the data model cover container and overlay button behavior intact
while swapping the underlying imagery

## Testing
- Not run (not requested)
- Existing frontend typecheck and formatting checks were exercised
during implementation

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas des Francs
2026-04-22 15:07:41 +02:00
committed by GitHub
parent 0c929e7903
commit 68d509e98d
31 changed files with 334 additions and 82 deletions
@@ -14,7 +14,7 @@ const StyledContainer = styled.div`
display: inline-flex;
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[2]};
gap: ${themeCssVariables.spacing[1]};
max-width: 100%;
min-width: 0;
overflow: hidden;
@@ -33,7 +33,7 @@ export const AppChip = ({ applicationId, className }: AppChipProps) => {
<StyledContainer className={className}>
<Avatar
type="app"
size="md"
size="sm"
placeholder={applicationChipData.name}
placeholderColorSeed={applicationChipData.seed}
color={applicationChipData.colors?.color}
@@ -1,9 +1,10 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isTwentyStandardApplication } from '@/applications/utils/isTwentyStandardApplication';
import { isWorkspaceCustomApplication } from '@/applications/utils/isWorkspaceCustomApplication';
import { useContext } from 'react';
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { isDefined } from 'twenty-shared/utils';
export type ApplicationAvatarColors = {
color: string;
@@ -17,18 +18,6 @@ type UseApplicationAvatarColorsArgs = {
universalIdentifier?: string | null;
};
const STANDARD_APPLICATION_AVATAR_COLORS: ApplicationAvatarColors = {
// The standard application avatar follows the Figma `Colors/Blue` palette,
// which is Radix's pure blue and not Twenty's `theme.color.blue*` tokens
// (those map to the indigo palette).
// oxlint-disable-next-line twenty/no-hardcoded-colors
backgroundColor: '#CEE7FE',
// oxlint-disable-next-line twenty/no-hardcoded-colors
borderColor: '#B7D9F8',
// oxlint-disable-next-line twenty/no-hardcoded-colors
color: '#113264',
};
export const useApplicationAvatarColors = (
application: UseApplicationAvatarColorsArgs | null | undefined,
): ApplicationAvatarColors | undefined => {
@@ -39,24 +28,19 @@ export const useApplicationAvatarColors = (
return undefined;
}
const isStandard =
isDefined(application.universalIdentifier) &&
application.universalIdentifier ===
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER;
const isCustom =
isDefined(currentWorkspace?.workspaceCustomApplication?.id) &&
currentWorkspace.workspaceCustomApplication.id === application.id;
if (isStandard) {
return STANDARD_APPLICATION_AVATAR_COLORS;
if (isTwentyStandardApplication(application)) {
return {
backgroundColor: theme.color.blue3,
borderColor: theme.color.blue4,
color: theme.color.blue9,
};
}
if (isCustom) {
if (isWorkspaceCustomApplication(application, currentWorkspace)) {
return {
backgroundColor: theme.color.orange5,
borderColor: theme.color.orange6,
color: theme.color.orange12,
backgroundColor: theme.color.orange3,
borderColor: theme.color.orange4,
color: theme.color.orange9,
};
}
@@ -3,11 +3,12 @@ import {
useApplicationAvatarColors,
type ApplicationAvatarColors,
} from '@/applications/hooks/useApplicationAvatarColors';
import { isTwentyStandardApplication } from '@/applications/utils/isTwentyStandardApplication';
import { isWorkspaceCustomApplication } from '@/applications/utils/isWorkspaceCustomApplication';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
type UseApplicationChipDataArgs = {
@@ -48,20 +49,11 @@ export const useApplicationChipData = ({
const isCurrent =
isDefined(currentApplicationId) && currentApplicationId === applicationId;
const isStandard =
isDefined(application.universalIdentifier) &&
application.universalIdentifier ===
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER;
const isCustom =
isDefined(currentWorkspace?.workspaceCustomApplication?.id) &&
currentWorkspace.workspaceCustomApplication.id === application.id;
const displayName = isCurrent
? t`This app`
: isStandard
: isTwentyStandardApplication(application)
? t`Standard`
: isCustom
: isWorkspaceCustomApplication(application, currentWorkspace)
? t`Custom`
: application.name;
@@ -0,0 +1,28 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isTwentyStandardApplication } from '@/applications/utils/isTwentyStandardApplication';
import { isWorkspaceCustomApplication } from '@/applications/utils/isWorkspaceCustomApplication';
import { getCustomApplicationDescription } from '~/pages/settings/applications/utils/getCustomApplicationDescription';
import { getStandardApplicationDescription } from '~/pages/settings/applications/utils/getStandardApplicationDescription';
type ApplicationLike = {
id?: string | null;
universalIdentifier?: string | null;
description?: string | null;
};
export const useResolvedApplicationDescription = (
application: ApplicationLike | null | undefined,
): string => {
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
if (isTwentyStandardApplication(application)) {
return getStandardApplicationDescription();
}
if (isWorkspaceCustomApplication(application, currentWorkspace)) {
return getCustomApplicationDescription();
}
return application?.description ?? '';
};
@@ -0,0 +1,13 @@
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
type ApplicationLike = {
universalIdentifier?: string | null;
};
export const isTwentyStandardApplication = (
application: ApplicationLike | null | undefined,
): boolean =>
isDefined(application?.universalIdentifier) &&
application.universalIdentifier ===
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER;
@@ -0,0 +1,17 @@
import { isDefined } from 'twenty-shared/utils';
type ApplicationLike = {
id?: string | null;
};
type WorkspaceLike = {
workspaceCustomApplication?: { id?: string | null } | null;
};
export const isWorkspaceCustomApplication = (
application: ApplicationLike | null | undefined,
currentWorkspace: WorkspaceLike | null | undefined,
): boolean =>
isDefined(application?.id) &&
isDefined(currentWorkspace?.workspaceCustomApplication?.id) &&
currentWorkspace.workspaceCustomApplication.id === application.id;
@@ -7,28 +7,38 @@ type SettingsApplicationScreenshotGalleryProps = {
displayName: string;
};
const StyledGalleryContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[2]};
margin-bottom: ${themeCssVariables.spacing[6]};
min-width: 0;
width: 100%;
`;
const StyledScreenshotsContainer = styled.div`
align-items: center;
aspect-ratio: 8 / 5;
background-color: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
display: flex;
height: 300px;
justify-content: center;
margin-bottom: ${themeCssVariables.spacing[2]};
overflow: hidden;
`;
const StyledScreenshotImage = styled.img`
height: 100%;
object-fit: contain;
object-fit: cover;
object-position: center;
width: 100%;
`;
const StyledScreenshotThumbnails = styled.div`
display: flex;
gap: ${themeCssVariables.spacing[2]};
margin-bottom: ${themeCssVariables.spacing[6]};
min-width: 0;
overflow-x: auto;
`;
const StyledThumbnail = styled.div<{ isSelected?: boolean }>`
@@ -42,8 +52,8 @@ const StyledThumbnail = styled.div<{ isSelected?: boolean }>`
border-radius: ${themeCssVariables.border.radius.sm};
cursor: pointer;
display: flex;
flex: 1;
height: 60px;
flex: 0 0 96px;
height: 56px;
justify-content: center;
overflow: hidden;
@@ -54,7 +64,8 @@ const StyledThumbnail = styled.div<{ isSelected?: boolean }>`
const StyledThumbnailImage = styled.img`
height: 100%;
object-fit: contain;
object-fit: cover;
object-position: center;
width: 100%;
`;
@@ -71,7 +82,7 @@ export const SettingsApplicationScreenshotGallery = ({
const safeIndex = Math.min(selectedScreenshotIndex, screenshots.length - 1);
return (
<>
<StyledGalleryContainer>
<StyledScreenshotsContainer>
<StyledScreenshotImage
src={screenshots[safeIndex]}
@@ -92,6 +103,6 @@ export const SettingsApplicationScreenshotGallery = ({
</StyledThumbnail>
))}
</StyledScreenshotThumbnails>
</>
</StyledGalleryContainer>
);
};
@@ -28,7 +28,7 @@ export const SettingsItemTypeTag = ({
leftComponent={
<Avatar
type="app"
size="md"
size="sm"
placeholder="Remote"
placeholderColorSeed="Remote"
/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 31 KiB

@@ -5,7 +5,10 @@ import React from 'react';
export const SETTINGS_OBJECT_TABLE_COLUMN_WIDTH = '98.7px';
export const SETTINGS_OBJECT_TABLE_ROW_GRID_TEMPLATE_COLUMNS = `180px ${SETTINGS_OBJECT_TABLE_COLUMN_WIDTH} ${SETTINGS_OBJECT_TABLE_COLUMN_WIDTH} ${SETTINGS_OBJECT_TABLE_COLUMN_WIDTH} 36px`;
const SETTINGS_OBJECT_TABLE_APP_COLUMN_WIDTH = '140px';
const SETTINGS_OBJECT_TABLE_FIELDS_COLUMN_WIDTH = '72px';
export const SETTINGS_OBJECT_TABLE_ROW_GRID_TEMPLATE_COLUMNS = `180px ${SETTINGS_OBJECT_TABLE_APP_COLUMN_WIDTH} ${SETTINGS_OBJECT_TABLE_FIELDS_COLUMN_WIDTH} ${SETTINGS_OBJECT_TABLE_COLUMN_WIDTH} 36px`;
export const SETTINGS_OBJECT_TABLE_ROW_MOBILE_MIN_WIDTH = '520px';
@@ -12,43 +12,60 @@ import LightCoverImage from '@/settings/data-model/assets/cover-light.png';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledCoverImageContainer = styled.div`
align-items: center;
background-size: cover;
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
box-sizing: border-box;
display: flex;
justify-content: center;
margin-bottom: ${themeCssVariables.spacing[8]};
min-height: 153px;
overflow: hidden;
position: relative;
`;
const StyledButtonContainer = styled.div`
padding-top: ${themeCssVariables.spacing[5]};
const StyledCoverImage = styled.img`
display: block;
height: 100%;
inset: 0;
object-fit: cover;
object-position: center;
position: absolute;
width: 100%;
`;
const StyledButtonOverlay = styled.div`
align-items: center;
display: flex;
inset: 0;
justify-content: center;
pointer-events: none;
position: absolute;
& > * {
pointer-events: auto;
}
`;
export const SettingsObjectCoverImage = () => {
const { colorScheme } = useContext(ThemeContext);
const { t } = useLingui();
return (
<StyledCoverImageContainer
style={{
backgroundImage:
<StyledCoverImageContainer>
<StyledCoverImage
src={
colorScheme === 'light'
? `url('${LightCoverImage.toString()}')`
: `url('${DarkCoverImage.toString()}')`,
}}
>
<StyledButtonContainer>
? LightCoverImage.toString()
: DarkCoverImage.toString()
}
alt=""
aria-hidden
/>
<StyledButtonOverlay>
<FloatingButton
Icon={IconEye}
title={t`Visualize`}
size="small"
to={getSettingsPath(SettingsPath.ObjectOverview)}
/>
</StyledButtonContainer>
</StyledButtonOverlay>
</StyledCoverImageContainer>
);
};
@@ -1,4 +1,8 @@
import { CurrentApplicationContext } from '@/applications/contexts/CurrentApplicationContext';
import { useResolvedApplicationDescription } from '@/applications/hooks/useResolvedApplicationDescription';
import { isTwentyStandardApplication } from '@/applications/utils/isTwentyStandardApplication';
import { isWorkspaceCustomApplication } from '@/applications/utils/isWorkspaceCustomApplication';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@@ -38,6 +42,8 @@ import {
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { SettingsApplicationDetailSkeletonLoader } from '~/pages/settings/applications/components/SettingsApplicationDetailSkeletonLoader';
import { SettingsApplicationDetailTitle } from '~/pages/settings/applications/components/SettingsApplicationDetailTitle';
import { CUSTOM_APPLICATION_ILLUSTRATIONS } from '~/pages/settings/applications/constants/CustomApplicationIllustrations';
import { STANDARD_APPLICATION_ILLUSTRATIONS } from '~/pages/settings/applications/constants/StandardApplicationIllustrations';
import { SettingsApplicationCustomTab } from '~/pages/settings/applications/tabs/SettingsApplicationCustomTab';
import { SettingsApplicationDetailAboutTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailAboutTab';
import { SettingsApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailContentTab';
@@ -70,13 +76,30 @@ export const SettingsApplicationDetails = () => {
const detail = detailData?.findMarketplaceAppDetail;
const manifest = detail?.manifest as Manifest | undefined;
const app = manifest?.application;
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const isStandardApplication = isTwentyStandardApplication(application);
const isCustomApplication = isWorkspaceCustomApplication(
application,
currentWorkspace,
);
const resolvedDescription = useResolvedApplicationDescription(application);
const displayName =
app?.displayName ?? application?.name ?? t`Application details`;
const description = app?.description ?? application?.description ?? undefined;
const description = app?.description ?? resolvedDescription;
const logoUrl =
app?.logoUrl ?? application?.applicationRegistration?.logoUrl ?? undefined;
const getScreenshots = () => {
if (app?.screenshots?.length) return app.screenshots;
if (isStandardApplication) return STANDARD_APPLICATION_ILLUSTRATIONS;
if (isCustomApplication) return CUSTOM_APPLICATION_ILLUSTRATIONS;
return undefined;
};
const screenshots = getScreenshots();
const settingsCustomTabFrontComponentId =
application?.settingsCustomTabFrontComponentId;
@@ -228,7 +251,7 @@ export const SettingsApplicationDetails = () => {
displayName={displayName}
description={description}
aboutDescription={app?.aboutDescription}
screenshots={app?.screenshots}
screenshots={screenshots}
author={app?.author}
category={app?.category}
contentEntries={contentEntries}
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

@@ -4,6 +4,7 @@ import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { Avatar, IconEyeOff } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { getApplicationDescriptionSummary } from '~/pages/settings/applications/utils/getApplicationDescriptionSummary';
type SettingsApplicationDetailTitleProps = {
displayName: string;
@@ -37,7 +38,7 @@ const StyledHeaderLeft = styled.div`
const StyledHeaderTop = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
gap: ${themeCssVariables.spacing[1]};
`;
const StyledAppName = styled.div`
@@ -82,6 +83,7 @@ export const SettingsApplicationDetailTitle = ({
name: applicationName,
universalIdentifier,
});
const descriptionSummary = getApplicationDescriptionSummary(description);
return (
<StyledTitleContainer>
@@ -106,8 +108,8 @@ export const SettingsApplicationDetailTitle = ({
/>
<StyledAppName>{displayName}</StyledAppName>
</StyledHeaderTop>
{description && (
<StyledAppDescription>{description}</StyledAppDescription>
{descriptionSummary && (
<StyledAppDescription>{descriptionSummary}</StyledAppDescription>
)}
</StyledHeaderLeft>
</StyledHeader>
@@ -1,6 +1,7 @@
import { type ReactNode } from 'react';
import { useApplicationAvatarColors } from '@/applications/hooks/useApplicationAvatarColors';
import { useResolvedApplicationDescription } from '@/applications/hooks/useResolvedApplicationDescription';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { t } from '@lingui/core/macro';
@@ -8,6 +9,7 @@ import { Tag } from 'twenty-ui/components';
import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type ApplicationWithoutRelation } from '~/pages/settings/applications/types/applicationWithoutRelation';
import { getApplicationDescriptionSummary } from '~/pages/settings/applications/utils/getApplicationDescriptionSummary';
export type SettingsApplicationTableRowProps = {
action: ReactNode;
@@ -26,6 +28,9 @@ export const SettingsApplicationTableRow = ({
link,
}: SettingsApplicationTableRowProps) => {
const colors = useApplicationAvatarColors(application);
const resolvedDescription = useResolvedApplicationDescription(application);
const descriptionSummary =
getApplicationDescriptionSummary(resolvedDescription);
return (
<TableRow
@@ -53,7 +58,7 @@ export const SettingsApplicationTableRow = ({
<OverflowingTextWithTooltip text={application.name} />
</TableCell>
<TableCell gap={themeCssVariables.spacing[2]} minWidth="0">
<OverflowingTextWithTooltip text={application.description} />
<OverflowingTextWithTooltip text={descriptionSummary} />
{hasUpdate === true && (
<Tag color="blue" text={t`Update`} weight="medium" />
)}
@@ -12,6 +12,7 @@ import { Avatar } from 'twenty-ui/display';
import { Card } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type MarketplaceApp } from '~/generated-metadata/graphql';
import { getApplicationDescriptionSummary } from '~/pages/settings/applications/utils/getApplicationDescriptionSummary';
type SettingsAvailableApplicationCardProps = {
application: MarketplaceApp;
@@ -45,6 +46,10 @@ const StyledDescription = styled.div`
export const SettingsAvailableApplicationCard = ({
application,
}: SettingsAvailableApplicationCardProps) => {
const descriptionSummary = getApplicationDescriptionSummary(
application.description,
);
return (
<StyledLinkContainer>
<Link
@@ -65,7 +70,7 @@ export const SettingsAvailableApplicationCard = ({
<StyledSettingsCardTitle>
{application.name}
</StyledSettingsCardTitle>
<StyledDescription>{application.description}</StyledDescription>
<StyledDescription>{descriptionSummary}</StyledDescription>
<StyledSettingsCardThirdLine>
{t`by {author}`} {application.author}
</StyledSettingsCardThirdLine>
@@ -0,0 +1,7 @@
import CustomFieldsAndObjectsIllustration from '~/pages/settings/applications/assets/custom-illustrations/custom-fields-and-objects.webp';
import CustomLayoutsIllustration from '~/pages/settings/applications/assets/custom-illustrations/custom-layouts.webp';
export const CUSTOM_APPLICATION_ILLUSTRATIONS = [
CustomFieldsAndObjectsIllustration,
CustomLayoutsIllustration,
];
@@ -0,0 +1,11 @@
import DashboardsIllustration from '~/pages/settings/applications/assets/standard-illustrations/dashboards.webp';
import KanbansIllustration from '~/pages/settings/applications/assets/standard-illustrations/kanbans.webp';
import PeopleAndCompaniesIllustration from '~/pages/settings/applications/assets/standard-illustrations/people-and-companies.webp';
import WorkflowsIllustration from '~/pages/settings/applications/assets/standard-illustrations/workflows.webp';
export const STANDARD_APPLICATION_ILLUSTRATIONS = [
WorkflowsIllustration,
KanbansIllustration,
DashboardsIllustration,
PeopleAndCompaniesIllustration,
];
@@ -57,6 +57,48 @@ const StyledMainContent = styled.div`
overflow: hidden;
`;
const StyledMarkdownContent = styled.div`
.markdown-section {
margin: 0;
}
.markdown-section h4 {
font-size: ${themeCssVariables.font.size.lg};
font-weight: ${themeCssVariables.font.weight.semiBold};
line-height: 1.35;
margin-bottom: ${themeCssVariables.spacing[2]};
margin-top: ${themeCssVariables.spacing[5]};
}
.markdown-section ul {
margin-bottom: ${themeCssVariables.spacing[3]};
margin-top: ${themeCssVariables.spacing[2]};
padding-left: ${themeCssVariables.spacing[4]};
}
.markdown-section li {
margin-bottom: ${themeCssVariables.spacing[1]} !important;
padding-bottom: 0 !important;
padding-top: 0 !important;
}
.markdown-section .markdown-code-outer-container {
margin: ${themeCssVariables.spacing[3]} 0 ${themeCssVariables.spacing[4]};
}
.markdown-section .markdown-block-code {
padding: ${themeCssVariables.spacing[3]} ${themeCssVariables.spacing[4]};
}
.markdown-section .markdown-block-code code {
color: ${themeCssVariables.font.color.primary};
display: block;
font-family: ${themeCssVariables.code.font.family}, monospace;
font-size: ${themeCssVariables.font.size.sm};
line-height: 1.6;
}
`;
export const SettingsApplicationDetailAboutTab = ({
displayName,
description,
@@ -161,7 +203,9 @@ export const SettingsApplicationDetailAboutTab = ({
<StyledContentContainer>
<StyledMainContent>
<Section>
<LazyMarkdownRenderer text={markdownText} />
<StyledMarkdownContent>
<LazyMarkdownRenderer text={markdownText} />
</StyledMarkdownContent>
</Section>
</StyledMainContent>
@@ -15,6 +15,7 @@ import { useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useContext, useMemo, useState } from 'react';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import {
@@ -29,7 +30,6 @@ import {
} from 'twenty-ui/display';
import { Button, SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import {
type ApplicationRegistrationFragmentFragment,
@@ -42,6 +42,7 @@ import {
APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
SettingsApplicationTableRow,
} from '~/pages/settings/applications/components/SettingsApplicationTableRow';
import { getApplicationDescriptionSummary } from '~/pages/settings/applications/utils/getApplicationDescriptionSummary';
const StyledButtonContainer = styled.div`
display: flex;
@@ -276,7 +277,9 @@ export const SettingsApplicationsDeveloperTab = () => {
whiteSpace="nowrap"
>
<OverflowingTextWithTooltip
text={application.description}
text={getApplicationDescriptionSummary(
application.description,
)}
/>
</TableCell>
<StyledActionTableCell>
@@ -0,0 +1,28 @@
const stripMarkdown = (value: string) =>
value
.replace(/```[\s\S]*?```/g, ' ')
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
.replace(/^>\s?/gm, '')
.replace(/^\s{0,3}(?:[-*+]\s+|\d+\.\s+)/gm, '')
.replace(/[*_~`#]/g, '')
.replace(/\s+/g, ' ')
.trim();
export const getApplicationDescriptionSummary = (
description?: string | null,
): string => {
if (!description) {
return '';
}
for (const block of description.split(/\n\s*\n/)) {
const summary = stripMarkdown(block);
if (summary.length > 0) {
return summary;
}
}
return '';
};
@@ -0,0 +1,26 @@
import { t } from '@lingui/core/macro';
export const getCustomApplicationDescription =
(): string => t`Host your workspace's customizations and overrides.
#### What it includes
Every extension you create on top of the standard app is grouped under Custom. It keeps your schema changes, interface changes, and workspace-specific logic in one place.
- Custom objects and fields for your own data model
- Views, navigation items, and record layouts that shape how your team works
- Logic functions, front components, and agents that automate or extend the workspace
#### Why it exists
Use this app for workspace-specific customization that should stay local to this workspace.
If you are shaping one workspace for one business, keep it here. If you are building a reusable business app with its own data model, UI, and automation that should be versioned, shared, or installed across workspaces, create a dedicated app instead.
#### Build your own app
Scaffold a new app in one command:
\`\`\`bash
npx create-twenty-app@latest my-twenty-app
\`\`\`
See the [Getting Started guide](https://twenty.com/developers/extend/apps/getting-started) for the full walkthrough, and [Building Apps](https://twenty.com/developers/extend/apps/building) for the \`defineApplication\` / \`defineEntity\` APIs.`;
@@ -0,0 +1,33 @@
import { t } from '@lingui/core/macro';
export const getStandardApplicationDescription =
(): string => t`The base data model every Twenty workspace runs on.
#### What "foundation" means
Every Twenty workspace starts with this set of objects. They define the shape of your CRM, including relationships, activity, and reporting. Everything else, including marketplace apps, AI agents, and custom objects, plugs into them.
#### Included objects
- **People & Companies**: contact and account records
- **Opportunities**: your sales pipeline
- **Notes & Tasks**: activity and follow-ups
- **Workflows & Dashboards**: automation and reporting
Remove this app and the rest of Twenty has nothing to hang off.
#### Build your own app
Extend Twenty with your own objects, fields, logic functions, or AI skills. Scaffold a new app in one command:
\`\`\`bash
npx create-twenty-app@latest my-twenty-app
\`\`\`
Then inside the folder:
\`\`\`bash
cd my-twenty-app
yarn twenty dev
\`\`\`
See the [Getting Started guide](https://twenty.com/developers/extend/apps/getting-started) for the full walkthrough, and [Building Apps](https://twenty.com/developers/extend/apps/building) for the \`defineApplication\` / \`defineEntity\` APIs.`;
@@ -354,7 +354,7 @@ export class ApplicationService {
const workspaceCustomApplication = await this.create(
{
description: 'Workspace custom application',
description: null,
name: 'Custom',
sourcePath: 'workspace-custom',
version: '1.0.1',
@@ -6,11 +6,11 @@ import {
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
// description is owned by the frontend (translated) — see getStandardApplicationDescription.
export const TWENTY_STANDARD_APPLICATION = {
universalIdentifier: TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
name: TWENTY_STANDARD_APPLICATION_NAME,
description:
'Twenty is an open-source CRM that allows you to manage your sales and customer relationships',
description: null,
version: '1.0.1',
sourcePath: 'cli-sync',
sourceType: ApplicationRegistrationSourceType.LOCAL,