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>
@@ -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"
|
||||
/>
|
||||
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 30 KiB |
|
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}
|
||||
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
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,
|
||||
|
||||