diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/edit-entity-slide-over.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/edit-entity-slide-over.tsx index 4afec57747f..7bf86185dc2 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/edit-entity-slide-over.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/edit-entity-slide-over.tsx @@ -59,9 +59,9 @@ interface EditEntitySlideOverProps { */ entityId?: EntityId; /** - * If a modal container ref is provided, the modal will be attached to it (defaults to the MUI default, the body) + * If a container ref is provided, the slide will be attached to it (defaults to the MUI default, the body) */ - modalContainerRef?: RefObject; + slideContainerRef?: RefObject; } /** @@ -69,7 +69,7 @@ interface EditEntitySlideOverProps { */ export const EditEntitySlideOver = ({ hideOpenInNew, - modalContainerRef, + slideContainerRef, open, onClose, onSubmit, @@ -284,7 +284,7 @@ export const EditEntitySlideOver = ({ onClose={onClose} anchor="right" ModalProps={{ - container: modalContainerRef?.current ?? undefined, + container: slideContainerRef?.current ?? undefined, }} PaperProps={{ sx: (theme) => ({ diff --git a/apps/hash-frontend/src/pages/shared/entities-table.tsx b/apps/hash-frontend/src/pages/shared/entities-table.tsx index 3c236888e69..befb603fea2 100644 --- a/apps/hash-frontend/src/pages/shared/entities-table.tsx +++ b/apps/hash-frontend/src/pages/shared/entities-table.tsx @@ -92,8 +92,10 @@ export const EntitiesTable: FunctionComponent<{ }); const [showSearch, setShowSearch] = useState(false); - const [selectedEntityTypeId, setSelectedEntityTypeId] = - useState(null); + const [selectedEntityType, setSelectedEntityType] = useState<{ + entityTypeId: VersionedUrl; + slideContainerRef?: RefObject; + } | null>(null); const { entityTypeBaseUrl, @@ -224,7 +226,7 @@ export const EntitiesTable: FunctionComponent<{ const [selectedEntity, setSelectedEntity] = useState<{ entityId: EntityId; - modalContainerRef?: RefObject; + slideContainerRef?: RefObject; subgraph: Subgraph; } | null>(null); @@ -241,7 +243,7 @@ export const EntitiesTable: FunctionComponent<{ setSelectedEntity({ entityId, - modalContainerRef, + slideContainerRef: modalContainerRef, subgraph: entitySubgraph, }); } @@ -317,7 +319,7 @@ export const EntitiesTable: FunctionComponent<{ if (columnId === "web") { void router.push(`/${cellValue}`); } else { - setSelectedEntityTypeId(row.entityTypeId); + setSelectedEntityType({ entityTypeId: row.entityTypeId }); } }, }, @@ -680,10 +682,11 @@ export const EntitiesTable: FunctionComponent<{ return ( <> - {selectedEntityTypeId && ( + {selectedEntityType && ( setSelectedEntityTypeId(null)} + rootTypeId={selectedEntityType.entityTypeId} + onClose={() => setSelectedEntityType(null)} + slideContainerRef={selectedEntityType.slideContainerRef} /> )} {selectedEntity ? ( @@ -702,7 +705,7 @@ export const EntitiesTable: FunctionComponent<{ If we've been given a specific DOM element to contain the modal, pass it here. This is for use when attaching to the body is not suitable (e.g. a specific DOM element is full-screened). */ - modalContainerRef={selectedEntity.modalContainerRef} + slideContainerRef={selectedEntity.slideContainerRef} open onClose={() => setSelectedEntity(null)} readonly diff --git a/apps/hash-frontend/src/pages/shared/entity-type-page/type-slide-over-stack.tsx b/apps/hash-frontend/src/pages/shared/entity-type-page/type-slide-over-stack.tsx index 5ff75d0593a..dfee74d9cc2 100644 --- a/apps/hash-frontend/src/pages/shared/entity-type-page/type-slide-over-stack.tsx +++ b/apps/hash-frontend/src/pages/shared/entity-type-page/type-slide-over-stack.tsx @@ -1,6 +1,6 @@ import type { VersionedUrl } from "@blockprotocol/type-system"; import { Backdrop } from "@mui/material"; -import type { FunctionComponent } from "react"; +import type { FunctionComponent, RefObject } from "react"; import { useCallback, useState } from "react"; import { TypeSlideOverSlide } from "./type-slide-over-stack/type-slide-over-slide"; @@ -8,7 +8,11 @@ import { TypeSlideOverSlide } from "./type-slide-over-stack/type-slide-over-slid export const TypeSlideOverStack: FunctionComponent<{ rootTypeId: VersionedUrl; onClose: () => void; -}> = ({ rootTypeId, onClose }) => { + /** + * If a container ref is provided, the slide will be attached to it (defaults to the MUI default, the body) + */ + slideContainerRef?: RefObject; +}> = ({ rootTypeId, onClose, slideContainerRef }) => { const [animateOut, setAnimateOut] = useState(false); const [items, setItems] = useState([rootTypeId]); const [currentIndex, setCurrentIndex] = useState(0); @@ -59,6 +63,7 @@ export const TypeSlideOverStack: FunctionComponent<{ onForward={index < items.length - 1 ? handleForward : undefined} onClose={handleClose} onNavigateToType={handleNavigateToType} + slideContainerRef={slideContainerRef} typeUrl={typeId} stackPosition={index} /> diff --git a/apps/hash-frontend/src/pages/shared/entity-type-page/type-slide-over-stack/type-slide-over-slide.tsx b/apps/hash-frontend/src/pages/shared/entity-type-page/type-slide-over-stack/type-slide-over-slide.tsx index a19999a55c8..9539510f6e9 100644 --- a/apps/hash-frontend/src/pages/shared/entity-type-page/type-slide-over-stack/type-slide-over-slide.tsx +++ b/apps/hash-frontend/src/pages/shared/entity-type-page/type-slide-over-stack/type-slide-over-slide.tsx @@ -19,7 +19,7 @@ import { import type { EntityTypeWithMetadata } from "@local/hash-graph-types/ontology"; import { componentsFromVersionedUrl } from "@local/hash-subgraph/type-system-patch"; import { Box, ButtonBase, Slide, Tooltip } from "@mui/material"; -import type { FunctionComponent } from "react"; +import type { FunctionComponent, RefObject } from "react"; import { useCallback, useMemo, useState } from "react"; import { useEntityTypesContextRequired } from "../../../../shared/entity-types-context/hooks/use-entity-types-context-required"; @@ -109,6 +109,10 @@ interface TypeSlideOverSlideProps { onForward?: () => void; onClose: () => void; onNavigateToType: (url: VersionedUrl) => void; + /** + * If a container ref is provided, the slide will be attached to it (defaults to the MUI default, the body) + */ + slideContainerRef?: RefObject; typeUrl: VersionedUrl; } @@ -120,6 +124,7 @@ export const TypeSlideOverSlide: FunctionComponent = ({ onBack, onClose, onForward, + slideContainerRef, }) => { const { loading: loadingNamespace, routeNamespace } = useRouteNamespace(); @@ -185,6 +190,7 @@ export const TypeSlideOverSlide: FunctionComponent = ({ return ( event.stopPropagation()} diff --git a/apps/hash-frontend/src/pages/shared/type-graph-visualizer.tsx b/apps/hash-frontend/src/pages/shared/type-graph-visualizer.tsx index 13076954042..37f2d53a541 100644 --- a/apps/hash-frontend/src/pages/shared/type-graph-visualizer.tsx +++ b/apps/hash-frontend/src/pages/shared/type-graph-visualizer.tsx @@ -6,6 +6,7 @@ import type { PropertyTypeWithMetadata, } from "@local/hash-graph-types/ontology"; import { useTheme } from "@mui/material"; +import type { RefObject } from "react"; import { useCallback, useMemo } from "react"; import { useEntityTypesContextRequired } from "../../shared/entity-types-context/hooks/use-entity-types-context-required"; @@ -18,11 +19,23 @@ import type { const anythingNodeId = "anything"; +const defaultConfig = { + graphKey: "type-graph", + nodeHighlighting: { + direction: "All", + depth: 1, + }, + nodeSizing: { mode: "static" }, +} as const; + export const TypeGraphVisualizer = ({ onTypeClick, types, }: { - onTypeClick: (typeId: VersionedUrl) => void; + onTypeClick: ( + typeId: VersionedUrl, + screenContainerRef?: RefObject, + ) => void; types: ( | DataTypeWithMetadata | EntityTypeWithMetadata @@ -38,6 +51,7 @@ export const TypeGraphVisualizer = ({ const nodesToAdd: GraphVizNode[] = []; const addedNodeIds = new Set(); + const addedEdgeIds = new Set(); const anythingNode: GraphVizNode = { color: palette.gray[30], @@ -109,7 +123,7 @@ export const TypeGraphVisualizer = ({ * The id is therefore based on the link type and the destination types. */ const linkNodeId = `${linkTypeId}~${ - destinationTypeIds?.join("-") ?? "anything" + destinationTypeIds?.sort().join("-") ?? "anything" }`; if (!addedNodeIds.has(linkNodeId)) { @@ -143,12 +157,16 @@ export const TypeGraphVisualizer = ({ continue; } - edgesToAdd.push({ - edgeId: `${linkNodeId}~${destinationTypeId}`, - size: 3, - source: linkNodeId, - target: destinationTypeId, - }); + const edgeId = `${linkNodeId}~${destinationTypeId}`; + if (!addedEdgeIds.has(edgeId)) { + edgesToAdd.push({ + edgeId: `${linkNodeId}~${destinationTypeId}`, + size: 3, + source: linkNodeId, + target: destinationTypeId, + }); + addedEdgeIds.add(edgeId); + } } } else { /** @@ -184,12 +202,16 @@ export const TypeGraphVisualizer = ({ linkNodesByEntityTypeId[linkTypeId].instanceIds.push(linkNodeId); } - edgesToAdd.push({ - edgeId: `${entityTypeId}~${linkNodeId}`, - size: 3, - source: entityTypeId, - target: linkNodeId, - }); + const edgeId = `${entityTypeId}~${linkNodeId}`; + if (!addedEdgeIds.has(edgeId)) { + edgesToAdd.push({ + edgeId, + size: 3, + source: entityTypeId, + target: linkNodeId, + }); + addedEdgeIds.add(edgeId); + } } } @@ -220,33 +242,21 @@ export const TypeGraphVisualizer = ({ const onNodeClick = useCallback< NonNullable >( - ({ nodeId, isFullScreen }) => { + ({ nodeId, screenContainerRef }) => { if (nodeId === anythingNodeId) { return; } - if (isFullScreen) { - return; - } - const typeVersionedUrl = nodeId.split("~")[0] as VersionedUrl; - onTypeClick(typeVersionedUrl); + onTypeClick(typeVersionedUrl, screenContainerRef); }, [onTypeClick], ); return ( (null); + const [selectedEntityType, setSelectedEntityType] = useState<{ + entityTypeId: VersionedUrl; + slideContainerRef?: RefObject; + } | null>(null); const { isSpecialEntityTypeLookup } = useEntityTypesContextRequired(); @@ -355,7 +357,7 @@ export const TypesTable: FunctionComponent<{ value: row.title, onClick: () => { if (row.kind === "entity-type") { - setSelectedEntityTypeId(row.typeId); + setSelectedEntityType({ entityTypeId: row.typeId }); } else { void router.push(generateLinkParameters(row.typeId).href); } @@ -456,12 +458,22 @@ export const TypesTable: FunctionComponent<{ const currentlyDisplayedRowsRef = useRef(null); + const onTypeClick = useCallback( + (typeId: VersionedUrl, slideContainerRef?: RefObject) => + setSelectedEntityType({ + entityTypeId: typeId, + slideContainerRef, + }), + [], + ); + return ( <> - {selectedEntityTypeId && ( + {selectedEntityType && ( setSelectedEntityTypeId(null)} + rootTypeId={selectedEntityType.entityTypeId} + onClose={() => setSelectedEntityType(null)} + slideContainerRef={selectedEntityType.slideContainerRef} /> )} @@ -530,7 +542,7 @@ export const TypesTable: FunctionComponent<{ ) : (