diff --git a/packages/hash/frontend/src/lib/use-init-type-system.ts b/packages/hash/frontend/src/lib/use-init-type-system.ts index 3fa703e7471..668519d762a 100644 --- a/packages/hash/frontend/src/lib/use-init-type-system.ts +++ b/packages/hash/frontend/src/lib/use-init-type-system.ts @@ -1,18 +1,31 @@ import init from "@blockprotocol/type-system-web"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; -export const useInitTypeSystem = () => { +export const useAdvancedInitTypeSystem = () => { const [loadingTypeSystem, setLoadingTypeSystem] = useState(true); + const loadingPromise = useRef | null>(null); + + const loadTypeSystem = useCallback(() => { + if (loadingPromise.current) { + return loadingPromise.current; + } + + loadingPromise.current = (async () => { + await init().then(() => { + setLoadingTypeSystem(false); + }); + })(); + + return loadingPromise.current; + }, []); useEffect(() => { if (loadingTypeSystem) { - void (async () => { - await init().then(() => { - setLoadingTypeSystem(false); - }); - })(); + void loadTypeSystem(); } - }, [loadingTypeSystem, setLoadingTypeSystem]); + }, [loadTypeSystem, loadingTypeSystem]); - return loadingTypeSystem; + return [loadingTypeSystem, loadTypeSystem] as const; }; + +export const useInitTypeSystem = () => useAdvancedInitTypeSystem()[0]; diff --git a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/[entity-type-id].page.tsx b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/[entity-type-id].page.tsx index 9a18c26218c..926a409ff6e 100644 --- a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/[entity-type-id].page.tsx +++ b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/[entity-type-id].page.tsx @@ -1,198 +1,184 @@ -import { EntityType, PropertyType } from "@blockprotocol/type-system-web"; +import { extractBaseUri, extractVersion } from "@blockprotocol/type-system-web"; import { faAsterisk } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@hashintel/hash-design-system/fontawesome-icon"; -import { Box, Collapse, Container, Stack, Typography } from "@mui/material"; +import { Box, Container, Typography } from "@mui/material"; import { useRouter } from "next/router"; -import { useEffect, useLayoutEffect, useRef, useState } from "react"; -import { useBlockProtocolGetEntityType } from "../../../../components/hooks/blockProtocolFunctions/ontology/useBlockProtocolGetEntityType"; +import { FormProvider, useForm } from "react-hook-form"; import { FRONTEND_URL } from "../../../../lib/config"; -import { useInitTypeSystem } from "../../../../lib/use-init-type-system"; import { getPlainLayout, NextPageWithLayout } from "../../../../shared/layout"; import { TopContextBar } from "../../../shared/top-context-bar"; import { EditBar } from "./edit-bar"; +import { EntityTypeEditorForm } from "./form-types"; import { HashOntologyIcon } from "./hash-ontology-icon"; import { OntologyChip } from "./ontology-chip"; import { PropertyListCard } from "./property-list-card"; +import { useEntityType } from "./use-entity-type"; import { PropertyTypesContext, useRemotePropertyTypes, } from "./use-property-types"; - -const useEntityType = (entityTypeId: string, onCompleted?: () => void) => { - const { getEntityType } = useBlockProtocolGetEntityType(); - const [entityType, setEntityType] = useState(null); - - const onCompletedRef = useRef(onCompleted); - - useLayoutEffect(() => { - onCompletedRef.current = onCompleted; - }); - - useEffect(() => { - void getEntityType({ - data: { entityTypeId }, - }).then((value) => { - if (value.data) { - setEntityType(value.data.entityType); - onCompletedRef.current?.(); - } - }); - }, [getEntityType, entityTypeId]); - - return entityType; -}; +import { mustBeVersionedUri } from "./util"; // @todo loading state // @todo handle displaying entity type not yet created const Page: NextPageWithLayout = () => { const router = useRouter(); - const [insertedPropertyTypes, setInsertedPropertyTypes] = useState< - PropertyType[] - >([]); - const [removedPropertyTypes, setRemovedPropertyTypes] = useState< - PropertyType[] - >([]); - - // @todo find this out somehow - const currentVersion = 1; - const entityTypeId = `${FRONTEND_URL}/${router.query["account-slug"]}/types/entity-type/${router.query["entity-type-id"]}/v/${currentVersion}`; + // @todo how to handle remote types + const baseEntityTypeUri = `${FRONTEND_URL}/${router.query["account-slug"]}/types/entity-type/${router.query["entity-type-id"]}/`; - const entityType = useEntityType(entityTypeId, () => { - setInsertedPropertyTypes([]); - setRemovedPropertyTypes([]); + const formMethods = useForm({ + defaultValues: { properties: [] }, }); + const { handleSubmit: wrapHandleSubmit, reset } = formMethods; + + const [entityType, updateEntityType] = useEntityType( + baseEntityTypeUri, + (fetchedEntityType) => { + reset({ + properties: Object.values(fetchedEntityType.properties).map((ref) => { + if ("type" in ref) { + // @todo handle property arrays + throw new Error("handle property arrays"); + } + return { $id: mustBeVersionedUri(ref.$ref) }; + }), + }); + }, + ); const propertyTypes = useRemotePropertyTypes(); - const loadingTypeSystem = useInitTypeSystem(); - if (!entityType || loadingTypeSystem) { + const handleSubmit = wrapHandleSubmit(async (data) => { + if (!entityType) { + return; + } + + const properties = Object.fromEntries( + data.properties.map((property) => { + const propertyKey = extractBaseUri(property.$id); + + return [propertyKey, { $ref: property.$id }]; + }), + ); + + const res = await updateEntityType({ + properties, + }); + + if (!res.errors?.length) { + reset(data); + } else { + throw new Error("Could not publish changes"); + } + }); + + if (!entityType || !propertyTypes) { return null; } + const currentVersion = extractVersion(mustBeVersionedUri(entityType.$id)); + return ( - ({ - minHeight: "100vh", - background: theme.palette.gray[10], - })} - > - - , - }, - ]} - scrollToTop={() => {}} - /> - 0} - > + + ({ + minHeight: "100vh", + background: theme.palette.gray[10], + display: "flex", + flexDirection: "column", + })} + component="form" + onSubmit={handleSubmit} + > + + , + }, + ]} + scrollToTop={() => {}} + /> { - setInsertedPropertyTypes([]); - setRemovedPropertyTypes([]); + reset(); }} /> - - - - } - domain="hash.ai" - path={ - <> - theme.palette.blue[70]} - > - {router.query["account-slug"]} - - theme.palette.blue[70]} - > - /types/entity-types/ - - theme.palette.blue[70]} - > - {router.query["entity-type-id"]} - - - } - /> - - ({ - fontSize: 40, - mr: 3, - color: theme.palette.gray[70], - verticalAlign: "middle", - })} + + + + } + domain="hash.ai" + path={ + <> + theme.palette.blue[70]} + > + {router.query["account-slug"]} + + theme.palette.blue[70]} + > + /types/entity-types/ + + theme.palette.blue[70]} + > + {router.query["entity-type-id"]} + + + } /> - {entityType.title} + + ({ + fontSize: 40, + mr: 3, + color: theme.palette.gray[70], + verticalAlign: "middle", + })} + /> + {entityType.title} + + + + + + + + Properties of{" "} + + {entityType.title} + + - - - - Properties of{" "} - - {entityType.title} - - - { - if (insertedPropertyTypes.includes(propertyType)) { - const nextInsertedPropertyTypes = - insertedPropertyTypes.filter( - (type) => type !== propertyType, - ); - setInsertedPropertyTypes(nextInsertedPropertyTypes); - } else if (!removedPropertyTypes.includes(propertyType)) { - setRemovedPropertyTypes([ - ...removedPropertyTypes, - propertyType, - ]); - } - }} - onAddPropertyType={(propertyType) => { - if (!insertedPropertyTypes.includes(propertyType)) { - setInsertedPropertyTypes([ - ...insertedPropertyTypes, - propertyType, - ]); - } - }} - /> - - - + ); }; diff --git a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/edit-bar.tsx b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/edit-bar.tsx index 13fcdcb127e..3ee921f704f 100644 --- a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/edit-bar.tsx +++ b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/edit-bar.tsx @@ -1,10 +1,27 @@ import { faSmile } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@hashintel/hash-design-system/fontawesome-icon"; -import { Box, Container, Stack, Typography } from "@mui/material"; -import { ReactNode } from "react"; +import { Box, Collapse, Container, Stack, Typography } from "@mui/material"; +import { ReactNode, useState } from "react"; +import { useFormContext } from "react-hook-form"; import { PencilSimpleLine } from "../../../../shared/icons/svg"; import { Button, ButtonProps } from "../../../../shared/ui/button"; +import { EntityTypeEditorForm } from "./form-types"; +const useFrozenValue = (value: T): T => { + const { + formState: { isDirty }, + } = useFormContext(); + + const [frozen, setFrozen] = useState(value); + + if (isDirty && frozen !== value) { + setFrozen(value); + } + + return frozen; +}; + +// @todo disabled button styles const EditBarContents = ({ icon, title, @@ -17,43 +34,60 @@ const EditBarContents = ({ label: ReactNode; discardButtonProps: ButtonProps; confirmButtonProps: ButtonProps; -}) => ( - - {icon} - - - {title} - {" "} - {label} - - - - - - -); + "&:hover": { + backgroundColor: theme.palette.blue[80], + color: "white", + }, + })} + disabled={frozenSubmitting} + {...discardButtonProps} + > + {discardButtonProps.children} + + + + + ); +}; export const EditBar = ({ currentVersion, @@ -61,50 +95,53 @@ export const EditBar = ({ }: { currentVersion: number; onDiscardChanges: () => void; -}) => ( - ({ - height: 66, - backgroundColor: theme.palette.blue[70], - color: theme.palette.white, +}) => { + const { + formState: { isDirty }, + } = useFormContext(); + + const frozenVersion = useFrozenValue(currentVersion); - display: "flex", - alignItems: "center", - })} - > - {currentVersion === 0 ? ( - } - title="Currently editing" - label="- this type has not yet been created" - discardButtonProps={{ - // @todo implement this - href: "#", - children: "Discard this type", - }} - confirmButtonProps={{ - onClick() { - // @todo implement - }, - children: "Create", - }} - /> - ) : ( - } - title="Currently editing" - label={`Version ${currentVersion} -> ${currentVersion + 1}`} - discardButtonProps={{ - onClick: onDiscardChanges, - children: "Discard changes", - }} - confirmButtonProps={{ - onClick() { - // @todo implement - }, - children: "Publish update", - }} - /> - )} - -); + return ( + + ({ + height: 66, + backgroundColor: theme.palette.blue[70], + color: theme.palette.white, + display: "flex", + alignItems: "center", + })} + > + {currentVersion === 0 ? ( + } + title="Currently editing" + label="- this type has not yet been created" + discardButtonProps={{ + // @todo implement this + href: "#", + children: "Discard this type", + }} + confirmButtonProps={{ + children: "Create", + }} + /> + ) : ( + } + title="Currently editing" + label={`Version ${frozenVersion} -> ${frozenVersion + 1}`} + discardButtonProps={{ + onClick: onDiscardChanges, + children: "Discard changes", + }} + confirmButtonProps={{ + children: "Publish update", + }} + /> + )} + + + ); +}; diff --git a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/form-types.ts b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/form-types.ts new file mode 100644 index 00000000000..aaf6ab61f18 --- /dev/null +++ b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/form-types.ts @@ -0,0 +1,9 @@ +import { VersionedUri } from "@blockprotocol/type-system-web"; + +type EntityTypeEditorPropertyData = { + $id: VersionedUri; +}; + +export type EntityTypeEditorForm = { + properties: EntityTypeEditorPropertyData[]; +}; diff --git a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-expected-values.tsx b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-expected-values.tsx index 4595c30fff6..932d54057fd 100644 --- a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-expected-values.tsx +++ b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-expected-values.tsx @@ -1,6 +1,16 @@ import { PropertyType } from "@blockprotocol/type-system-web"; import { Chip } from "@hashintel/hash-design-system/chip"; -import { dataTypeNames } from "./use-property-types"; + +const dataTypeNames = { + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1": "Text", + "https://blockprotocol.org/@blockprotocol/types/data-type/number/v/1": + "Number", + "https://blockprotocol.org/@blockprotocol/types/data-type/boolean/v/1": + "Boolean", + "https://blockprotocol.org/@blockprotocol/types/data-type/null/v/1": "Null", + "https://blockprotocol.org/@blockprotocol/types/data-type/object/v/1": + "JSON Object", +} as Record; // @todo handle this being too many export const PropertyExpectedValues = ({ diff --git a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-list-card.tsx b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-list-card.tsx index 1dc5944107e..d428c4de9d7 100644 --- a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-list-card.tsx +++ b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-list-card.tsx @@ -30,8 +30,10 @@ import { usePopupState, } from "material-ui-popup-state/hooks"; import { Ref, useId, useRef, useState } from "react"; +import { useFieldArray, useFormContext } from "react-hook-form"; import { Modal } from "../../../../components/Modals/Modal"; import { EmptyPropertyListCard } from "./empty-property-list-card"; +import { EntityTypeEditorForm } from "./form-types"; import { PropertyExpectedValues } from "./property-expected-values"; import { PropertyListSelectorDropdownContext } from "./property-list-selector-dropdown"; import { PropertyMenu } from "./property-menu"; @@ -39,7 +41,8 @@ import { PropertySelector } from "./property-selector"; import { PropertyTypeForm } from "./property-type-form"; import { QuestionIcon } from "./question-icon"; import { StyledPlusCircleIcon } from "./styled-plus-circle-icon"; -import { useStateCallback, withHandler } from "./util"; +import { usePropertyTypes } from "./use-property-types"; +import { mustBeVersionedUri, useStateCallback, withHandler } from "./util"; import { WhiteCard } from "./white-card"; const CenteredTableCell = styled(TableCell)( @@ -53,12 +56,10 @@ const InsertPropertyRow = ({ inputRef, onCancel, onAdd, - filterProperty, }: { inputRef: Ref; onCancel: () => void; onAdd: (option: PropertyType) => void; - filterProperty: (property: PropertyType) => boolean; }) => { const modalTooltipId = useId(); const modalPopupState = usePopupState({ @@ -71,6 +72,9 @@ const InsertPropertyRow = ({ const ourInputRef = useRef(null); const sharedRef = useForkRef(inputRef, ourInputRef); + const { watch } = useFormContext(); + const properties = watch("properties"); + return ( + !properties.some( + (includedProperty) => includedProperty.$id === property.$id, + ) + } /> void; -}) => ( - - - - {property.title} - - - - - - - - - - - - - { + const { watch } = useFormContext(); + const propertyTypes = usePropertyTypes(); + const propertyId = mustBeVersionedUri( + watch(`properties.${propertyIndex}.$id`), + ); + const property = propertyTypes[propertyId]; + + if (!property) { + throw new Error("Missing property type"); + } + + return ( + + + + {property.title} + + + + + + + + + + + + + - - - - - -); + }} + inputProps={{ sx: { textOverflow: "ellipsis" } }} + /> + + + + + + ); +}; + +export const PropertyListCard = () => { + const { control, getValues } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control, + name: "properties", + }); -export const PropertyListCard = ({ - propertyTypes, - onAddPropertyType, - onRemovePropertyType, -}: { - propertyTypes: PropertyType[]; - onAddPropertyType: (type: PropertyType) => void; - onRemovePropertyType: (type: PropertyType) => void; -}) => { const [addingNewProperty, setAddingNewProperty] = useStateCallback(false); const addingNewPropertyRef = useRef(null); - if (!addingNewProperty && propertyTypes.length === 0) { + if (!addingNewProperty && fields.length === 0) { return ( { @@ -307,12 +326,12 @@ export const PropertyListCard = ({ - {propertyTypes.map((type) => ( + {fields.map((type, index) => ( { - onRemovePropertyType(type); + remove(index); }} /> ))} @@ -326,9 +345,14 @@ export const PropertyListCard = ({ }} onAdd={(type) => { setAddingNewProperty(false); - onAddPropertyType(type); + if ( + !getValues("properties").some(({ $id }) => $id === type.$id) + ) { + append({ + $id: mustBeVersionedUri(type.$id), + }); + } }} - filterProperty={(property) => !propertyTypes.includes(property)} /> ) : ( diff --git a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-menu.tsx b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-menu.tsx index f4b81f2648c..b36e6f5a936 100644 --- a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-menu.tsx +++ b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-menu.tsx @@ -1,8 +1,4 @@ -import { - extractVersion, - PropertyType, - validateVersionedUri, -} from "@blockprotocol/type-system-web"; +import { extractVersion, PropertyType } from "@blockprotocol/type-system-web"; import { faEllipsis } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@hashintel/hash-design-system/fontawesome-icon"; import { IconButton } from "@hashintel/hash-design-system/icon-button"; @@ -26,12 +22,7 @@ import { } from "material-ui-popup-state/hooks"; import { Fragment, useId } from "react"; import { OntologyChip, parseUriForOntologyChip } from "./ontology-chip"; - -const parseVersion = (id: string) => { - const uri = validateVersionedUri(id); - - return uri.type === "Ok" ? extractVersion(uri.inner) : null; -}; +import { mustBeVersionedUri } from "./util"; export const PropertyMenu = ({ onRemove, @@ -47,7 +38,7 @@ export const PropertyMenu = ({ popupId: `property-${id}`, }); - const version = parseVersion(property.$id); + const version = extractVersion(mustBeVersionedUri(property.$id)); const ontology = parseUriForOntologyChip(property.$id); return ( diff --git a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-selector.tsx b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-selector.tsx index 418690b3be3..86a4d6d1e28 100644 --- a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-selector.tsx +++ b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/property-selector.tsx @@ -46,7 +46,8 @@ const PropertySelector: ForwardRefRenderFunction< }, ref, ) => { - const propertyTypes = usePropertyTypes(); + const propertyTypesObj = usePropertyTypes(); + const propertyTypes = Object.values(propertyTypesObj); const modifiers = useMemo( (): PopperProps["modifiers"] => [ @@ -72,14 +73,8 @@ const PropertySelector: ForwardRefRenderFunction< ); const [open, setOpen] = useState(false); - const highlightedRef = useRef(null); - if (!propertyTypes) { - // @todo loading indicator - return null; - } - return ( )} - options={propertyTypes.filter((type) => filterProperty(type))} + options={ + // @todo make this more efficient + propertyTypes.filter((type) => filterProperty(type)) + } getOptionLabel={(obj) => obj.title} renderOption={(props, property: PropertyType) => { const ontology = parseUriForOntologyChip(property.$id); diff --git a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/use-entity-type.tsx b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/use-entity-type.tsx new file mode 100644 index 00000000000..dd01d7f2f09 --- /dev/null +++ b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/use-entity-type.tsx @@ -0,0 +1,90 @@ +import { EntityType, extractBaseUri } from "@blockprotocol/type-system-web"; +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; +import { useBlockProtocolAggregateEntityTypes } from "../../../../components/hooks/blockProtocolFunctions/ontology/useBlockProtocolAggregateEntityTypes"; +import { useBlockProtocolUpdateEntityType } from "../../../../components/hooks/blockProtocolFunctions/ontology/useBlockProtocolUpdateEntityType"; +import { useAdvancedInitTypeSystem } from "../../../../lib/use-init-type-system"; +import { mustBeVersionedUri } from "./util"; + +export const useEntityType = ( + entityTypeBaseUri: string, + onCompleted?: (entityType: EntityType) => void, +) => { + const [typeSystemLoading, loadTypeSystem] = useAdvancedInitTypeSystem(); + + const [entityType, setEntityType] = useState(null); + const entityTypeRef = useRef(entityType); + + const onCompletedRef = useRef(onCompleted); + useLayoutEffect(() => { + onCompletedRef.current = onCompleted; + }); + + const { aggregateEntityTypes } = useBlockProtocolAggregateEntityTypes(); + const { updateEntityType } = useBlockProtocolUpdateEntityType(); + + useEffect(() => { + let cancelled = false; + + setEntityType(null); + entityTypeRef.current = null; + + void aggregateEntityTypes({ data: {} }).then(async (res) => { + const relevantEntity = + res.data?.results.find((item) => { + const baseUri = extractBaseUri(mustBeVersionedUri(item.entityTypeId)); + return baseUri === entityTypeBaseUri; + })?.entityType ?? null; + + await loadTypeSystem(); + + if (!cancelled) { + setEntityType(relevantEntity); + entityTypeRef.current = relevantEntity; + if (relevantEntity) { + onCompletedRef.current?.(relevantEntity); + } + } + }); + + return () => { + cancelled = true; + }; + }, [aggregateEntityTypes, entityTypeBaseUri, loadTypeSystem]); + + const updateCallback = useCallback( + async (partialEntityType: Partial>) => { + if (!entityTypeRef.current) { + throw new Error("Cannot update yet"); + } + + const currentEntity = entityTypeRef.current; + const { $id, ...restOfEntityType } = currentEntity; + + const res = await updateEntityType({ + data: { + entityTypeId: $id, + entityType: { + ...restOfEntityType, + ...partialEntityType, + }, + }, + }); + + if (entityTypeRef.current === currentEntity && res.data) { + setEntityType(res.data.entityType); + entityTypeRef.current = res.data.entityType; + } + + return res; + }, + [updateEntityType], + ); + + return [typeSystemLoading ? null : entityType, updateCallback] as const; +}; diff --git a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/use-property-types.tsx b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/use-property-types.tsx index 4d751fd3355..c2ceccf82e5 100644 --- a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/use-property-types.tsx +++ b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/use-property-types.tsx @@ -1,36 +1,46 @@ -import { PropertyType } from "@blockprotocol/type-system-web"; +import { PropertyType, VersionedUri } from "@blockprotocol/type-system-web"; import { createContext, useContext, useEffect, useState } from "react"; import { useBlockProtocolAggregatePropertyTypes } from "../../../../components/hooks/blockProtocolFunctions/ontology/useBlockProtocolAggregatePropertyTypes"; +import { mustBeVersionedUri } from "./util"; -export const dataTypeNames = { - "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1": "Text", - "https://blockprotocol.org/@blockprotocol/types/data-type/number/v/1": - "Number", - "https://blockprotocol.org/@blockprotocol/types/data-type/boolean/v/1": - "Boolean", - "https://blockprotocol.org/@blockprotocol/types/data-type/null/v/1": "Null", - "https://blockprotocol.org/@blockprotocol/types/data-type/object/v/1": - "JSON Object", -} as Record; +type PropertyTypesContextValues = Record; export const useRemotePropertyTypes = () => { - const [propertyTypes, setPropertyTypes] = useState( - null, - ); + const [propertyTypes, setPropertyTypes] = + useState(null); const { aggregatePropertyTypes } = useBlockProtocolAggregatePropertyTypes(); useEffect(() => { void aggregatePropertyTypes({ data: {} }).then((data) => { // @todo error handling - setPropertyTypes( - data.data?.results.map((result) => result.propertyType) ?? null, - ); + if (data.data) { + setPropertyTypes( + Object.fromEntries( + data.data.results.map( + (result) => + [ + mustBeVersionedUri(result.propertyTypeId), + result.propertyType, + ] as const, + ), + ), + ); + } }); }, [aggregatePropertyTypes]); return propertyTypes; }; -export const PropertyTypesContext = createContext(null); +export const PropertyTypesContext = + createContext(null); -export const usePropertyTypes = () => useContext(PropertyTypesContext); +export const usePropertyTypes = () => { + const types = useContext(PropertyTypesContext); + + if (!types) { + throw new Error("Property types not loaded yet"); + } + + return types; +}; diff --git a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/util.ts b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/util.ts index 72aa8c63368..ef26e15c4d5 100644 --- a/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/util.ts +++ b/packages/hash/frontend/src/pages/[account-slug]/types/entity-type/util.ts @@ -1,3 +1,4 @@ +import { validateVersionedUri } from "@blockprotocol/type-system-web"; import { bindToggle, bindTrigger } from "material-ui-popup-state/hooks"; import { SetStateAction, useLayoutEffect, useRef, useState } from "react"; @@ -51,3 +52,14 @@ export const withHandler = < }, }; }; + +/** + * Necessary as type system isn't fully correctly typed yet + */ +export const mustBeVersionedUri = (uri: string) => { + const validatedId = validateVersionedUri(uri); + if (validatedId.type === "Err") { + throw new Error("uri not versioned"); + } + return validatedId.inner; +};