Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

H-3542: Make use of entity/type icons consistently #5598

Merged
merged 11 commits into from
Nov 7, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ export const BlockLoader: FunctionComponent<BlockLoaderProps> = ({
* The traversal depths will not be accurate, because the actual traversal was rooted at the block collection.
* This data is only used briefly – we fetch the block's subgraph from the API further on this effect.
*/
const latestEditionId = Object.keys(entityEditionMap).sort().pop()!;
const latestEditionId = Object.keys(entityEditionMap)
.toSorted()
.at(-1)!;
subgraphToRewrite = {
...blockCollectionSubgraph,
roots: [
Expand Down Expand Up @@ -231,8 +233,8 @@ export const BlockLoader: FunctionComponent<BlockLoaderProps> = ({
* doing so only for the latest edition is an optimization which assumes blocks only care about the latest value.
*/
const latestSubgraphEditionTimestamp = Object.keys(entityOrTypeEditionMap)
.sort()
.pop() as EntityRevisionId;
.toSorted()
.at(-1) as EntityRevisionId;

/**
* Check if we have a version of this entity in the local store, if provided, and if it's newer than in the subgraph.
Expand Down
2 changes: 1 addition & 1 deletion apps/hash-frontend/src/components/grid/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const getYCenter = (
* @returns cell horizontal padding
*/
export const getCellHorizontalPadding = (atFirstColumn?: boolean) =>
atFirstColumn ? 36 : 22;
atFirstColumn ? 42 : 22;

export type BlankCell = CustomCell<{ kind: "blank-cell" }>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const drawCellFadeOutGradient = (
ctx.fillRect(rectLeft, rect.y, extraWidth, rect.height);
}

const grdWidth = 50;
const grdWidth = 20;
const grdLeft = rectLeft - grdWidth;
const grd = ctx.createLinearGradient(rectLeft - grdWidth, 0, rectLeft, 0);
grd.addColorStop(0, "#ffffff00");
Expand Down
171 changes: 143 additions & 28 deletions apps/hash-frontend/src/components/grid/utils/draw-chip-with-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,63 @@ import { getYCenter } from "../utils";
import type { CustomIcon } from "./custom-grid-icons";
import { drawChip } from "./draw-chip";

const filledIconCanvasCache: {
[originalUrl: string]: {
[fillColor: string]: HTMLCanvasElement;
};
} = {};

/**
* In order to fill SVGs with a new color, we draw them on an off-screen canvas,
* and fill the non-transparent parts of the image with the desired color.
*/
const getFilledCanvas = ({
fill,
iconUrl,
image,
}: {
fill: string;
iconUrl: string;
image: HTMLImageElement | ImageBitmap;
}): HTMLImageElement | ImageBitmap | HTMLCanvasElement => {
if (!iconUrl.endsWith(".svg")) {
return image;
}

const cachedCanvas = filledIconCanvasCache[iconUrl]?.[fill];

if (cachedCanvas) {
return cachedCanvas;
}

const offScreenCanvas = document.createElement("canvas");
const offScreenContext = offScreenCanvas.getContext("2d");

if (!offScreenContext) {
throw new Error("Could not create off-screen canvas");
}

offScreenCanvas.width = image.width;
offScreenCanvas.height = image.height;

// Draw the image onto the off-screen canvas
offScreenContext.drawImage(image, 0, 0);

offScreenContext.globalCompositeOperation = "source-in";
offScreenContext.fillStyle = fill;
offScreenContext.fillRect(
0,
0,
offScreenCanvas.width,
offScreenCanvas.height,
);

filledIconCanvasCache[iconUrl] ??= {};
filledIconCanvasCache[iconUrl][fill] = offScreenCanvas;

return offScreenCanvas;
};

const drawClippedImage = ({
ctx,
height,
Expand Down Expand Up @@ -71,6 +128,21 @@ const drawClippedImage = ({
return width;
};

export type DrawChipWithIconProps = {
args: DrawArgs<CustomCell>;
icon?:
| {
inbuiltIcon: CustomIcon;
}
| { imageSrc: string }
| { entityTypeIcon: string };
iconFill?: string;
text: string;
left: number;
color: ChipCellColor;
variant?: ChipCellVariant;
};

/**
* @param args draw args of cell
* @param text text content of chip
Expand All @@ -84,26 +156,18 @@ const drawClippedImage = ({
*/
export const drawChipWithIcon = ({
args,
icon,
iconFill,
text,
icon = "bpAsterisk",
imageSrc,
left,
color,
variant,
}: {
args: DrawArgs<CustomCell>;
text: string;
icon?: CustomIcon;
imageSrc?: string;
left: number;
color: ChipCellColor;
variant?: ChipCellVariant;
}) => {
}: DrawChipWithIconProps) => {
const { ctx, theme, imageLoader, col, row } = args;
const yCenter = getYCenter(args);

const paddingX = 12;
const iconHeight = imageSrc ? 24 : 12;
const iconHeight = icon && "imageSrc" in icon ? 24 : 12;
const gap = 8;

const iconLeft = left + paddingX;
Expand All @@ -113,18 +177,22 @@ export const drawChipWithIcon = ({

const iconTop = yCenter - iconHeight / 2;

const { bgColor, borderColor, iconColor, textColor } = getChipColors(
color,
variant,
);
const {
bgColor,
borderColor,
iconColor: defaultColor,
textColor,
} = getChipColors(color, variant);

const iconColor = iconFill ?? defaultColor;

let chipWidth = iconHeight + gap + textWidth + 2 * paddingX;

let chipHeight;
let chipTop;

if (imageSrc) {
const image = imageLoader.loadOrGetImage(imageSrc, col, row);
if (icon && "imageSrc" in icon) {
const image = imageLoader.loadOrGetImage(icon.imageSrc, col, row);

if (image) {
const maxWidth = 80;
Expand Down Expand Up @@ -154,7 +222,7 @@ export const drawChipWithIcon = ({
width,
});
} else {
throw new Error(`Image not loaded: ${imageSrc}`);
throw new Error(`Image not loaded: ${icon.imageSrc}`);
}
} else {
({ height: chipHeight, top: chipTop } = drawChip(
Expand All @@ -165,15 +233,62 @@ export const drawChipWithIcon = ({
borderColor,
));

args.spriteManager.drawSprite(
icon,
"normal",
ctx,
iconLeft,
iconTop,
iconHeight,
{ ...theme, fgIconHeader: iconColor },
);
if (icon && "inbuiltIcon" in icon) {
args.spriteManager.drawSprite(
icon.inbuiltIcon,
"normal",
ctx,
iconLeft,
iconTop,
iconHeight,
{ ...theme, fgIconHeader: iconColor },
);
} else if (icon && "entityTypeIcon" in icon) {
if (icon.entityTypeIcon.match(/\p{Extended_Pictographic}$/u)) {
/**
* This is an emoji icon
*/
ctx.fillStyle = iconColor;
const currentFont = ctx.font;
ctx.font = `bold ${iconHeight}px Inter`;
ctx.fillText(icon.entityTypeIcon, iconLeft, yCenter);
ctx.font = currentFont;
} else {
let iconUrl;
if (icon.entityTypeIcon.startsWith("/")) {
iconUrl = new URL(icon.entityTypeIcon, window.location.origin).href;
} else if (icon.entityTypeIcon.startsWith("https")) {
iconUrl = icon.entityTypeIcon;
}

if (iconUrl) {
const image = imageLoader.loadOrGetImage(iconUrl, col, row);

if (image) {
const canvasWithFill = getFilledCanvas({
fill: iconColor,
iconUrl,
image,
});

const aspectRatio = image.width / image.height;

const width =
aspectRatio > 1 ? iconHeight : iconHeight * aspectRatio;
const height =
aspectRatio > 1 ? iconHeight / aspectRatio : iconHeight;

ctx.drawImage(
canvasWithFill,
iconLeft + (iconHeight - width) / 2,
iconTop + (iconHeight - height) / 2,
width,
height,
);
}
}
}
}
}

const textLeft = left + chipWidth - paddingX - textWidth;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { EntityOrTypeIcon } from "@hashintel/design-system";
import type { Entity } from "@local/hash-graph-sdk/entity";
import type { EntityTypeWithMetadata } from "@local/hash-graph-types/ontology";
import type { OwnedById } from "@local/hash-graph-types/web";
import { generateEntityLabel } from "@local/hash-isomorphic-utils/generate-entity-label";
import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids";
import { includesPageEntityTypeId } from "@local/hash-isomorphic-utils/page-entity-type-ids";
import type { EntityRootType, Subgraph } from "@local/hash-subgraph";
import { extractEntityUuidFromEntityId } from "@local/hash-subgraph";
import {
extractEntityUuidFromEntityId,
linkEntityTypeUrl,
} from "@local/hash-subgraph";
import {
Box,
Divider,
Expand All @@ -29,12 +33,9 @@ import type { Org, User } from "../../lib/user-and-org";
import { useEntityTypesContextRequired } from "../../shared/entity-types-context/hooks/use-entity-types-context-required";
import { ArrowDownAZRegularIcon } from "../../shared/icons/arrow-down-a-z-regular-icon";
import { ArrowUpZARegularIcon } from "../../shared/icons/arrow-up-a-z-regular-icon";
import { CanvasIcon } from "../../shared/icons/canvas-icon";
import { ClockRegularIcon } from "../../shared/icons/clock-regular-icon";
import { PageLightIcon } from "../../shared/icons/page-light-icon";
import { PlusRegularIcon } from "../../shared/icons/plus-regular";
import { Button, Link, MenuItem } from "../../shared/ui";
import { useEntityIcon } from "../../shared/use-entity-icon";
import { ProfileSectionHeading } from "../[shortname]/shared/profile-section-heading";
import { InlineSelect } from "../shared/inline-select";
import type { ProfilePageTab } from "./util";
Expand All @@ -59,20 +60,6 @@ const EntityRow: FunctionComponent<{
? format(updatedAt, "d MMMM yyyy")
: `${formatDistanceToNowStrict(updatedAt)} ago`;

const icon = useEntityIcon({
entity,
entityTypes: entityType ? [entityType] : undefined,
pageIcon: entity.metadata.entityTypeIds.includes(
systemEntityTypes.canvas.entityTypeId,
) ? (
<CanvasIcon
sx={{ fontSize: 20, fill: ({ palette }) => palette.gray[40] }}
/>
) : (
<PageLightIcon sx={{ fontSize: 18 }} />
),
});

return (
<Link
target="_blank"
Expand All @@ -90,17 +77,21 @@ const EntityRow: FunctionComponent<{
}}
>
<Box sx={{ padding: 3 }}>
<Box
display="flex"
alignItems="center"
columnGap={1.5}
sx={{
"> svg": {
color: ({ palette }) => palette.gray[50],
},
}}
>
{icon}
<Box display="flex" alignItems="center" columnGap={1.5}>
<EntityOrTypeIcon
entity={null}
icon={entityType?.schema.icon}
fontSize={14}
fill={({ palette }) => palette.blue[70]}
isLink={
/**
* @todo H-3363 use closed schema to take account of indirectly inherited link status
*/
!!entityType?.schema.allOf?.some(
(allOf) => allOf.$ref === linkEntityTypeUrl,
)
}
/>
<Typography component="h2" sx={{ fontWeight: 700, fontSize: 14 }}>
{label}
</Typography>
Expand Down
Loading
Loading