fix(settings): force display "Standard" and "Custom" for app chips (#19912)

## Summary
- Override the `AppChip` label so the Twenty standard application always
renders as `Standard` and the workspace custom application always
renders as `Custom`, instead of leaking each app's underlying name (e.g.
`Twenty Eng's custom application`).
- Detection mirrors the logic already used in
`useApplicationAvatarColors`, relying on
`TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER` /
`TWENTY_STANDARD_APPLICATION_NAME` and
`currentWorkspace.workspaceCustomApplication.id`.
- The `This app` label for the current application context and the
original `application.name` fallback for any other installed app are
preserved.

## Affected UI
- Settings → Data model → Existing objects (App column).
- Anywhere else `AppChip` / `useApplicationChipData` is used.
This commit is contained in:
Charles Bochet 2026-04-21 08:26:26 +02:00 committed by GitHub
parent 34e3b1b90b
commit 8d24551e71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 70 additions and 63 deletions

View file

@ -1,64 +1,48 @@
import { useApplicationChipData } from '@/applications/hooks/useApplicationChipData';
import { styled } from '@linaria/react';
import { Avatar } from 'twenty-ui/display';
import {
Chip,
ChipAccent,
type ChipSize,
ChipVariant,
LinkChip,
} from 'twenty-ui/components';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type AppChipProps = {
applicationId: string;
variant?: ChipVariant;
size?: ChipSize;
to?: string;
className?: string;
};
export const AppChip = ({
applicationId,
variant,
size,
to,
className,
}: AppChipProps) => {
const StyledContainer = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.secondary};
display: inline-flex;
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[2]};
max-width: 100%;
min-width: 0;
overflow: hidden;
`;
const StyledLabel = styled.span`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const AppChip = ({ applicationId, className }: AppChipProps) => {
const { applicationChipData } = useApplicationChipData({ applicationId });
const leftComponent = (
<Avatar
type="app"
size="md"
placeholder={applicationChipData.name}
placeholderColorSeed={applicationChipData.seed}
color={applicationChipData.colors?.color}
backgroundColor={applicationChipData.colors?.backgroundColor}
borderColor={applicationChipData.colors?.borderColor}
/>
);
if (to) {
return (
<LinkChip
className={className}
label={applicationChipData.name}
leftComponent={leftComponent}
size={size}
variant={variant ?? ChipVariant.Highlighted}
accent={ChipAccent.TextPrimary}
to={to}
/>
);
}
return (
<Chip
className={className}
label={applicationChipData.name}
leftComponent={leftComponent}
size={size}
variant={variant ?? ChipVariant.Transparent}
accent={ChipAccent.TextPrimary}
/>
<StyledContainer className={className}>
<Avatar
type="app"
size="md"
placeholder={applicationChipData.name}
placeholderColorSeed={applicationChipData.seed}
color={applicationChipData.colors?.color}
backgroundColor={applicationChipData.colors?.backgroundColor}
borderColor={applicationChipData.colors?.borderColor}
/>
<StyledLabel title={applicationChipData.name}>
{applicationChipData.name}
</StyledLabel>
</StyledContainer>
);
};

View file

@ -1,10 +1,7 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useContext } from 'react';
import {
TWENTY_STANDARD_APPLICATION_NAME,
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
} from 'twenty-shared/application';
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui/theme-constants';
@ -20,6 +17,18 @@ 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 => {
@ -31,20 +40,16 @@ export const useApplicationAvatarColors = (
}
const isStandard =
isDefined(application.universalIdentifier) &&
application.universalIdentifier ===
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER ||
application.name === TWENTY_STANDARD_APPLICATION_NAME;
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER;
const isCustom =
isDefined(currentWorkspace?.workspaceCustomApplication?.id) &&
currentWorkspace.workspaceCustomApplication.id === application.id;
if (isStandard) {
return {
backgroundColor: theme.color.blue5,
borderColor: theme.color.blue6,
color: theme.color.blue12,
};
return STANDARD_APPLICATION_AVATAR_COLORS;
}
if (isCustom) {

View file

@ -7,6 +7,7 @@ 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 = {
@ -47,9 +48,26 @@ 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
? t`Standard`
: isCustom
? t`Custom`
: application.name;
return {
applicationChipData: {
name: isCurrent ? t`This app` : application.name,
name: displayName,
seed: application.universalIdentifier ?? application.name,
colors,
},