fix: display workspace avatars correctly when URLs fail to load (#14814)

## Changes made
- Updated custom avatar components to favor background color by default
- Updated `AvatarData` component to let you manually specify the source
of the text used when images fail to load, and updated the orgs
breadcrumb segment to use it
- Added some logic for handling emoji images better
This commit is contained in:
Michael Smith
2024-10-02 09:46:25 -05:00
committed by GitHub
parent 0aa84b18a1
commit 680e28bdce
5 changed files with 83 additions and 42 deletions

View File

@ -8,18 +8,31 @@ export interface AvatarDataProps {
subtitle?: ReactNode;
src?: string;
avatar?: React.ReactNode;
/**
* Lets you specify the character(s) displayed in an avatar when an image is
* unavailable (like when the network request fails).
*
* If not specified, the component will try to parse the first character
* from the title prop if it is a string.
*/
imgFallbackText?: string;
}
export const AvatarData: FC<AvatarDataProps> = ({
title,
subtitle,
src,
imgFallbackText,
avatar,
}) => {
const theme = useTheme();
if (!avatar) {
avatar = <Avatar src={src}>{title}</Avatar>;
avatar = (
<Avatar background src={src}>
{(typeof title === "string" ? title : imgFallbackText) || "-"}
</Avatar>
);
}
return (

View File

@ -387,49 +387,49 @@ const styles = {
const classNames = {
link: (css, theme) => css`
color: inherit;
display: block;
font-size: 14px;
text-decoration: none;
padding: 10px 12px 10px 16px;
border-radius: 4px;
transition: background-color 0.15s ease-in-out;
position: relative;
color: inherit;
display: block;
font-size: 14px;
text-decoration: none;
padding: 10px 12px 10px 16px;
border-radius: 4px;
transition: background-color 0.15s ease-in-out;
position: relative;
&:hover {
background-color: ${theme.palette.action.hover};
}
&:hover {
background-color: ${theme.palette.action.hover};
}
border-left: 3px solid transparent;
`,
border-left: 3px solid transparent;
`,
activeLink: (css, theme) => css`
border-left-color: ${theme.palette.primary.main};
border-top-left-radius: 0;
border-bottom-left-radius: 0;
`,
border-left-color: ${theme.palette.primary.main};
border-top-left-radius: 0;
border-bottom-left-radius: 0;
`,
subLink: (css, theme) => css`
color: ${theme.palette.text.secondary};
text-decoration: none;
color: ${theme.palette.text.secondary};
text-decoration: none;
display: block;
font-size: 13px;
margin-left: 44px;
padding: 4px 12px;
border-radius: 4px;
transition: background-color 0.15s ease-in-out;
margin-bottom: 1px;
position: relative;
display: block;
font-size: 13px;
margin-left: 44px;
padding: 4px 12px;
border-radius: 4px;
transition: background-color 0.15s ease-in-out;
margin-bottom: 1px;
position: relative;
&:hover {
color: ${theme.palette.text.primary};
background-color: ${theme.palette.action.hover};
}
`,
&:hover {
color: ${theme.palette.text.primary};
background-color: ${theme.palette.action.hover};
}
`,
activeSubLink: (css, theme) => css`
color: ${theme.palette.text.primary};
font-weight: 600;
`,
color: ${theme.palette.text.primary};
font-weight: 600;
`,
} satisfies Record<string, ClassName>;

View File

@ -25,6 +25,7 @@ import { WorkspaceStatusBadge } from "modules/workspaces/WorkspaceStatusBadge/Wo
import type { FC } from "react";
import { useQuery } from "react-query";
import { Link as RouterLink } from "react-router-dom";
import { isEmojiUrl } from "utils/appearance";
import { displayDormantDeletion } from "utils/dormant";
import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions";
import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications";
@ -344,9 +345,15 @@ const OrganizationBreadcrumb: FC<OrganizationBreadcrumbProps> = ({
subtitle="Organization"
avatar={
orgIconUrl && (
<ExternalAvatar src={orgIconUrl} variant="square" fitImage />
<ExternalAvatar
src={orgIconUrl}
title={orgName}
variant={isEmojiUrl(orgIconUrl) ? "square" : "circular"}
fitImage
/>
)
}
imgFallbackText={orgName}
/>
</HelpTooltipContent>
</Popover>
@ -405,8 +412,14 @@ const WorkspaceBreadcrumb: FC<WorkspaceBreadcrumbProps> = ({
</Link>
}
avatar={
<ExternalAvatar src={templateIconUrl} variant="square" fitImage />
<ExternalAvatar
src={templateIconUrl}
title={workspaceName}
variant={isEmojiUrl(templateIconUrl) ? "square" : "circular"}
fitImage
/>
}
imgFallbackText={templateVersionDisplayName}
/>
</HelpTooltipContent>
</Popover>

View File

@ -75,8 +75,8 @@ const parseInvertFilterParameters = (
let extraStyles: CSSObject | undefined;
const brightness = params.get("brightness");
if (multiplier.test(brightness!)) {
const brightness = params.get("brightness") ?? "";
if (multiplier.test(brightness)) {
let filter = baseStyles.filter ?? "";
filter += ` brightness(${brightness})`;
extraStyles = { ...extraStyles, filter };
@ -131,7 +131,7 @@ export function getExternalImageStylesFromUrl(
) {
return parseImageParameters(
modes,
defaultParametersForBuiltinIcons.get(url.pathname)!,
defaultParametersForBuiltinIcons.get(url.pathname) as string,
);
}

View File

@ -14,3 +14,18 @@ export const getLogoURL = (): string => {
?.getAttribute("content");
return c && !c.startsWith("{{ .") ? c : "";
};
/**
* Exposes an easy way to determine if a given URL is for an emoji hosted on
* the Coder deployment.
*
* Helps when you need to style emojis differently (i.e., not adding rounding to
* the container so that the emoji doesn't get cut off).
*/
export function isEmojiUrl(url: string | undefined): boolean {
if (!url) {
return false;
}
return url.startsWith("/emojis/");
}