From 3c555c871e0e9c4ccad89dbd6a4c92c321f3fe03 Mon Sep 17 00:00:00 2001 From: Ciaran Morinan Date: Thu, 5 Dec 2024 19:39:47 +0000 Subject: [PATCH] finish updating entity editor for dataTypeId in metadata --- .../entities/[entity-uuid].page.tsx | 3 +- .../[entity-uuid].page/create-entity-page.tsx | 51 ++- .../edit-entity-slide-over.tsx | 1 + .../history-section/get-history-events.ts | 7 +- .../property-table/cells/change-type-cell.tsx | 1 + .../cells/value-cell/array-editor.tsx | 44 ++- .../value-cell/array-editor/draft-row.tsx | 6 +- .../value-cell/array-editor/sortable-row.tsx | 15 +- .../value-cell/array-editor/value-chip.tsx | 3 +- .../cells/value-cell/single-value-editor.tsx | 49 ++- .../property-table/types.ts | 22 +- .../use-create-get-cell-content.ts | 7 +- .../use-create-on-cell-edited.ts | 13 +- .../generate-property-row-recursively.ts | 8 +- .../use-rows/use-property-rows-from-entity.ts | 85 +++-- .../shared/create-draft-entity-subgraph.ts | 12 +- .../@local/graph/sdk/typescript/src/entity.ts | 300 +++++++++++++----- .../graph/types/typescript/src/entity.ts | 1 + 18 files changed, 476 insertions(+), 152 deletions(-) diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page.tsx index 19e3ef3715e..8b66ccc5530 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page.tsx @@ -337,7 +337,7 @@ const Page: NextPageWithLayout = () => { oldProperties: entityFromDb?.properties ?? {}, newProperties: mergePropertyObjectAndMetadata( overrideProperties ?? draftEntity.properties, - undefined, + draftEntity.metadata.properties, ), }), }, @@ -457,6 +457,7 @@ const Page: NextPageWithLayout = () => { omitProperties: [], }), ); + setIsDirty(true); }} /> ); diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/create-entity-page.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/create-entity-page.tsx index 92bfac86227..1be0bb57c63 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/create-entity-page.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/create-entity-page.tsx @@ -1,5 +1,10 @@ +import { useMutation } from "@apollo/client"; import type { VersionedUrl } from "@blockprotocol/type-system"; import { AlertModal } from "@hashintel/design-system"; +import { + Entity, + mergePropertyObjectAndMetadata, +} from "@local/hash-graph-sdk/entity"; import type { PropertyObject } from "@local/hash-graph-types/entity"; import { generateEntityLabel } from "@local/hash-isomorphic-utils/generate-entity-label"; import { blockProtocolEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; @@ -10,9 +15,17 @@ import { Typography } from "@mui/material"; import { useRouter } from "next/router"; import { useCallback, useContext, useEffect, useState } from "react"; -import { useBlockProtocolCreateEntity } from "../../../../components/hooks/block-protocol-functions/knowledge/use-block-protocol-create-entity"; import { PageErrorState } from "../../../../components/page-error-state"; +import type { + CreateEntityMutation, + CreateEntityMutationVariables, +} from "../../../../graphql/api-types.gen"; +import { + createEntityMutation, + getEntitySubgraphQuery, +} from "../../../../graphql/queries/knowledge/entity.queries"; import { Link } from "../../../../shared/ui/link"; +import { generateUseEntityTypeEntitiesQueryVariables } from "../../../../shared/use-entity-type-entities"; import { useGetClosedMultiEntityType } from "../../../shared/use-get-closed-multi-entity-type"; import { WorkspaceContext } from "../../../shared/workspace-context"; import { EditBar } from "../../shared/edit-bar"; @@ -79,9 +92,25 @@ export const CreateEntityPage = ({ entityTypeId }: CreateEntityPageProps) => { const { activeWorkspace, activeWorkspaceOwnedById } = useContext(WorkspaceContext); - const { createEntity } = useBlockProtocolCreateEntity( - activeWorkspaceOwnedById ?? null, - ); + + const [createEntity] = useMutation< + CreateEntityMutation, + CreateEntityMutationVariables + >(createEntityMutation, { + refetchQueries: [ + /** + * This refetch query accounts for the "Entities" section + * in the sidebar being updated when the first instance of + * a type is created by a user that is from a different web. + */ + { + query: getEntitySubgraphQuery, + variables: generateUseEntityTypeEntitiesQueryVariables({ + ownedById: activeWorkspaceOwnedById, + }), + }, + ], + }); const [creating, setCreating] = useState(false); @@ -110,13 +139,21 @@ export const CreateEntityPage = ({ entityTypeId }: CreateEntityPageProps) => { try { setCreating(true); - const { data: createdEntity } = await createEntity({ - data: { + const { data } = await createEntity({ + variables: { entityTypeIds: entity.metadata.entityTypeIds, - properties: overrideProperties ?? draftEntity.properties, + ownedById: activeWorkspaceOwnedById, + properties: mergePropertyObjectAndMetadata( + overrideProperties ?? draftEntity.properties, + draftEntity.metadata.properties, + ), }, }); + const createdEntity = data?.createEntity + ? new Entity(data.createEntity) + : null; + if (!createdEntity) { return; } 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 897044abae6..aa228c730cd 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 @@ -601,6 +601,7 @@ const EditEntitySlideOver = memo( omitProperties: [], }), ); + setIsDirty(true); }} isDirty={isDirty} onEntityClick={onEntityClick} diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/history-section/get-history-events.ts b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/history-section/get-history-events.ts index d32e642543b..dba4cd30d70 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/history-section/get-history-events.ts +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/history-section/get-history-events.ts @@ -2,11 +2,8 @@ import { extractBaseUrl, type VersionedUrl } from "@blockprotocol/type-system"; import { extractVersion } from "@blockprotocol/type-system/slim"; import { typedEntries } from "@local/advanced-types/typed-entries"; import type { EntityTypeIdDiff } from "@local/hash-graph-client"; -import { - EntityId, - isValueMetadata, - PropertyPath, -} from "@local/hash-graph-types/entity"; +import type { EntityId, PropertyPath } from "@local/hash-graph-types/entity"; +import { isValueMetadata } from "@local/hash-graph-types/entity"; import type { BaseUrl, EntityTypeWithMetadata, diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/change-type-cell.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/change-type-cell.tsx index cc2603c3946..495fd5578fb 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/change-type-cell.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/change-type-cell.tsx @@ -115,6 +115,7 @@ export const createRenderChangeTypeCell = ( const newContent = produce(valueCellOfThisRow, (draft) => { draft.data.propertyRow.value = undefined; + draft.data.propertyRow.valueMetadata = undefined; }); /** diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor.tsx index 2ebd7aa9730..42415a8f202 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor.tsx @@ -1,3 +1,4 @@ +import type { VersionedUrl } from "@blockprotocol/type-system/slim"; import type { DragEndEvent } from "@dnd-kit/core"; import { closestCenter, @@ -54,8 +55,9 @@ export const ArrayEditor: ValueCellEditorComponent = ({ const { value: propertyValue, valueMetadata, + generateNewMetadataObject, permittedDataTypes, - setPropertyMetadata, + propertyKeyChain, maxItems, minItems, } = cell.data.propertyRow; @@ -146,14 +148,22 @@ export const ArrayEditor: ValueCellEditorComponent = ({ setSelectedRow((prevId) => (id === prevId ? "" : id)); }; - const addItem = (value: unknown) => { + const addItem = (value: unknown, dataTypeId: VersionedUrl) => { setEditingRow(""); + const { propertyMetadata } = generateNewMetadataObject({ + propertyKeyChain, + valuePath: [...propertyKeyChain, items.length], + valueMetadata: { metadata: { dataTypeId } }, + }); + const newCell = produce(cell, (draftCell) => { draftCell.data.propertyRow.value = [ ...items.map((item) => item.value), value, ]; + + draftCell.data.propertyRow.valueMetadata = propertyMetadata; }); onChange(newCell); @@ -166,11 +176,20 @@ export const ArrayEditor: ValueCellEditorComponent = ({ }; const removeItem = (indexToRemove: number) => { + const { propertyMetadata } = generateNewMetadataObject({ + propertyKeyChain, + valuePath: [...propertyKeyChain, indexToRemove], + valueMetadata: "delete", + }); + const newCell = produce(cell, (draftCell) => { draftCell.data.propertyRow.value = items .filter((_, index) => indexToRemove !== index) .map(({ value }) => value); + + draftCell.data.propertyRow.valueMetadata = propertyMetadata; }); + onChange(newCell); }; @@ -192,6 +211,25 @@ export const ArrayEditor: ValueCellEditorComponent = ({ const newItems = arrayMove(items, oldIndex, newIndex); draftCell.data.propertyRow.value = newItems.map(({ value }) => value); + + if (!valueMetadata) { + throw new Error( + "Expected valueMetadata to be set when there are values", + ); + } + + if (!isArrayMetadata(valueMetadata)) { + throw new Error( + `Expected array metadata for value '${JSON.stringify(newItems)}', got ${JSON.stringify(valueMetadata)}`, + ); + } + + const newMetadata = arrayMove(valueMetadata.value, oldIndex, newIndex); + + draftCell.data.propertyRow.valueMetadata = { + ...valueMetadata, + value: newMetadata, + }; }); onChange(newCell); }; @@ -233,7 +271,7 @@ export const ArrayEditor: ValueCellEditorComponent = ({ // add the value on click instead of showing draftRow if (onlyOneExpectedType && noEditMode) { - return addItem(editorSpec.defaultValue); + return addItem(editorSpec.defaultValue, expectedType.$id); } setEditingRow(DRAFT_ROW_KEY); diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/draft-row.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/draft-row.tsx index 126eac66ebd..904c0a08626 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/draft-row.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/draft-row.tsx @@ -1,4 +1,4 @@ -import type { ClosedDataType } from "@blockprotocol/type-system"; +import type { ClosedDataType, VersionedUrl } from "@blockprotocol/type-system"; import { useState } from "react"; import { DRAFT_ROW_KEY } from "../array-editor"; @@ -9,7 +9,7 @@ import { SortableRow } from "./sortable-row"; interface DraftRowProps { expectedTypes: ClosedDataType[]; existingItemCount: number; - onDraftSaved: (value: unknown) => void; + onDraftSaved: (value: unknown, dataTypeId: VersionedUrl) => void; onDraftDiscarded: () => void; } @@ -56,7 +56,7 @@ export const DraftRow = ({ return onDraftDiscarded(); } - onDraftSaved(value); + onDraftSaved(value, dataType.$id); }} onDiscardChanges={onDraftDiscarded} expectedTypes={expectedTypes} diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/sortable-row.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/sortable-row.tsx index 7a97e8e711c..a1c356e78b7 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/sortable-row.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/sortable-row.tsx @@ -13,7 +13,7 @@ import { getMergedDataTypeSchema, } from "@local/hash-isomorphic-utils/data-types"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; -import { Box, Divider, Typography } from "@mui/material"; +import { Box, Divider, Stack, Typography } from "@mui/material"; import { useRef, useState } from "react"; import { getEditorSpecs } from "../editor-specs"; @@ -148,9 +148,16 @@ export const SortableRow = ({ return ( part.text) - .join("")} + title={ + + {formatDataValue(value as JsonValue, schema).map((part, idx) => ( + // eslint-disable-next-line react/no-array-index-key + + {part.text} + + ))} + + } selected={!!selected} icon={{ icon: editorSpec.icon }} tooltip={dataType.title} diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/value-chip.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/value-chip.tsx index 5cbd3194d06..222e50d18fc 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/value-chip.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/array-editor/value-chip.tsx @@ -2,6 +2,7 @@ import type { IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { faAsterisk } from "@fortawesome/free-solid-svg-icons"; import { Chip, FontAwesomeIcon } from "@hashintel/design-system"; import { Box, Tooltip } from "@mui/material"; +import type { ReactNode } from "react"; export const ValueChip = ({ title, @@ -10,7 +11,7 @@ export const ValueChip = ({ selected, tooltip = "", }: { - title: string; + title: ReactNode; icon?: Pick; imageSrc?: string; selected: boolean; diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/single-value-editor.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/single-value-editor.tsx index e6174c91405..a72494cf3d0 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/single-value-editor.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/single-value-editor.tsx @@ -1,6 +1,7 @@ import type { ClosedDataType } from "@blockprotocol/type-system"; import { Chip } from "@hashintel/design-system"; import { GRID_CLICK_IGNORE_CLASS } from "@hashintel/design-system/constants"; +import type { PropertyMetadataValue } from "@local/hash-graph-types/entity"; import { isValueMetadata } from "@local/hash-graph-types/entity"; import type { MergedDataTypeSingleSchema } from "@local/hash-isomorphic-utils/data-types"; import { getMergedDataTypeSchema } from "@local/hash-isomorphic-utils/data-types"; @@ -19,9 +20,9 @@ import type { ValueCell, ValueCellEditorComponent } from "./types"; export const SingleValueEditor: ValueCellEditorComponent = (props) => { const { value: cell, onChange, onFinishedEditing } = props; const { + generateNewMetadataObject, permittedDataTypes, propertyKeyChain, - setPropertyMetadata, value, valueMetadata, } = cell.data.propertyRow; @@ -86,21 +87,42 @@ export const SingleValueEditor: ValueCellEditorComponent = (props) => { }); useEffect(() => { - if (chosenDataType) { - setPropertyMetadata(propertyKeyChain, { - metadata: { - dataTypeId: chosenDataType.dataType.$id, + if ( + chosenDataType && + (valueMetadata as PropertyMetadataValue | undefined)?.metadata + .dataTypeId !== chosenDataType.dataType.$id + ) { + const { propertyMetadata } = generateNewMetadataObject({ + propertyKeyChain, + valuePath: propertyKeyChain, + valueMetadata: { + metadata: { + dataTypeId: chosenDataType.dataType.$id, + }, }, }); + + const newCell = produce(cell, (draftCell) => { + draftCell.data.propertyRow.valueMetadata = propertyMetadata; + }); + + onChange(newCell); } - }, [chosenDataType, propertyKeyChain, setPropertyMetadata]); + }, [ + cell, + chosenDataType, + generateNewMetadataObject, + onChange, + propertyKeyChain, + valueMetadata, + ]); const latestValueCellRef = useRef(cell); useEffect(() => { latestValueCellRef.current = cell; }); - if (!chosenDataType) { + if (!chosenDataType || !cell.data.propertyRow.valueMetadata) { return ( { const editorSpec = getEditorSpecs(type, schema); + setChosenDataType({ + dataType: type, + schema, + }); + // if no edit mode supported for selected type, set the default value and close the editor if (editorSpec.arrayEditException === "no-edit-mode") { const newCell = produce(cell, (draftCell) => { draftCell.data.propertyRow.value = editorSpec.defaultValue; + draftCell.data.propertyRow.valueMetadata = { + metadata: { dataTypeId: type.$id }, + }; }); return onFinishedEditing(newCell); } - - setChosenDataType({ - dataType: type, - schema, - }); }} /> diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/types.ts b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/types.ts index c5893855089..c6d6453dc60 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/types.ts +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/types.ts @@ -1,8 +1,9 @@ import type { ClosedDataType } from "@blockprotocol/type-system"; import type { SizedGridColumn } from "@glideapps/glide-data-grid"; -import type { Entity } from "@local/hash-graph-sdk/entity"; import type { PropertyMetadata, + PropertyMetadataObject, + PropertyMetadataValue, PropertyPath, } from "@local/hash-graph-types/entity"; @@ -11,6 +12,24 @@ import type { VerticalIndentationLineDir } from "../../../../../../../components export type PropertyRow = { children: PropertyRow[]; depth: number; + generateNewMetadataObject: (args: { + /** + * The path to the property in the entity's properties (i.e. row.propertyKeyChain) + */ + propertyKeyChain: PropertyPath; + /** + * The path to the leaf value in the entity's properties, + * which will start with propertyKeyChain, but may have additional array indices (depending on the property's structure) + */ + valuePath: PropertyPath; + /** + * The metadata to set for the leaf value + */ + valueMetadata: PropertyMetadataValue | "delete"; + }) => { + entityPropertiesMetadata: PropertyMetadataObject; + propertyMetadata: PropertyMetadata | undefined; + }; indent: number; isArray: boolean; isSingleUrl: boolean; @@ -20,7 +39,6 @@ export type PropertyRow = { propertyKeyChain: PropertyPath; required: boolean; rowId: string; - setPropertyMetadata: Entity["setPropertyMetadata"]; title: string; value: unknown; valueMetadata?: PropertyMetadata; diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-create-get-cell-content.ts b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-create-get-cell-content.ts index f90251aba2f..546c4621887 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-create-get-cell-content.ts +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-create-get-cell-content.ts @@ -64,10 +64,13 @@ export const useCreateGetCellContent = ( }, }; - const { isArray, permittedDataTypes, valueMetadata } = row; + const { isArray, permittedDataTypes, value, valueMetadata } = row; const shouldShowChangeTypeCell = - permittedDataTypes.length > 1 && !isArray && !readonly; + permittedDataTypes.length > 1 && + !isArray && + typeof value !== "undefined" && + !readonly; switch (columnKey) { case "title": diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-create-on-cell-edited.ts b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-create-on-cell-edited.ts index aea275cdac9..06b671ffc37 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-create-on-cell-edited.ts +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-create-on-cell-edited.ts @@ -26,7 +26,7 @@ export const useCreateOnCellEdited = () => { return; } - const valueCell = newValue as ValueCell; + const newValueCell = newValue as ValueCell; const key = propertyGridIndexes[colIndex]; const row = rows[rowIndex]; @@ -42,6 +42,8 @@ export const useCreateOnCellEdited = () => { const updatedProperties = cloneDeep(entity.properties); + const updatedMetadata = cloneDeep(entity.metadata); + const { propertyKeyChain } = row; /** @@ -52,12 +54,19 @@ export const useCreateOnCellEdited = () => { set( updatedProperties, propertyKeyChain, - valueCell.data.propertyRow.value, + newValueCell.data.propertyRow.value, + ); + + set( + updatedMetadata, + ["properties", "value", ...propertyKeyChain], + newValueCell.data.propertyRow.valueMetadata, ); setEntity( new Entity({ ...entity.toJSON(), + metadata: updatedMetadata, properties: updatedProperties, }), ); diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-rows/generate-property-rows-from-entity/generate-property-row-recursively.ts b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-rows/generate-property-rows-from-entity/generate-property-row-recursively.ts index 38cb07bdd00..442018aaeb0 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-rows/generate-property-rows-from-entity/generate-property-row-recursively.ts +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-rows/generate-property-rows-from-entity/generate-property-row-recursively.ts @@ -53,23 +53,23 @@ import { getExpectedTypesOfPropertyType } from "./get-expected-types-of-property export const generatePropertyRowRecursively = ({ closedMultiEntityType, closedMultiEntityTypesDefinitions, + generateNewMetadataObject, propertyTypeBaseUrl, propertyKeyChain, entity, requiredPropertyTypes, depth = 0, propertyRefSchema, - setPropertyMetadata, }: { closedMultiEntityType: ClosedMultiEntityType; closedMultiEntityTypesDefinitions: ClosedMultiEntityTypesDefinitions; + generateNewMetadataObject: PropertyRow["generateNewMetadataObject"]; propertyTypeBaseUrl: BaseUrl; propertyKeyChain: BaseUrl[]; entity: Entity; requiredPropertyTypes: BaseUrl[]; depth?: number; propertyRefSchema: ValueOrArray; - setPropertyMetadata: Entity["setPropertyMetadata"]; }): PropertyRow => { const propertyTypeId = "$ref" in propertyRefSchema @@ -127,6 +127,7 @@ export const generatePropertyRowRecursively = ({ generatePropertyRowRecursively({ closedMultiEntityType, closedMultiEntityTypesDefinitions, + generateNewMetadataObject, propertyTypeBaseUrl: subPropertyTypeBaseUrl as BaseUrl, propertyKeyChain: [ ...propertyKeyChain, @@ -138,7 +139,6 @@ export const generatePropertyRowRecursively = ({ requiredPropertyTypes, depth: depth + 1, propertyRefSchema: subPropertyRefSchema, - setPropertyMetadata, }), ); } @@ -181,6 +181,7 @@ export const generatePropertyRowRecursively = ({ ...minMaxConfig, children, depth, + generateNewMetadataObject, indent, isArray, isSingleUrl, @@ -188,7 +189,6 @@ export const generatePropertyRowRecursively = ({ propertyKeyChain, required, rowId, - setPropertyMetadata, title: propertyType.title, value, valueMetadata, diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-rows/use-property-rows-from-entity.ts b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-rows/use-property-rows-from-entity.ts index ae644a26d4d..9b83d9e5994 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-rows/use-property-rows-from-entity.ts +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/use-rows/use-property-rows-from-entity.ts @@ -1,31 +1,72 @@ import { typedKeys } from "@local/advanced-types/typed-entries"; +import { + Entity, + generateChangedPropertyMetadataObject, +} from "@local/hash-graph-sdk/entity"; +import type { + PropertyMetadataObject, + PropertyMetadataValue, + PropertyPath, +} from "@local/hash-graph-types/entity"; import type { BaseUrl } from "@local/hash-graph-types/ontology"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; import { useEntityEditor } from "../../../entity-editor-context"; import type { PropertyRow } from "../types"; import { generatePropertyRowRecursively } from "./generate-property-rows-from-entity/generate-property-row-recursively"; -import { - PropertyMetadataValue, - PropertyPath, -} from "@local/hash-graph-types/entity"; export const usePropertyRowsFromEntity = (): PropertyRow[] => { - const { - entity, - closedMultiEntityType, - closedMultiEntityTypesDefinitions, - setEntity, - } = useEntityEditor(); + const { entity, closedMultiEntityType, closedMultiEntityTypesDefinitions } = + useEntityEditor(); + + /** + * Generate a new metadata object based on applying a patch to the previous version. + * + * We can't use the Entity's metadata as a base each time because the ArrayEditor allows adding multiple items + * before the editor is closed and before the Entity in state is updated. So we need to keep a record of changes to the metadata object, + * which are reset when the entity in the context state is actually updated (after the editor is closed). + */ + const generateNewMetadataObject = useMemo< + PropertyRow["generateNewMetadataObject"] + >(() => { + let basePropertiesMetadata = JSON.parse( + JSON.stringify( + entity.metadata.properties ?? + ({ value: {} } satisfies PropertyMetadataObject), + ), + ); + + return ({ + propertyKeyChain, + valuePath, + valueMetadata, + }: { + propertyKeyChain: PropertyPath; + valuePath: PropertyPath; + valueMetadata: PropertyMetadataValue | "delete"; + }) => { + basePropertiesMetadata = generateChangedPropertyMetadataObject( + valuePath, + valueMetadata, + basePropertiesMetadata, + ); - const setPropertyMetadata = useCallback( - (propertyPath: PropertyPath, metadata: PropertyMetadataValue) => { - const updatedEntity = entity.setPropertyMetadata(propertyPath, metadata); + const temporaryEntity = new Entity({ + ...entity.toJSON(), + metadata: { + ...entity.metadata, + properties: basePropertiesMetadata, + }, + }); - setEntity(updatedEntity); - }, - [entity, setEntity], - ); + const pathMetadata = temporaryEntity.propertyMetadata(propertyKeyChain); + + return { + propertyMetadata: pathMetadata, + entityPropertiesMetadata: basePropertiesMetadata, + }; + }; + }, [entity]); return useMemo(() => { const processedPropertyTypes = new Set(); @@ -48,6 +89,7 @@ export const usePropertyRowsFromEntity = (): PropertyRow[] => { return generatePropertyRowRecursively({ closedMultiEntityType, closedMultiEntityTypesDefinitions, + generateNewMetadataObject, propertyTypeBaseUrl: propertyTypeBaseUrl as BaseUrl, propertyKeyChain: [propertyTypeBaseUrl as BaseUrl], entity, @@ -57,5 +99,10 @@ export const usePropertyRowsFromEntity = (): PropertyRow[] => { }); }, ); - }, [entity, closedMultiEntityType, closedMultiEntityTypesDefinitions]); + }, [ + entity, + closedMultiEntityType, + closedMultiEntityTypesDefinitions, + generateNewMetadataObject, + ]); }; diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/shared/create-draft-entity-subgraph.ts b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/shared/create-draft-entity-subgraph.ts index 59296eb0b9e..79aa91a4a65 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/shared/create-draft-entity-subgraph.ts +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/shared/create-draft-entity-subgraph.ts @@ -44,11 +44,13 @@ export const createDraftEntitySubgraph = ({ ), ); - const newEntity = new Entity({ - ...entity.toJSON(), - metadata, - properties: newProperties, - }); + const newEntity = Object.freeze( + new Entity({ + ...entity.toJSON(), + metadata, + properties: newProperties, + }), + ); return { depths: { diff --git a/libs/@local/graph/sdk/typescript/src/entity.ts b/libs/@local/graph/sdk/typescript/src/entity.ts index 302ce63fdc4..6b3d4f991a8 100644 --- a/libs/@local/graph/sdk/typescript/src/entity.ts +++ b/libs/@local/graph/sdk/typescript/src/entity.ts @@ -492,7 +492,8 @@ export const getDisplayFieldsForClosedEntityType = ( * This is a multi-entity-type, where each item in the root 'allOf' is a closed type, * each of which has its inheritance chain (including itself) in a breadth-first order in its own 'allOf'. * We need to sort all this by depth rather than going through the root 'allOf' directly, - * which would process the entire inheritance chain of each type in the multi-type before moving to the next one. + * which would process the entire inheritance chain of each type in the multi-type before moving to the next + * one. */ closedType.allOf .flatMap((type) => type.allOf ?? []) @@ -510,7 +511,8 @@ export const getDisplayFieldsForClosedEntityType = ( isLink = $id === /** - * Ideally this wouldn't be hardcoded but the only places to import it from would create a circular dependency between packages + * Ideally this wouldn't be hardcoded but the only places to import it from would create a circular dependency + * between packages * @todo do something about this */ "https://blockprotocol.org/@blockprotocol/types/entity-type/link/v/1"; @@ -681,6 +683,217 @@ export const getPropertyTypeForClosedEntityType = ({ }; }; +/** + * Generate a new property metadata object based on an existing one, with a value's metadata set or deleted. + * This is a temporary solution to be replaced by the SDK accepting {@link PropertyPatchOperation}s directly, + * which it then applies to the entity to generate the new properties and metadata. + * + * @returns {PropertyMetadataObject} a new object with the changed metadata + * @throws {Error} if the path is not supported (complex arrays or property objects) + */ +export const generateChangedPropertyMetadataObject = ( + path: PropertyPath, + metadata: PropertyMetadataValue | "delete", + baseMetadataObject: PropertyMetadataObject, +): PropertyMetadataObject => { + const clonedMetadata = JSON.parse(JSON.stringify(baseMetadataObject)) as + | PropertyMetadataObject + | undefined; + + if (!clonedMetadata) { + throw new Error( + `Expected metadata to be an object, but got metadata for property array: ${JSON.stringify( + clonedMetadata, + null, + 2, + )}`, + ); + } + + const firstKey = path[0]; + + if (!firstKey) { + throw new Error("Expected path to have at least one key"); + } + + if (typeof firstKey === "number") { + throw new Error(`Expected first key to be a string, but got ${firstKey}`); + } + + const propertyMetadata = clonedMetadata.value[firstKey]; + + const secondKey = path[1]; + + const remainingKeys = path.slice(2); + + const thirdKey = remainingKeys[0]; + + if (typeof secondKey === "undefined") { + /** + * Set or delete metadata for a single value. + * This happens regardless of whether there's already metadata set at this baseUrl on the entity's properties, + * because we're just going to overwrite it or delete it regardless. + */ + if (metadata === "delete") { + delete clonedMetadata.value[firstKey]; + } else { + clonedMetadata.value[firstKey] = metadata; + } + return clonedMetadata; + } + + if (!propertyMetadata) { + if (metadata === "delete") { + /** + * We've been asked to delete metadata at a path, but we don't have any metadata for it anyway. + */ + return clonedMetadata; + } + + if (typeof secondKey === "number") { + if (thirdKey) { + if (typeof thirdKey === "number") { + throw new Error( + `Multi-dimensional arrays as property values are not yet supported`, + ); + } else { + throw new Error(`Arrays of property objects are not yet supported`); + } + } + + if (secondKey !== 0) { + throw new Error( + `Expected array index to be 0 on new array, got ${secondKey}`, + ); + } + + /** + * Set metadata for a new array + */ + clonedMetadata.value[firstKey] = { + value: [metadata], + } satisfies PropertyMetadataArray; + return clonedMetadata; + } + + /** + * Set metadata for a new property object + */ + if (typeof thirdKey !== "number") { + throw new Error("Nested property objects are not yet supported"); + } + + if (typeof thirdKey !== "undefined") { + if (thirdKey !== 0) { + throw new Error( + `Expected array index to be 0 on new array, got ${thirdKey}`, + ); + } + + /** + * This is a new property object with an array set one of its inner properties, + * i.e. metadata for entity properties that looks like this + * { + * properties: { + * [firstKey]: { + * [secondKey]: [value that `metadata` identifies] + * } + * } + * } + */ + clonedMetadata.value[firstKey] = { + value: { + [secondKey]: { value: [metadata] } satisfies PropertyMetadataArray, + }, + } satisfies PropertyMetadataObject; + } else { + /** + * This is a new property object with a single value set for one of its properties + */ + clonedMetadata.value[firstKey] = { + value: { [secondKey]: metadata }, + } satisfies PropertyMetadataObject; + } + + return clonedMetadata; + } + + /** + * If we reached here, we already have metadata set at this baseUrl on the entity's properties, + * and it isn't a single value. + */ + if (typeof secondKey === "number") { + if (!isArrayMetadata(propertyMetadata)) { + throw new Error( + `Expected property metadata to be an array at path ${JSON.stringify([firstKey, secondKey])}, but got ${JSON.stringify( + propertyMetadata, + )}`, + ); + } + + if (typeof thirdKey !== "undefined") { + if (typeof thirdKey === "number") { + throw new Error( + `Multi-dimensional arrays as property values are not yet supported`, + ); + } else { + throw new Error(`Arrays of property objects are not yet supported`); + } + } + + if (metadata === "delete") { + propertyMetadata.value.splice(secondKey, 1); + } else { + propertyMetadata.value[secondKey] = metadata; + } + return clonedMetadata; + } + + /** + * This is an existing property object + */ + if (!isObjectMetadata(propertyMetadata)) { + throw new Error( + `Expected property metadata to be an object at path ${firstKey}, but got ${JSON.stringify( + propertyMetadata, + )}`, + ); + } + + if (typeof thirdKey !== "undefined") { + if (typeof thirdKey !== "number") { + throw new Error("Nested property objects are not yet supported"); + } + + propertyMetadata.value[secondKey] ??= { + value: [], + }; + + if (!isArrayMetadata(propertyMetadata.value[secondKey])) { + throw new Error( + `Expected property metadata to be an array at path ${JSON.stringify([firstKey, secondKey])}, but got ${JSON.stringify( + propertyMetadata.value[secondKey], + )}`, + ); + } + + const propertyObjectArrayValueMetadata = + propertyMetadata.value[secondKey].value; + + if (metadata === "delete") { + propertyObjectArrayValueMetadata.splice(thirdKey, 1); + } else { + propertyObjectArrayValueMetadata[thirdKey] = metadata; + } + } else if (metadata === "delete") { + delete propertyMetadata.value[secondKey]; + } else { + propertyMetadata.value[secondKey] = metadata; + } + + return clonedMetadata; +}; + export class Entity { #entity: EntityData; @@ -698,7 +911,9 @@ export class Entity { .entityTypeIds as PropertyMap["entityTypeIds"], temporalVersioning: entity.metadata .temporalVersioning as EntityTemporalVersioningMetadata, - properties: entity.metadata.properties as PropertyMetadataObject, + properties: entity.metadata.properties as + | PropertyMetadataObject + | undefined, provenance: { ...entity.metadata.provenance, createdById: entity.metadata.provenance.createdById as CreatedById, @@ -909,85 +1124,6 @@ export class Entity { }, this.#entity.metadata.properties); } - public setPropertyMetadata( - path: PropertyPath, - metadata: PropertyMetadataValue, - ): this { - const clonedMetadata = JSON.parse( - JSON.stringify(this.#entity.metadata.properties), - ) as PropertyMetadataObject | undefined; - - if (!clonedMetadata) { - throw new Error( - `Expected metadata to be an object, but got metadata for property array: ${JSON.stringify( - clonedMetadata, - null, - 2, - )}`, - ); - } - - const firstKey = path[0]; - - if (!firstKey) { - throw new Error("Expected path to have at least one key"); - } - - if (typeof firstKey === "number") { - throw new Error(`Expected first key to be a string, but got ${firstKey}`); - } - - const propertyMetadata = clonedMetadata.value[firstKey]; - - const secondKey = path[1]; - - const remainingKeys = path.slice(2); - - if (!propertyMetadata) { - if (typeof secondKey === "number") { - if (remainingKeys[0]) { - if (typeof remainingKeys[0] === "number") { - throw new Error( - `Multi-dimensional arrays as property values are not yet supported`, - ); - } else { - throw new Error(`Arrays of property objects are not yet supported`); - } - } - - clonedMetadata.value[firstKey] = { - value: [metadata], - }; - this.#entity.metadata.properties = clonedMetadata; - return this; - } - } else if (typeof secondKey === "number") { - if (!isArrayMetadata(propertyMetadata)) { - throw new Error( - `Expected property metadata to be an array at path ${JSON.stringify([firstKey, secondKey])}, but got ${JSON.stringify( - propertyMetadata, - )}`, - ); - } - - if (remainingKeys[0]) { - if (typeof remainingKeys[0] === "number") { - throw new Error( - `Multi-dimensional arrays as property values are not yet supported`, - ); - } else { - throw new Error(`Arrays of property objects are not yet supported`); - } - } - - propertyMetadata.value[secondKey] = metadata; - this.#entity.metadata.properties = clonedMetadata; - return this; - } - - // @todo handle property objects - } - public flattenedPropertiesMetadata(): { path: PropertyPath; metadata: PropertyMetadata["metadata"]; diff --git a/libs/@local/graph/types/typescript/src/entity.ts b/libs/@local/graph/types/typescript/src/entity.ts index 6db58293050..0ba579f4a73 100644 --- a/libs/@local/graph/types/typescript/src/entity.ts +++ b/libs/@local/graph/types/typescript/src/entity.ts @@ -78,6 +78,7 @@ export type EntityMetadata< entityTypeIds: EntityTypeIds; temporalVersioning: EntityTemporalVersioningMetadata; archived: boolean; + properties?: PropertyMetadataObject; provenance: EntityProvenance; } >;