mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
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:
@ -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 (
|
||||
|
@ -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>;
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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/");
|
||||
}
|
||||
|
Reference in New Issue
Block a user