Skip to content

Commit

Permalink
Entity type editor – persist changes, and related updates (#1194)
Browse files Browse the repository at this point in the history
  • Loading branch information
nathggns committed Oct 13, 2022
1 parent 2646115 commit d7c2bdf
Show file tree
Hide file tree
Showing 11 changed files with 530 additions and 350 deletions.
31 changes: 22 additions & 9 deletions 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<Promise<void> | 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];
@@ -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<EntityType | null>(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<EntityTypeEditorForm>({
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 (
<PropertyTypesContext.Provider value={propertyTypes}>
<Box
component={Stack}
sx={(theme) => ({
minHeight: "100vh",
background: theme.palette.gray[10],
})}
>
<Box bgcolor="white" borderBottom={1} borderColor="gray.20">
<TopContextBar
defaultCrumbIcon={null}
crumbs={[
{
title: "Types",
href: "#",
id: "types",
},
{
title: "Entity types",
href: "#",
id: "entity-types",
},
{
title: entityType.title,
href: "#",
id: entityType.$id,
icon: <FontAwesomeIcon icon={faAsterisk} />,
},
]}
scrollToTop={() => {}}
/>
<Collapse
in={insertedPropertyTypes.length + removedPropertyTypes.length > 0}
>
<FormProvider {...formMethods}>
<Box
sx={(theme) => ({
minHeight: "100vh",
background: theme.palette.gray[10],
display: "flex",
flexDirection: "column",
})}
component="form"
onSubmit={handleSubmit}
>
<Box bgcolor="white" borderBottom={1} borderColor="gray.20">
<TopContextBar
defaultCrumbIcon={null}
crumbs={[
{
title: "Types",
href: "#",
id: "types",
},
{
title: "Entity types",
href: "#",
id: "entity-types",
},
{
title: entityType.title,
href: "#",
id: entityType.$id,
icon: <FontAwesomeIcon icon={faAsterisk} />,
},
]}
scrollToTop={() => {}}
/>
<EditBar
currentVersion={currentVersion}
onDiscardChanges={() => {
setInsertedPropertyTypes([]);
setRemovedPropertyTypes([]);
reset();
}}
/>
</Collapse>
<Box pt={3.75}>
<Container>
<OntologyChip
icon={<HashOntologyIcon />}
domain="hash.ai"
path={
<>
<Typography
component="span"
fontWeight="bold"
color={(theme) => theme.palette.blue[70]}
>
{router.query["account-slug"]}
</Typography>
<Typography
component="span"
color={(theme) => theme.palette.blue[70]}
>
/types/entity-types/
</Typography>
<Typography
component="span"
fontWeight="bold"
color={(theme) => theme.palette.blue[70]}
>
{router.query["entity-type-id"]}
</Typography>
</>
}
/>
<Typography variant="h1" fontWeight="bold" mt={3} mb={4.5}>
<FontAwesomeIcon
icon={faAsterisk}
sx={(theme) => ({
fontSize: 40,
mr: 3,
color: theme.palette.gray[70],
verticalAlign: "middle",
})}

<Box pt={3.75}>
<Container>
<OntologyChip
icon={<HashOntologyIcon />}
domain="hash.ai"
path={
<>
<Typography
component="span"
fontWeight="bold"
color={(theme) => theme.palette.blue[70]}
>
{router.query["account-slug"]}
</Typography>
<Typography
component="span"
color={(theme) => theme.palette.blue[70]}
>
/types/entity-types/
</Typography>
<Typography
component="span"
fontWeight="bold"
color={(theme) => theme.palette.blue[70]}
>
{router.query["entity-type-id"]}
</Typography>
</>
}
/>
{entityType.title}
<Typography variant="h1" fontWeight="bold" mt={3} mb={4.5}>
<FontAwesomeIcon
icon={faAsterisk}
sx={(theme) => ({
fontSize: 40,
mr: 3,
color: theme.palette.gray[70],
verticalAlign: "middle",
})}
/>
{entityType.title}
</Typography>
</Container>
</Box>
</Box>
<Box py={5}>
<Container>
<Typography variant="h5" mb={1.25}>
Properties of{" "}
<Box component="span" sx={{ fontWeight: "bold" }}>
{entityType.title}
</Box>
</Typography>
<PropertyListCard />
</Container>
</Box>
</Box>
<Box py={5}>
<Container>
<Typography variant="h5" mb={1.25}>
Properties of{" "}
<Box component="span" sx={{ fontWeight: "bold" }}>
{entityType.title}
</Box>
</Typography>
<PropertyListCard
propertyTypes={insertedPropertyTypes}
onRemovePropertyType={(propertyType) => {
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,
]);
}
}}
/>
</Container>
</Box>
</Box>
</FormProvider>
</PropertyTypesContext.Provider>
);
};
Expand Down

0 comments on commit d7c2bdf

Please sign in to comment.