diff --git a/apps/hash-api/src/graphql/resolvers/ontology/entity-type.ts b/apps/hash-api/src/graphql/resolvers/ontology/entity-type.ts index 5a7721dc164..892ac38934d 100644 --- a/apps/hash-api/src/graphql/resolvers/ontology/entity-type.ts +++ b/apps/hash-api/src/graphql/resolvers/ontology/entity-type.ts @@ -162,8 +162,9 @@ export const getClosedMultiEntityTypeResolver: ResolverFn< graphQLContext.authentication, { entityTypeIds, - // All references to other types are resolved, and those types provided under 'definitions' in the response - includeResolved: "resolved", + // All references to other types are resolved, and those types provided under 'definitions' in the response, + // including the children of any data types which are resolved (to allow picking more specific types) + includeResolved: "resolvedWithDataTypeChildren", includeDrafts: includeDrafts ?? false, temporalAxes: includeArchived ? fullTransactionTimeAxis diff --git a/apps/hash-frontend/src/components/grid/utils/override-custom-renderers.tsx b/apps/hash-frontend/src/components/grid/utils/override-custom-renderers.tsx index ddf51082870..c47696cd722 100644 --- a/apps/hash-frontend/src/components/grid/utils/override-custom-renderers.tsx +++ b/apps/hash-frontend/src/components/grid/utils/override-custom-renderers.tsx @@ -8,12 +8,8 @@ import { InteractableManager } from "./interactable-manager"; const ScrollLockWrapper = ({ children }: PropsWithChildren) => { /** - * We're locking scroll on `main` element, - * because our table components are rendered inside `LayoutWithSidebar`. - * Which has the overflow-y happening on `main` instead of `body` or `html` - * So we need to lock `main` elements scroll when grid editors are visible - * */ - + * The editBarContext provides the node that will have vertical scroll when the page is longer than the viewport + */ const editBarContext = useEditBarContext(); useScrollLock(true, editBarContext?.scrollingNode); 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 8b66ccc5530..7a2986ca721 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 @@ -161,7 +161,7 @@ const Page: NextPageWithLayout = () => { hasLeftEntity: { outgoing: 1, incoming: 1 }, hasRightEntity: { outgoing: 1, incoming: 1 }, }, - includeEntityTypes: "resolved", + includeEntityTypes: "resolvedWithDataTypeChildren", includeDrafts: !!draftId, temporalAxes: currentTimeInstantTemporalAxes, }, 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 35b366dbacb..39f69d0e99a 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 @@ -329,7 +329,7 @@ const EditEntitySlideOver = memo( hasRightEntity: { incoming: 1, outgoing: 1 }, }, includeDrafts: !!draftId, - includeEntityTypes: "resolved", + includeEntityTypes: "resolvedWithDataTypeChildren", }, includePermissions: false, }, 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 495fd5578fb..6d0b1d53424 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 @@ -114,8 +114,7 @@ export const createRenderChangeTypeCell = ( ); const newContent = produce(valueCellOfThisRow, (draft) => { - draft.data.propertyRow.value = undefined; - draft.data.propertyRow.valueMetadata = undefined; + draft.data.showTypePicker = true; }); /** diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell.tsx index 2fe53fc6252..a63429139aa 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell.tsx @@ -37,8 +37,13 @@ export const renderValueCell: CustomRenderer = { const { readonly } = cell.data; - const { value, valueMetadata, permittedDataTypes, isArray, isSingleUrl } = - cell.data.propertyRow; + const { + value, + valueMetadata, + permittedDataTypesIncludingChildren, + isArray, + isSingleUrl, + } = cell.data.propertyRow; ctx.fillStyle = theme.textHeader; ctx.font = theme.baseFontStyle; @@ -102,8 +107,8 @@ export const renderValueCell: CustomRenderer = { const dataTypeId = arrayItemMetadata.metadata.dataTypeId; - const dataType = permittedDataTypes.find( - (type) => type.$id === dataTypeId, + const dataType = permittedDataTypesIncludingChildren.find( + (type) => type.schema.$id === dataTypeId, ); if (!dataType) { @@ -112,7 +117,7 @@ export const renderValueCell: CustomRenderer = { ); } - const schema = getMergedDataTypeSchema(dataType); + const schema = getMergedDataTypeSchema(dataType.schema); if ("anyOf" in schema) { throw new Error( @@ -138,8 +143,8 @@ export const renderValueCell: CustomRenderer = { const dataTypeId = valueMetadata.metadata.dataTypeId; - const dataType = permittedDataTypes.find( - (type) => type.$id === dataTypeId, + const dataType = permittedDataTypesIncludingChildren.find( + (type) => type.schema.$id === dataTypeId, ); if (!dataType) { @@ -148,7 +153,7 @@ export const renderValueCell: CustomRenderer = { ); } - const schema = getMergedDataTypeSchema(dataType); + const schema = getMergedDataTypeSchema(dataType.schema); if ("anyOf" in schema) { throw new Error( 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 42415a8f202..aafb73bc142 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 @@ -57,6 +57,7 @@ export const ArrayEditor: ValueCellEditorComponent = ({ valueMetadata, generateNewMetadataObject, permittedDataTypes, + permittedDataTypesIncludingChildren, propertyKeyChain, maxItems, minItems, @@ -94,9 +95,9 @@ export const ArrayEditor: ValueCellEditorComponent = ({ const dataTypeId = arrayItemMetadata.metadata.dataTypeId; - const dataType = permittedDataTypes.find( - (type) => type.$id === dataTypeId, - ); + const dataType = permittedDataTypesIncludingChildren.find( + (type) => type.schema.$id === dataTypeId, + )?.schema; if (!dataType) { throw new Error( @@ -113,7 +114,7 @@ export const ArrayEditor: ValueCellEditorComponent = ({ }); return itemsArray; - }, [propertyValue, valueMetadata, permittedDataTypes]); + }, [propertyValue, valueMetadata, permittedDataTypesIncludingChildren]); const [selectedRow, setSelectedRow] = useState(""); const [editingRow, setEditingRow] = useState(() => { @@ -122,10 +123,13 @@ export const ArrayEditor: ValueCellEditorComponent = ({ return ""; } - if (permittedDataTypes.length === 1) { + if ( + permittedDataTypes.length === 1 && + !permittedDataTypes[0]!.schema.abstract + ) { const expectedType = permittedDataTypes[0]!; - const schema = getMergedDataTypeSchema(expectedType); + const schema = getMergedDataTypeSchema(expectedType.schema); if ("anyOf" in schema) { throw new Error( @@ -134,7 +138,7 @@ export const ArrayEditor: ValueCellEditorComponent = ({ } if ( - getEditorSpecs(expectedType, schema).arrayEditException === + getEditorSpecs(expectedType.schema, schema).arrayEditException === "no-edit-mode" ) { return ""; @@ -254,10 +258,12 @@ export const ArrayEditor: ValueCellEditorComponent = ({ const handleAddAnotherClick = () => { setSelectedRow(""); - const onlyOneExpectedType = permittedDataTypes.length === 1; + const onlyOneExpectedType = + permittedDataTypes.length === 1 && + !permittedDataTypes[0]!.schema.abstract; const expectedType = permittedDataTypes[0]!; - const schema = getMergedDataTypeSchema(expectedType); + const schema = getMergedDataTypeSchema(expectedType.schema); if ("anyOf" in schema) { throw new Error( @@ -265,13 +271,13 @@ export const ArrayEditor: ValueCellEditorComponent = ({ ); } - const editorSpec = getEditorSpecs(expectedType, schema); + const editorSpec = getEditorSpecs(expectedType.schema, schema); const noEditMode = editorSpec.arrayEditException === "no-edit-mode"; // add the value on click instead of showing draftRow if (onlyOneExpectedType && noEditMode) { - return addItem(editorSpec.defaultValue, expectedType.$id); + return addItem(editorSpec.defaultValue, expectedType.schema.$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 904c0a08626..c84fcd00785 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,13 +1,16 @@ import type { ClosedDataType, VersionedUrl } from "@blockprotocol/type-system"; +import type { ClosedDataTypeDefinition } from "@local/hash-graph-types/ontology"; +import { getMergedDataTypeSchema } from "@local/hash-isomorphic-utils/data-types"; import { useState } from "react"; import { DRAFT_ROW_KEY } from "../array-editor"; +import { getEditorSpecs } from "../editor-specs"; import { EditorTypePicker } from "../editor-type-picker"; import { isBlankStringOrNullish } from "../utils"; import { SortableRow } from "./sortable-row"; interface DraftRowProps { - expectedTypes: ClosedDataType[]; + expectedTypes: ClosedDataTypeDefinition[]; existingItemCount: number; onDraftSaved: (value: unknown, dataTypeId: VersionedUrl) => void; onDraftDiscarded: () => void; @@ -28,7 +31,7 @@ export const DraftRow = ({ throw new Error("there is no expectedType found on property type"); } - return expectedTypes[0]; + return expectedTypes[0].schema; }); if (!dataType) { @@ -36,6 +39,18 @@ export const DraftRow = ({ { + const schema = getMergedDataTypeSchema(type); + + if ("anyOf" in schema) { + throw new Error("Expected a single data type, but got multiple"); + } + + const editorSpec = getEditorSpecs(type, schema); + + if (editorSpec.arrayEditException === "no-edit-mode") { + onDraftSaved(editorSpec.defaultValue, type.$id); + } + setDataType(type); }} /> 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 a1c356e78b7..9d2d5b11be3 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 @@ -1,5 +1,4 @@ import type { JsonValue } from "@blockprotocol/core"; -import type { ClosedDataType } from "@blockprotocol/type-system"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { @@ -8,6 +7,7 @@ import { faPencil, faTrash, } from "@fortawesome/free-solid-svg-icons"; +import type { ClosedDataTypeDefinition } from "@local/hash-graph-types/ontology"; import { formatDataValue, getMergedDataTypeSchema, @@ -31,7 +31,7 @@ interface SortableRowProps { onSelect?: (id: string) => void; onEditClicked?: (id: string) => void; editing: boolean; - expectedTypes: ClosedDataType[]; + expectedTypes: ClosedDataTypeDefinition[]; onSaveChanges: (index: number, value: unknown) => void; onDiscardChanges: () => void; } @@ -60,20 +60,23 @@ export const SortableRow = ({ animateLayoutChanges: undefined, }); + const schema = getMergedDataTypeSchema(dataType); + + const editorSpec = + "anyOf" in schema ? undefined : getEditorSpecs(dataType, schema); + const [hovered, setHovered] = useState(false); - const [draftValue, setDraftValue] = useState(value); + const [draftValue, setDraftValue] = useState( + value === undefined ? editorSpec?.defaultValue : value, + ); const [prevEditing, setPrevEditing] = useState(editing); - const schema = getMergedDataTypeSchema(dataType); - - if ("anyOf" in schema) { + if ("anyOf" in schema || !editorSpec) { throw new Error( "Data types with different expected sets of constraints (anyOf) are not yet supported", ); } - const editorSpec = getEditorSpecs(dataType, schema); - const { arrayEditException } = editorSpec; const shouldShowActions = diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/editor-specs.ts b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/editor-specs.ts index 4434478141c..370072508cc 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/editor-specs.ts +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/editor-specs.ts @@ -1,17 +1,9 @@ import type { ClosedDataType } from "@blockprotocol/type-system"; import type { IconDefinition } from "@fortawesome/free-solid-svg-icons"; import { - fa100, - faAtRegular, - faBracketsCurly, - faCalendarClockRegular, - faCalendarRegular, - faClockRegular, - faEmptySet, - faInputPipeRegular, - faRulerRegular, - faSquareCheck, - faText, + getIconForDataType, + identifierTypeTitles, + measurementTypeTitles, } from "@hashintel/design-system"; import type { MergedDataTypeSingleSchema } from "@local/hash-isomorphic-utils/data-types"; @@ -19,37 +11,32 @@ import type { CustomIcon } from "../../../../../../../../../components/grid/util import type { EditorType } from "./types"; interface EditorSpec { - icon: IconDefinition["icon"]; gridIcon: CustomIcon; defaultValue?: unknown; + icon: IconDefinition["icon"]; arrayEditException?: "no-edit-mode" | "no-save-and-discard-buttons"; shouldBeDrawnAsAChip?: boolean; } // @todo consolidate this with data-types-options-context.tsx in the entity type editor -const editorSpecs: Record = { +const editorSpecs: Record> = { boolean: { - icon: faSquareCheck, gridIcon: "bpTypeBoolean", defaultValue: true, arrayEditException: "no-edit-mode", }, number: { - icon: fa100, gridIcon: "bpTypeNumber", }, string: { - icon: faText, gridIcon: "bpTypeText", }, object: { - icon: faBracketsCurly, gridIcon: "bpBracketsCurly", arrayEditException: "no-save-and-discard-buttons", shouldBeDrawnAsAChip: true, }, null: { - icon: faEmptySet, gridIcon: "bpEmptySet", defaultValue: null, arrayEditException: "no-edit-mode", @@ -57,67 +44,65 @@ const editorSpecs: Record = { }, }; -const measurementTypeTitles = [ - "Inches", - "Feet", - "Yards", - "Miles", - "Nanometers", - "Millimeters", - "Centimeters", - "Meters", - "Kilometers", -]; - -const identifierTypeTitles = ["URL", "URI"]; - export const getEditorSpecs = ( dataType: ClosedDataType, schema: MergedDataTypeSingleSchema, ): EditorSpec => { + const icon = getIconForDataType({ + title: dataType.title, + format: "format" in schema ? schema.format : undefined, + type: schema.type, + }); + switch (schema.type) { case "boolean": - return editorSpecs.boolean; + return { + ...editorSpecs.boolean, + icon, + }; case "number": - if (dataType.title && measurementTypeTitles.includes(dataType.title)) { + if (measurementTypeTitles.includes(dataType.title)) { return { ...editorSpecs.number, - icon: faRulerRegular, + icon, gridIcon: "rulerRegular", }; } - return editorSpecs.number; + return { + ...editorSpecs.number, + icon, + }; case "string": if ("format" in schema) { switch (schema.format) { case "uri": return { ...editorSpecs.string, - icon: faInputPipeRegular, + icon, gridIcon: "inputPipeRegular", }; case "email": return { ...editorSpecs.string, - icon: faAtRegular, + icon, gridIcon: "atRegular", }; case "date": return { ...editorSpecs.string, - icon: faCalendarRegular, + icon, gridIcon: "calendarRegular", }; case "time": return { ...editorSpecs.string, - icon: faClockRegular, + icon, gridIcon: "clockRegular", }; case "date-time": return { ...editorSpecs.string, - icon: faCalendarClockRegular, + icon, gridIcon: "calendarClockRegular", }; } @@ -125,22 +110,31 @@ export const getEditorSpecs = ( if (dataType.title === "Email") { return { ...editorSpecs.string, - icon: faAtRegular, + icon, gridIcon: "atRegular", }; } if (dataType.title && identifierTypeTitles.includes(dataType.title)) { return { ...editorSpecs.string, - icon: faInputPipeRegular, + icon, gridIcon: "inputPipeRegular", }; } - return editorSpecs.string; + return { + ...editorSpecs.string, + icon, + }; case "object": - return editorSpecs.object; + return { + ...editorSpecs.object, + icon, + }; case "null": - return editorSpecs.null; + return { + ...editorSpecs.null, + icon, + }; default: throw new Error(`Unhandled type: ${schema.type}`); } diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/editor-type-picker.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/editor-type-picker.tsx index f228d16d3eb..c4507787fd0 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/editor-type-picker.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/editor-type-picker.tsx @@ -1,89 +1,50 @@ -import type { ClosedDataType } from "@blockprotocol/type-system/slim"; -import { FontAwesomeIcon } from "@hashintel/design-system"; -import { getMergedDataTypeSchema } from "@local/hash-isomorphic-utils/data-types"; -import { Box, ButtonBase, Typography } from "@mui/material"; - -import { getEditorSpecs } from "./editor-specs"; +import type { VersionedUrl } from "@blockprotocol/type-system/slim"; +import { + buildDataTypeTreesForSelector, + DataTypeSelector, +} from "@hashintel/design-system"; +import type { ClosedDataTypeDefinition } from "@local/hash-graph-types/ontology"; +import { useMemo } from "react"; + +import { useEntityEditor } from "../../../../entity-editor-context"; import type { OnTypeChange } from "./types"; -const ExpectedTypeButton = ({ - onClick, - expectedType, -}: { - onClick: () => void; - expectedType: ClosedDataType; -}) => { - const schema = getMergedDataTypeSchema(expectedType); - - if ("anyOf" in schema) { - throw new Error( - "Data types with different expected sets of constraints (anyOf) are not yet supported", - ); - } - - const editorSpec = getEditorSpecs(expectedType, schema); - - const { description, title } = expectedType; - - return ( - - - {title} - {!!description && ( - - {description} - - )} - - ); -}; - interface EditorTypePickerProps { - expectedTypes: ClosedDataType[]; + expectedTypes: ClosedDataTypeDefinition[]; onTypeChange: OnTypeChange; + selectedDataTypeId?: VersionedUrl; } export const EditorTypePicker = ({ expectedTypes, onTypeChange, + selectedDataTypeId, }: EditorTypePickerProps) => { - return ( - - - Choose data type - - - How are you representing this value? - + const { closedMultiEntityTypesDefinitions } = useEntityEditor(); + + const dataTypeTrees = useMemo(() => { + return buildDataTypeTreesForSelector({ + targetDataTypes: expectedTypes, + dataTypePoolById: closedMultiEntityTypesDefinitions.dataTypes, + }); + }, [expectedTypes, closedMultiEntityTypesDefinitions]); + + const onSelect = (dataTypeId: VersionedUrl) => { + const selectedType = + closedMultiEntityTypesDefinitions.dataTypes[dataTypeId]; - - {expectedTypes.map((expectedType) => { - return ( - onTypeChange(expectedType)} - /> - ); - })} - - + if (!selectedType) { + throw new Error(`Could not find data type with id ${dataTypeId}`); + } + + onTypeChange(selectedType.schema); + }; + + return ( + ); }; 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 a72494cf3d0..fa02297f2fe 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 @@ -22,20 +22,26 @@ export const SingleValueEditor: ValueCellEditorComponent = (props) => { const { generateNewMetadataObject, permittedDataTypes, + permittedDataTypesIncludingChildren, propertyKeyChain, value, valueMetadata, } = cell.data.propertyRow; + const { showTypePicker } = cell.data; + const textInputFormRef = useRef(null); const [chosenDataType, setChosenDataType] = useState<{ dataType: ClosedDataType; schema: MergedDataTypeSingleSchema; } | null>(() => { - if (permittedDataTypes.length === 1) { + if ( + permittedDataTypes.length === 1 && + !permittedDataTypes[0]!.schema.abstract + ) { const dataType = permittedDataTypes[0]!; - const schema = getMergedDataTypeSchema(dataType); + const schema = getMergedDataTypeSchema(dataType.schema); if ("anyOf" in schema) { throw new Error( @@ -44,7 +50,7 @@ export const SingleValueEditor: ValueCellEditorComponent = (props) => { } return { - dataType, + dataType: dataType.schema, schema, }; } @@ -64,7 +70,9 @@ export const SingleValueEditor: ValueCellEditorComponent = (props) => { const dataTypeId = valueMetadata.metadata.dataTypeId; - const dataType = permittedDataTypes.find((type) => type.$id === dataTypeId); + const dataType = permittedDataTypesIncludingChildren.find( + (type) => type.schema.$id === dataTypeId, + ); if (!dataType) { throw new Error( @@ -72,7 +80,7 @@ export const SingleValueEditor: ValueCellEditorComponent = (props) => { ); } - const schema = getMergedDataTypeSchema(dataType); + const schema = getMergedDataTypeSchema(dataType.schema); if ("anyOf" in schema) { throw new Error( @@ -81,7 +89,7 @@ export const SingleValueEditor: ValueCellEditorComponent = (props) => { } return { - dataType, + dataType: dataType.schema, schema, }; }); @@ -122,7 +130,11 @@ export const SingleValueEditor: ValueCellEditorComponent = (props) => { latestValueCellRef.current = cell; }); - if (!chosenDataType || !cell.data.propertyRow.valueMetadata) { + if ( + !chosenDataType || + !cell.data.propertyRow.valueMetadata || + showTypePicker + ) { return ( { draftCell.data.propertyRow.valueMetadata = { metadata: { dataTypeId: type.$id }, }; + draftCell.data.showTypePicker = false; }); return onFinishedEditing(newCell); + } else { + const newCell = produce(cell, (draftCell) => { + draftCell.data.propertyRow.valueMetadata = { + metadata: { dataTypeId: type.$id }, + }; + draftCell.data.showTypePicker = false; + }); + + return onChange(newCell); } }} + selectedDataTypeId={chosenDataType?.dataType.$id} /> ); diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/types.ts b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/types.ts index 8e5f47a7c92..cfe0687514c 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/types.ts +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/entity-editor/properties-section/property-table/cells/value-cell/types.ts @@ -11,6 +11,7 @@ export interface ValueCellProps extends TooltipCellProps { readonly kind: "value-cell"; propertyRow: PropertyRow; readonly: boolean; + showTypePicker?: boolean; } export type ValueCell = CustomCell; 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 c6d6453dc60..3a74de75eaf 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,4 +1,3 @@ -import type { ClosedDataType } from "@blockprotocol/type-system"; import type { SizedGridColumn } from "@glideapps/glide-data-grid"; import type { PropertyMetadata, @@ -6,6 +5,7 @@ import type { PropertyMetadataValue, PropertyPath, } from "@local/hash-graph-types/entity"; +import type { ClosedDataTypeDefinition } from "@local/hash-graph-types/ontology"; import type { VerticalIndentationLineDir } from "../../../../../../../components/grid/utils/draw-vertical-indentation-line"; @@ -35,7 +35,8 @@ export type PropertyRow = { isSingleUrl: boolean; maxItems?: number; minItems?: number; - permittedDataTypes: ClosedDataType[]; + permittedDataTypes: ClosedDataTypeDefinition[]; + permittedDataTypesIncludingChildren: ClosedDataTypeDefinition[]; propertyKeyChain: PropertyPath; required: boolean; rowId: string; 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 546c4621887..3094d667517 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,12 +64,18 @@ export const useCreateGetCellContent = ( }, }; - const { isArray, permittedDataTypes, value, valueMetadata } = row; + const { + isArray, + permittedDataTypes, + permittedDataTypesIncludingChildren, + valueMetadata, + } = row; const shouldShowChangeTypeCell = - permittedDataTypes.length > 1 && + (permittedDataTypes.length > 1 || + permittedDataTypes[0]?.schema.abstract) && !isArray && - typeof value !== "undefined" && + valueMetadata && !readonly; switch (columnKey) { @@ -114,12 +120,6 @@ export const useCreateGetCellContent = ( } if (shouldShowChangeTypeCell) { - if (!valueMetadata) { - throw new Error( - `Expected value metadata to be set when showing change type cell`, - ); - } - if (!isValueMetadata(valueMetadata)) { throw new Error( `Expected single value when showing change type cell`, @@ -128,8 +128,8 @@ export const useCreateGetCellContent = ( const dataTypeId = valueMetadata.metadata.dataTypeId; - const dataType = permittedDataTypes.find( - (type) => type.$id === dataTypeId, + const dataType = permittedDataTypesIncludingChildren.find( + (type) => type.schema.$id === dataTypeId, ); if (!dataType) { @@ -142,11 +142,11 @@ export const useCreateGetCellContent = ( kind: GridCellKind.Custom, allowOverlay: false, readonly: true, - copyData: dataType.$id, + copyData: dataType.schema.$id, cursor: "pointer", data: { kind: "change-type-cell", - currentType: dataType, + currentType: dataType.schema, propertyRow: row, valueCellOfThisRow: valueCell, }, @@ -161,7 +161,7 @@ export const useCreateGetCellContent = ( data: { kind: "chip-cell", chips: row.permittedDataTypes.map((type) => { - const schema = getMergedDataTypeSchema(type); + const schema = getMergedDataTypeSchema(type.schema); if ("anyOf" in schema) { throw new Error( @@ -169,10 +169,10 @@ export const useCreateGetCellContent = ( ); } - const editorSpec = getEditorSpecs(type, schema); + const editorSpec = getEditorSpecs(type.schema, schema); return { - text: type.title, + text: type.schema.title, icon: { inbuiltIcon: editorSpec.gridIcon }, faIconDefinition: { icon: editorSpec.icon }, }; 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 442018aaeb0..94d918b8530 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 @@ -8,6 +8,7 @@ import type { ClosedMultiEntityType, ClosedMultiEntityTypesDefinitions, } from "@local/hash-graph-types/ontology"; +import { getPermittedDataTypes } from "@local/hash-isomorphic-utils/data-types"; import get from "lodash/get"; import { @@ -186,6 +187,10 @@ export const generatePropertyRowRecursively = ({ isArray, isSingleUrl, permittedDataTypes: expectedTypes, + permittedDataTypesIncludingChildren: getPermittedDataTypes({ + targetDataTypes: expectedTypes, + dataTypePoolById: closedMultiEntityTypesDefinitions.dataTypes, + }), propertyKeyChain, required, rowId, 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/get-expected-types-of-property-type.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/get-expected-types-of-property-type.ts index 315560c0e60..d56f7581053 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/get-expected-types-of-property-type.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/get-expected-types-of-property-type.ts @@ -1,10 +1,12 @@ import type { - ClosedDataType, DataTypeReference, PropertyType, PropertyValues, } from "@blockprotocol/type-system"; -import type { ClosedMultiEntityTypesDefinitions } from "@local/hash-graph-types/ontology"; +import type { + ClosedDataTypeDefinition, + ClosedMultiEntityTypesDefinitions, +} from "@local/hash-graph-types/ontology"; import { isPropertyValueArray } from "../../../../../../../../../lib/typeguards"; @@ -26,11 +28,11 @@ const getReferencedDataTypes = ( propertyValues: PropertyValues[], definitions: ClosedMultiEntityTypesDefinitions, ) => { - const types: ClosedDataType[] = []; + const types: ClosedDataTypeDefinition[] = []; for (const value of propertyValues) { if ("$ref" in value) { - types.push(getDataType(value, definitions).schema); + types.push(getDataType(value, definitions)); } } @@ -41,11 +43,11 @@ export const getExpectedTypesOfPropertyType = ( propertyType: PropertyType, definitions: ClosedMultiEntityTypesDefinitions, ): { - expectedTypes: ClosedDataType[]; + expectedTypes: ClosedDataTypeDefinition[]; isArray: boolean; } => { let isArray = false; - let expectedTypes: ClosedDataType[] = []; + let expectedTypes: ClosedDataTypeDefinition[] = []; /** * @todo handle property types with multiple expected values -- H-2257 diff --git a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/select-entity-type-page.tsx b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/select-entity-type-page.tsx index 72b387fab69..796fe964082 100644 --- a/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/select-entity-type-page.tsx +++ b/apps/hash-frontend/src/pages/[shortname]/entities/[entity-uuid].page/select-entity-type-page.tsx @@ -20,6 +20,8 @@ import { EntityPageHeader } from "./entity-page-wrapper/entity-page-header"; import { LinksSectionEmptyState } from "./shared/links-section-empty-state"; import { PropertiesSectionEmptyState } from "./shared/properties-section-empty-state"; +const selectorOrButtonHeight = 46; + export const SelectEntityTypePage = () => { const router = useRouter(); const { triggerSnackbar } = useSnackbar(); @@ -91,7 +93,7 @@ export const SelectEntityTypePage = () => { display: "flex", gap: 1, alignItems: "center", - p: 3, + p: 4, justifyContent: "center", flexWrap: "wrap", }} @@ -99,6 +101,7 @@ export const SelectEntityTypePage = () => { {isSelectingType ? ( setIsSelectingType(false)} onSelect={async (entityType) => { try { @@ -130,8 +133,13 @@ export const SelectEntityTypePage = () => { diff --git a/apps/hash-frontend/src/pages/_app.page.tsx b/apps/hash-frontend/src/pages/_app.page.tsx index c946c54ee4f..5f9ad5900e3 100644 --- a/apps/hash-frontend/src/pages/_app.page.tsx +++ b/apps/hash-frontend/src/pages/_app.page.tsx @@ -142,19 +142,24 @@ const App: FunctionComponent = ({ - {/* "spin" is used in some inline styles which have been temporarily introduced in https://github.com/hashintel/hash/pull/1471 */} - {/* @todo remove when inline styles are replaced with MUI styles */} ); diff --git a/apps/hash-frontend/src/pages/types/[[...type-kind]].page/types-table.tsx b/apps/hash-frontend/src/pages/types/[[...type-kind]].page/types-table.tsx index 5f9286ffe86..71a15302dc0 100644 --- a/apps/hash-frontend/src/pages/types/[[...type-kind]].page/types-table.tsx +++ b/apps/hash-frontend/src/pages/types/[[...type-kind]].page/types-table.tsx @@ -348,12 +348,15 @@ export const TypesTable: FunctionComponent<{ switch (column.id) { case "title": { + const isClickable = + row.kind === "entity-type" || row.kind === "link-type"; + return { kind: GridCellKind.Custom, readonly: true, allowOverlay: false, copyData: row.title, - cursor: "pointer", + cursor: isClickable ? "pointer" : "default", data: { kind: "chip-cell", chips: [ @@ -365,12 +368,11 @@ export const TypesTable: FunctionComponent<{ row.kind === "link-type" ? "bpLink" : "bpAsterisk", }, text: row.title, - onClick: - row.kind === "entity-type" || row.kind === "link-type" - ? () => { - setSelectedEntityType({ entityTypeId: row.typeId }); - } - : undefined, + onClick: isClickable + ? () => { + setSelectedEntityType({ entityTypeId: row.typeId }); + } + : undefined, iconFill: theme.palette.blue[70], }, ], @@ -392,19 +394,23 @@ export const TypesTable: FunctionComponent<{ ? `@${row.webShortname}` : typeNamespaceFromTypeId(row.typeId); + const isClickable = row.webShortname !== undefined; + return { kind: GridCellKind.Custom, allowOverlay: false, readonly: true, - cursor: "pointer", + cursor: isClickable ? "pointer" : "default", copyData: value, data: { kind: "text-icon-cell", icon: null, value, - onClick: () => { - void router.push(`/${value}`); - }, + onClick: isClickable + ? () => { + void router.push(`/${value}`); + } + : undefined, }, }; } diff --git a/apps/hash-frontend/src/shared/layout/layout-with-sidebar.tsx b/apps/hash-frontend/src/shared/layout/layout-with-sidebar.tsx index d35a3f75dbc..fbef7e0f092 100644 --- a/apps/hash-frontend/src/shared/layout/layout-with-sidebar.tsx +++ b/apps/hash-frontend/src/shared/layout/layout-with-sidebar.tsx @@ -1,7 +1,7 @@ import { IconButton } from "@hashintel/design-system"; import { Box, Collapse, Stack, styled } from "@mui/material"; import type { FunctionComponent, ReactNode } from "react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { EditBarScroller } from "../edit-bar-scroller"; import { ArrowRightToLineIcon } from "../icons"; @@ -38,7 +38,11 @@ export const LayoutWithSidebar: FunctionComponent = ({ }) => { const { openSidebar, sidebarOpen } = useSidebarContext(); const isReadonlyMode = useIsReadonlyModeForApp(); - const [main, setMain] = useState(null); + const [scrollingNode, setScrollingNode] = useState(null); + + useEffect(() => { + setScrollingNode(document.body); + }, []); return ( @@ -120,10 +124,11 @@ export const LayoutWithSidebar: FunctionComponent = ({ maxWidth: 820, }), })} - ref={setMain} > {/* Enables EditBar to make the page scroll as it animates in */} - {children} + + {children} + diff --git a/libs/@hashintel/design-system/src/components.ts b/libs/@hashintel/design-system/src/components.ts index 8cb5b48d432..0dcf32c81dd 100644 --- a/libs/@hashintel/design-system/src/components.ts +++ b/libs/@hashintel/design-system/src/components.ts @@ -6,6 +6,7 @@ export * from "./button"; export * from "./callout"; export * from "./chip"; export * from "./chip-group"; +export * from "./data-type-selector"; export * from "./e-chart"; export * from "./entity-or-type-icon"; export * from "./fa-icons/icons"; diff --git a/libs/@hashintel/design-system/src/data-type-selector.tsx b/libs/@hashintel/design-system/src/data-type-selector.tsx new file mode 100644 index 00000000000..1bcf1c836b8 --- /dev/null +++ b/libs/@hashintel/design-system/src/data-type-selector.tsx @@ -0,0 +1,659 @@ +import type { + DataType, + StringConstraints, + VersionedUrl, +} from "@blockprotocol/type-system/slim"; +// eslint-disable-next-line no-restricted-imports -- TODO needs fixing if this package to be used from npm +import type { ClosedDataTypeDefinition } from "@local/hash-graph-types/ontology"; +// eslint-disable-next-line no-restricted-imports -- TODO needs fixing if this package to be used from npm +import { getMergedDataTypeSchema } from "@local/hash-isomorphic-utils/data-types"; +import { + Box, + outlinedInputClasses, + Stack, + TextField, + Tooltip, + Typography, +} from "@mui/material"; +import type { MouseEventHandler } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; + +import { getIconForDataType } from "./data-type-selector/icons"; +import { FontAwesomeIcon } from "./fontawesome-icon"; +import { IconButton } from "./icon-button"; +import { CaretDownSolidIcon } from "./icon-caret-down-solid"; +import { CheckIcon } from "./icon-check"; + +export { + getIconForDataType, + identifierTypeTitles, + measurementTypeTitles, +} from "./data-type-selector/icons"; + +export type DataTypeForSelector = { + $id: VersionedUrl; + abstract: boolean; + children: DataTypeForSelector[]; + description: string; + directParents: VersionedUrl[]; + format?: StringConstraints["format"]; + label: DataType["label"]; + type: string; + title: string; +}; + +const isDataType = ( + dataType: DataType | ClosedDataTypeDefinition, +): dataType is DataType => { + return "$id" in dataType; +}; + +const defaultMaxHeight = 500; +const inputHeight = 48; +const hintHeight = 36; + +/** + * Build a tree rooted at the provided data type, with each node containing the information needed for the selector. + * + * Some callers will have the 'closed' data type (with all constraints resolved), others only the underlying schema, + * without having its parents' constraints merged in. + * + * We don't care about the closed schema for this purpose because the selector doesn't care about value constraints, + * but it's the structure that some callers may already have. + */ +const transformDataTypeForSelector = < + T extends DataType | ClosedDataTypeDefinition, +>( + dataType: T, + directChildrenByDataTypeId: Record, + allChildren: VersionedUrl[] = [], +): { + allChildren: VersionedUrl[]; + transformedDataType: DataTypeForSelector; +} => { + const schema = isDataType(dataType) ? dataType : dataType.schema; + const { $id } = schema; + + const children = directChildrenByDataTypeId[$id] ?? []; + const transformedChildren: DataTypeForSelector[] = []; + + /** + * We need to track all descendants of the data type, so that {@link buildDataTypeTreesForSelector} + * can remove any types as roots of a tree where they appear lower down another tree. + */ + allChildren.push( + ...children.map((child) => + isDataType(child) ? child.$id : child.schema.$id, + ), + ); + + for (const child of children) { + const { transformedDataType } = transformDataTypeForSelector( + child, + directChildrenByDataTypeId, + allChildren, + ); + transformedChildren.push(transformedDataType); + } + + let format; + let type; + + if (isDataType(dataType)) { + const firstSchema = "anyOf" in dataType ? dataType.anyOf[0] : dataType; + type = firstSchema.type; + format = "format" in firstSchema ? firstSchema.format : undefined; + } else { + const mergedSchema = getMergedDataTypeSchema(dataType.schema); + + const firstSchema = + "anyOf" in mergedSchema ? mergedSchema.anyOf[0]! : mergedSchema; + + type = firstSchema.type; + format = "format" in firstSchema ? firstSchema.format : undefined; + } + + const transformedDataType = { + $id, + abstract: !!schema.abstract, + children: transformedChildren.sort((a, b) => + a.title.localeCompare(b.title), + ), + description: schema.description, + directParents: isDataType(dataType) + ? (dataType.allOf?.map(({ $ref }) => $ref) ?? []) + : dataType.parents, + label: schema.label, + title: schema.title, + type, + format, + }; + + return { + allChildren, + transformedDataType, + }; +}; + +/** + * Build trees of data types that can be selected by the user, rooted at those data types + * among targetDataTypes which do not have any parents in targetDataTypes, + * i.e. rooted at the types which have no selectable parents. + * + * Data types may appear in multiple locations in the tree if they have multiple selectable parents. + * This will only happen if a user builds a type with multiple expected values, + * and one of the values is a descendant of another one. + * + * The data type pool must include all children of the targetDataTypes, which can be fetched from the API via one of: + * - fetching all data types: + * e.g. in the context of the type editor, where all types are selectable) + * - making a query for an entityType with the 'resolvedWithDataTypeChildren' resolution method + * e.g. when selecting a value valid for specific entity types, and having the API resolve the valid data types + */ +export const buildDataTypeTreesForSelector = < + T extends ClosedDataTypeDefinition | DataType, +>({ + targetDataTypes, + dataTypePoolById, +}: { + /** + * The data types that the user is allowed to select + * – does not need to include children, but they should appear in the dataTypePool. + */ + targetDataTypes: T[]; + /** + * All data types that are available for building the trees from. + * This MUST include targetDataTypes and any children of targetDataTypes to allow them to be included in the + * resulting tree. It MAY include other data types which are not selectable (e.g. parents of targetDataTypes, or + * unrelated types). Unrelated types will not be included in the resulting trees. + */ + dataTypePoolById: Record; +}): DataTypeForSelector[] => { + const directChildrenByDataTypeId: Record = {}; + + /** + * First, we need to know the children of all data types. Data types store references to their parents, not children. + * The selectable types are either targets or children of targets. + */ + for (const dataType of Object.values(dataTypePoolById)) { + const directParents = isDataType(dataType) + ? (dataType.allOf?.map(({ $ref }) => $ref) ?? []) + : dataType.parents; + + for (const parent of directParents) { + directChildrenByDataTypeId[parent] ??= []; + + const parentDataType = dataTypePoolById[parent]; + + if (parentDataType) { + /** + * If the parentDataType is not in the pool, it is not a selectable parent. + * The caller is responsible for ensuring that the pool contains all selectable data types, + * via one of the methods described in the function's JSDoc. + */ + directChildrenByDataTypeId[parent].push(dataType); + } + } + } + + const rootsById: Record = {}; + + const dataTypesBelowRoots: VersionedUrl[] = []; + + /** + * Build a tree for each target data type. + */ + for (const dataType of targetDataTypes) { + const { allChildren, transformedDataType } = transformDataTypeForSelector( + dataType, + directChildrenByDataTypeId, + ); + + if ( + /** + * There's no point adding abstract types with no children, because they cannot be selected. + */ + !transformedDataType.abstract || + transformedDataType.children.length > 0 + ) { + rootsById[transformedDataType.$id] = transformedDataType; + dataTypesBelowRoots.push(...allChildren); + } + } + + /** + * Finally, remove any trees rooted at a target data type which appears as the child of another target. + */ + for (const dataTypeId of dataTypesBelowRoots) { + delete rootsById[dataTypeId]; + } + + return Object.values(rootsById); +}; + +const DataTypeLabel = (props: { + dataType: DataTypeForSelector; + selected: boolean; +}) => { + const { dataType, selected } = props; + + const labelParts: string[] = []; + if (dataType.label?.left) { + labelParts.push(dataType.label.left); + } + if (dataType.label?.right) { + labelParts.push(dataType.label.right); + } + + const unitLabel = labelParts.length ? labelParts.join(" / ") : undefined; + + const icon = getIconForDataType(dataType); + + return ( + + + + selected ? palette.blue[60] : palette.gray[90], + mr: 1, + }} + /> + ({ color: palette.gray[90], fontSize: 14 })} + > + {dataType.title} + + {unitLabel && ( + ({ + fontSize: 12, + color: palette.gray[50], + ml: 1.5, + })} + > + {unitLabel} + + )} + + + ); +}; + +const DataTypeFlatView = (props: { + allowSelectingAbstractTypes?: boolean; + dataType: DataTypeForSelector; + selected: boolean; + onSelect: (dataTypeId: VersionedUrl) => void; +}) => { + const { allowSelectingAbstractTypes, dataType, onSelect, selected } = props; + + const ref = useRef(null); + + useEffect(() => { + if (selected) { + setTimeout(() => { + ref.current?.scrollIntoView({ behavior: "smooth" }); + }, 100); + } + }, [selected]); + + return ( + onSelect(dataType.$id) + } + sx={({ palette, transitions }) => ({ + cursor: "pointer", + px: 2.5, + py: 1.5, + background: selected ? palette.blue[20] : undefined, + borderRadius: 1, + border: `1px solid ${selected ? palette.blue[30] : palette.gray[30]}`, + "&:hover": { + background: selected + ? palette.blue[30] + : !!allowSelectingAbstractTypes || !dataType.abstract + ? palette.gray[10] + : undefined, + }, + transition: transitions.create("background"), + })} + > + + + ); +}; + +const defaultActionClassName = "data-type-selector-default-action-button"; + +const DataTypeTreeView = (props: { + allowSelectingAbstractTypes?: boolean; + dataType: DataTypeForSelector; + depth?: number; + isOnlyRoot?: boolean; + selectedDataTypeId?: VersionedUrl; + onSelect: (dataTypeId: VersionedUrl) => void; +}) => { + const { + allowSelectingAbstractTypes, + dataType, + depth = 0, + onSelect, + selectedDataTypeId, + } = props; + + const selected = dataType.$id === selectedDataTypeId; + + const ref = useRef(null); + + useEffect(() => { + if (selected) { + setTimeout(() => { + ref.current?.scrollIntoView({ behavior: "smooth" }); + }, 100); + } + }, [selected]); + + const { abstract, children, $id } = dataType; + + const [expanded, setExpanded] = useState(() => { + const stack = [...children]; + while (stack.length > 0) { + const current = stack.pop()!; + + if (current.$id === selectedDataTypeId) { + return true; + } + + stack.push(...current.children); + } + }); + + const defaultAction: MouseEventHandler = + abstract && !allowSelectingAbstractTypes + ? (event) => { + event.stopPropagation(); + setExpanded(!expanded); + } + : (event) => { + event.stopPropagation(); + onSelect($id); + }; + + return ( + <> + ({ + cursor: "pointer", + ml: depth * 3, + px: 2.5, + py: 1, + background: selected + ? palette.blue[20] + : dataType.abstract + ? palette.gray[20] + : undefined, + borderRadius: 1, + border: `1px solid ${selected ? palette.blue[30] : palette.gray[30]}`, + [`&:hover svg.${defaultActionClassName}`]: { + fill: palette.blue[50], + }, + "&:hover": { + background: selected + ? palette.blue[30] + : !!allowSelectingAbstractTypes || !abstract + ? palette.gray[10] + : undefined, + }, + transition: transitions.create("background"), + })} + > + + + {children.length > 0 && ( + { + event.stopPropagation(); + setExpanded(!expanded); + }} + rounded + sx={({ palette, transitions }) => ({ + fill: expanded ? palette.blue[70] : palette.gray[50], + transform: expanded ? "none" : "rotate(-90deg)", + transition: transitions.create(["transform", "fill"]), + p: 0.7, + "& svg": { fontSize: 12 }, + })} + > + + + )} + {(!abstract || allowSelectingAbstractTypes) && ( + { + event.stopPropagation(); + onSelect($id); + }} + size="small" + rounded + sx={({ palette }) => ({ + p: 0.7, + "& svg": { + fontSize: 14, + fill: selected ? palette.blue[60] : palette.gray[50], + }, + })} + > + + + )} + + + {expanded && + children.map((child) => { + return ( + + ); + })} + + ); +}; + +export type DataTypeSelectorProps = { + allowSelectingAbstractTypes?: boolean; + dataTypes: DataTypeForSelector[]; + hideHint?: boolean; + maxHeight?: number; + onSelect: (dataTypeId: VersionedUrl) => void; + searchText?: string; + selectedDataTypeId?: VersionedUrl; +}; + +export const DataTypeSelector = (props: DataTypeSelectorProps) => { + const { + allowSelectingAbstractTypes, + dataTypes, + hideHint, + maxHeight: maxHeightFromProps, + onSelect, + searchText: externallyControlledSearchText, + selectedDataTypeId, + } = props; + + const maxHeight = maxHeightFromProps ?? defaultMaxHeight; + + const [localSearchText, setLocalSearchText] = useState(""); + + const searchText = externallyControlledSearchText ?? localSearchText; + + const flattenedDataTypes = useMemo(() => { + const flattened: DataTypeForSelector[] = []; + + const stack = [...dataTypes]; + + const seenDataTypes = new Set(); + + while (stack.length > 0) { + const current = stack.pop()!; + + if (seenDataTypes.has(current.$id)) { + continue; + } + + flattened.push(current); + + stack.push(...current.children); + + seenDataTypes.add(current.$id); + } + + return flattened; + }, [dataTypes]); + + const dataTypesToDisplay = useMemo(() => { + if (!searchText) { + return dataTypes; + } + + return flattenedDataTypes.filter( + (dataType) => + (allowSelectingAbstractTypes || !dataType.abstract) && + dataType.title.toLowerCase().includes(searchText.toLowerCase()), + ); + }, [allowSelectingAbstractTypes, dataTypes, flattenedDataTypes, searchText]); + + const sortedDataTypes = useMemo(() => { + return dataTypesToDisplay.sort((a, b) => { + if (searchText) { + if (a.title.toLowerCase().startsWith(searchText.toLowerCase())) { + return -1; + } + if (b.title.toLowerCase().startsWith(searchText.toLowerCase())) { + return 1; + } + } + + return a.title.localeCompare(b.title); + }); + }, [dataTypesToDisplay, searchText]); + + return ( + + {externallyControlledSearchText === undefined && ( + setLocalSearchText(event.target.value)} + placeholder="Start typing to filter options..." + sx={{ + borderBottom: ({ palette }) => `1px solid ${palette.gray[30]}`, + height: inputHeight, + "*": { + border: "none", + boxShadow: "none", + borderRadius: 0, + }, + [`.${outlinedInputClasses.root} input`]: { + fontSize: 14, + }, + }} + /> + )} + {!hideHint && ( + + palette.gray[80], + mr: 1, + textTransform: "uppercase", + }} + > + Choose data type + + palette.gray[50], + }} + > + How are you representing this value? + + + )} + + + {!sortedDataTypes.length && ( + palette.gray[50], fontSize: 14 }} + > + No options found... + + )} + {sortedDataTypes.map((dataType) => { + const selected = dataType.$id === selectedDataTypeId; + + if (searchText) { + return ( + + ); + } + + return ( + + ); + })} + + + ); +}; diff --git a/libs/@hashintel/design-system/src/data-type-selector/icons.ts b/libs/@hashintel/design-system/src/data-type-selector/icons.ts new file mode 100644 index 00000000000..610aaaf700b --- /dev/null +++ b/libs/@hashintel/design-system/src/data-type-selector/icons.ts @@ -0,0 +1,83 @@ +import type { StringConstraints } from "@blockprotocol/type-system/slim"; +import type { IconDefinition } from "@fortawesome/free-solid-svg-icons"; + +import { fa100 } from "../fa-icons/fa-100"; +import { faAtRegular } from "../fa-icons/fa-at-regular"; +import { faBracketsCurly } from "../fa-icons/fa-brackets-curly"; +import { faCalendarClockRegular } from "../fa-icons/fa-calendar-clock-regular"; +import { faCalendarRegular } from "../fa-icons/fa-calendar-regular"; +import { faClockRegular } from "../fa-icons/fa-clock-regular"; +import { faEmptySet } from "../fa-icons/fa-empty-set"; +import { faInputPipeRegular } from "../fa-icons/fa-input-pipe-regular"; +import { faRulerRegular } from "../fa-icons/fa-ruler-regular"; +import { faSquareCheck } from "../fa-icons/fa-square-check"; +import { faText } from "../fa-icons/fa-text"; + +export const identifierTypeTitles = ["URL", "URI"]; + +export const measurementTypeTitles = [ + "Length", + "Imperial Length (UK)", + "Imperial Length (US)", + "Metric Length (SI)", + "Inches", + "Feet", + "Yards", + "Miles", + "Nanometers", + "Millimeters", + "Centimeters", + "Meters", + "Kilometers", +]; + +export const getIconForDataType = ({ + title, + format, + type, +}: { + title: string; + format?: StringConstraints["format"]; + type: string; +}): IconDefinition["icon"] => { + if (type === "boolean") { + return faSquareCheck; + } + if (type === "number") { + if (measurementTypeTitles.includes(title)) { + return faRulerRegular; + } + return fa100; + } + if (type === "object") { + return faBracketsCurly; + } + if (type === "null") { + return faEmptySet; + } + if (type === "string") { + if (format) { + switch (format) { + case "uri": + return faInputPipeRegular; + case "email": + return faAtRegular; + case "date": + return faCalendarRegular; + case "time": + return faClockRegular; + case "date-time": + return faCalendarClockRegular; + } + } + if (title === "Email") { + return faAtRegular; + } + if (identifierTypeTitles.includes(title)) { + return faInputPipeRegular; + } + return faText; + } + + throw new Error(`Unhandled type: ${type}`); +}; diff --git a/libs/@hashintel/design-system/src/theme/components/inputs/mui-input-base-theme-options.ts b/libs/@hashintel/design-system/src/theme/components/inputs/mui-input-base-theme-options.ts index 1091ba62f07..ba71ba9dd9f 100644 --- a/libs/@hashintel/design-system/src/theme/components/inputs/mui-input-base-theme-options.ts +++ b/libs/@hashintel/design-system/src/theme/components/inputs/mui-input-base-theme-options.ts @@ -2,6 +2,7 @@ import type { Components, Theme } from "@mui/material"; export const MuiInputBaseThemeOptions: Components["MuiInputBase"] = { defaultProps: { + disableInjectingGlobalStyles: true, inputProps: { "data-1p-ignore": true, }, diff --git a/libs/@hashintel/type-editor/package.json b/libs/@hashintel/type-editor/package.json index a664db315df..818e19d716e 100644 --- a/libs/@hashintel/type-editor/package.json +++ b/libs/@hashintel/type-editor/package.json @@ -25,6 +25,7 @@ "@fortawesome/free-regular-svg-icons": "6.7.1", "@fortawesome/free-solid-svg-icons": "6.7.1", "@hashintel/design-system": "0.0.8", + "@local/hash-isomorphic-utils": "0.0.0-private", "@local/hash-subgraph": "0.0.0-private", "clsx": "1.2.1", "lodash.memoize": "4.1.2", diff --git a/libs/@hashintel/type-editor/src/entity-type-editor/property-list-card/property-type-form/expected-value-selector.tsx b/libs/@hashintel/type-editor/src/entity-type-editor/property-list-card/property-type-form/expected-value-selector.tsx index f0a1954fe80..77e77833430 100644 --- a/libs/@hashintel/type-editor/src/entity-type-editor/property-list-card/property-type-form/expected-value-selector.tsx +++ b/libs/@hashintel/type-editor/src/entity-type-editor/property-list-card/property-type-form/expected-value-selector.tsx @@ -1,15 +1,20 @@ -import type { BaseUrl, VersionedUrl } from "@blockprotocol/type-system/slim"; +import type { + BaseUrl, + DataType, + VersionedUrl, +} from "@blockprotocol/type-system/slim"; import { - AutocompleteDropdown, + buildDataTypeTreesForSelector, Button, Chip, - FontAwesomeIcon, + DataTypeSelector, StyledPlusCircleIcon, TextField, } from "@hashintel/design-system"; import { fluidFontClassName } from "@hashintel/design-system/theme"; -import type { PaperProps } from "@mui/material"; -import { Autocomplete, Box, Typography } from "@mui/material"; +// eslint-disable-next-line no-restricted-imports -- TODO needs fixing to use this package outside the repo +import { blockProtocolDataTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; +import { Autocomplete, Paper, Typography } from "@mui/material"; import { useMemo, useRef, useState } from "react"; import { FormProvider, @@ -18,31 +23,79 @@ import { useFormContext, useWatch, } from "react-hook-form"; +import { useOutsideClickRef } from "rooks"; import { useDataTypesOptions } from "../../../shared/data-types-options-context"; -import { useStateCallback } from "../../shared/use-state-callback"; import { getExpectedValueDescriptor } from "../shared/get-expected-value-descriptor"; import type { PropertyTypeFormValues } from "../shared/property-type-form-values"; import { CustomExpectedValueBuilder } from "./expected-value-selector/custom-expected-value-builder"; import { ExpectedValueChip } from "./expected-value-selector/expected-value-chip"; -import type { CustomExpectedValueBuilderContextValue } from "./expected-value-selector/shared/custom-expected-value-builder-context"; +import type { ExpectedValueSelectorContextValue } from "./expected-value-selector/shared/expected-value-selector-context"; import { - CustomExpectedValueBuilderContext, - useCustomExpectedValueBuilderContext, -} from "./expected-value-selector/shared/custom-expected-value-builder-context"; + ExpectedValueSelectorContext, + useExpectedValueSelectorContext, +} from "./expected-value-selector/shared/expected-value-selector-context"; import type { ExpectedValueSelectorFormValues } from "./expected-value-selector/shared/expected-value-selector-form-values"; -const ExpectedValueSelectorDropdown = ({ children, ...props }: PaperProps) => { - const { customExpectedValueBuilderOpen, handleEdit } = - useCustomExpectedValueBuilderContext(); +const ExpectedValueSelectorDropdown = () => { + const { + addDataType, + autocompleteFocused, + closeAutocomplete, + customExpectedValueBuilderOpen, + handleEdit, + inputRef, + searchText, + } = useExpectedValueSelectorContext(); + + const { dataTypes } = useDataTypesOptions(); + + const dataTypeOptions = useMemo(() => { + return buildDataTypeTreesForSelector({ + targetDataTypes: dataTypes.filter((dataType) => + dataType.allOf?.some( + ({ $ref }) => $ref === blockProtocolDataTypes.value.dataTypeId, + ), + ), + dataTypePoolById: dataTypes.reduce>( + (acc, dataType) => { + acc[dataType.$id] = dataType; + return acc; + }, + {}, + ), + }); + }, [dataTypes]); + + const [paperRef] = useOutsideClickRef((event) => { + if (!customExpectedValueBuilderOpen && event.target === inputRef?.current) { + return; + } + closeAutocomplete(); + }, autocompleteFocused); return ( - + { + setTimeout(() => { + paperRef(el); + }, 100); + }} + > {customExpectedValueBuilderOpen ? ( ) : ( <> - {children} + { + addDataType(dataTypeId); + }} + searchText={searchText} + /> )} - + ); }; @@ -124,115 +184,132 @@ export const ExpectedValueSelector = ({ const inputRef = useRef(); + const [inputValue, setInputValue] = useState(""); + const [creatingCustomExpectedValue, setCreatingCustomExpectedValue] = - useStateCallback(false); + useState(false); - const customExpectedValueBuilderContextValue = - useMemo((): CustomExpectedValueBuilderContextValue => { - const closeCustomExpectedValueBuilder = () => { - expectedValueSelectorFormMethods.setValue( - "editingExpectedValueIndex", - undefined, - ); + const [autocompleteFocused, setAutocompleteFocused] = useState(false); + + const infrequentlyChangingContextValues = useMemo< + Omit + >(() => { + const closeCustomExpectedValueBuilder = () => { + expectedValueSelectorFormMethods.setValue( + "editingExpectedValueIndex", + undefined, + ); + expectedValueSelectorFormMethods.setValue( + "customExpectedValueId", + undefined, + ); + expectedValueSelectorFormMethods.setValue( + "flattenedCustomExpectedValueList", + {}, + ); + + setCreatingCustomExpectedValue(false); + }; + + return { + addDataType: (dataTypeId: VersionedUrl) => { + if (!expectedValuesField.value.includes(dataTypeId)) { + expectedValuesField.onChange([ + ...expectedValuesField.value, + dataTypeId, + ]); + } + + setInputValue(""); + inputRef.current?.focus(); + }, + autocompleteFocused, + closeAutocomplete: () => { + setAutocompleteFocused(false); + }, + customExpectedValueBuilderOpen: creatingCustomExpectedValue, + inputRef, + handleEdit: (index?: number, id?: string) => { expectedValueSelectorFormMethods.setValue( - "customExpectedValueId", - undefined, + "flattenedCustomExpectedValueList", + propertyTypeFormMethods.getValues("flattenedCustomExpectedValueList"), ); expectedValueSelectorFormMethods.setValue( - "flattenedCustomExpectedValueList", - {}, + "editingExpectedValueIndex", + index, ); - - setCreatingCustomExpectedValue(false, () => { - inputRef.current?.focus(); - }); - }; - - return { - customExpectedValueBuilderOpen: creatingCustomExpectedValue, - handleEdit: (index?: number, id?: string) => { - expectedValueSelectorFormMethods.setValue( - "flattenedCustomExpectedValueList", - propertyTypeFormMethods.getValues( - "flattenedCustomExpectedValueList", - ), - ); - expectedValueSelectorFormMethods.setValue( - "editingExpectedValueIndex", - index, - ); - expectedValueSelectorFormMethods.setValue( + expectedValueSelectorFormMethods.setValue("customExpectedValueId", id); + setCreatingCustomExpectedValue(true); + }, + handleCancelCustomBuilder: () => { + closeCustomExpectedValueBuilder(); + }, + handleSave: () => { + const [customExpectedValueId, editingExpectedValueIndex, newValues] = + expectedValueSelectorFormMethods.getValues([ "customExpectedValueId", - id, - ); - setCreatingCustomExpectedValue(true); - }, - handleCancel: closeCustomExpectedValueBuilder, - handleSave: () => { - const [customExpectedValueId, editingExpectedValueIndex, newValues] = - expectedValueSelectorFormMethods.getValues([ - "customExpectedValueId", - "editingExpectedValueIndex", - "flattenedCustomExpectedValueList", - ]); + "editingExpectedValueIndex", + "flattenedCustomExpectedValueList", + ]); - const existingExpectedValues = - propertyTypeFormMethods.getValues("expectedValues"); + const existingExpectedValues = + propertyTypeFormMethods.getValues("expectedValues"); - if (!customExpectedValueId) { - throw new Error("Cannot save if not editing"); - } + if (!customExpectedValueId) { + throw new Error("Cannot save if not editing"); + } - const expectedValue = getExpectedValueDescriptor( - customExpectedValueId, - newValues, - ); + const expectedValue = getExpectedValueDescriptor( + customExpectedValueId, + newValues, + ); - const newExpectedValues = [...existingExpectedValues]; + const newExpectedValues = [...existingExpectedValues]; - if (editingExpectedValueIndex !== undefined) { - newExpectedValues[editingExpectedValueIndex] = expectedValue; - } else { - newExpectedValues.push(expectedValue); - } - propertyTypeFormMethods.setValue( - "expectedValues", - newExpectedValues, - { shouldDirty: true }, - ); - propertyTypeFormMethods.setValue( - "flattenedCustomExpectedValueList", - newValues, - { shouldDirty: true }, - ); - closeCustomExpectedValueBuilder(); - }, + if (editingExpectedValueIndex !== undefined) { + newExpectedValues[editingExpectedValueIndex] = expectedValue; + } else { + newExpectedValues.push(expectedValue); + } + propertyTypeFormMethods.setValue("expectedValues", newExpectedValues, { + shouldDirty: true, + }); + propertyTypeFormMethods.setValue( + "flattenedCustomExpectedValueList", + newValues, + { shouldDirty: true }, + ); + closeCustomExpectedValueBuilder(); + }, + }; + }, [ + autocompleteFocused, + creatingCustomExpectedValue, + expectedValuesField, + expectedValueSelectorFormMethods, + propertyTypeFormMethods, + setCreatingCustomExpectedValue, + ]); + + const expectedValueSelectorContextValue = + useMemo((): ExpectedValueSelectorContextValue => { + return { + ...infrequentlyChangingContextValues, + searchText: inputValue, }; - }, [ - creatingCustomExpectedValue, - expectedValueSelectorFormMethods, - propertyTypeFormMethods, - setCreatingCustomExpectedValue, - ]); + }, [infrequentlyChangingContextValues, inputValue]); const { customExpectedValueBuilderOpen, handleEdit } = - customExpectedValueBuilderContextValue; + expectedValueSelectorContextValue; const creatingExpectedValue = useWatch({ control: expectedValueSelectorFormMethods.control, name: "customExpectedValueId", }); - const [inputValue, setInputValue] = useState(""); - - const [autocompleteFocused, setAutocompleteFocused] = useState(false); - - const { dataTypes, getExpectedValueDisplay } = useDataTypesOptions(); - const dataTypeOptions = dataTypes.map((option) => option.$id); - return ( - { - return options.filter((option) => { - const lowercaseInput = searchValue.toLowerCase(); - - if (typeof option === "object") { - return option.typeId.toLowerCase().includes(lowercaseInput); - } - const dataType = dataTypes.find((dt) => dt.$id === option); - if (!dataType) { - return option.toLowerCase().includes(lowercaseInput); - } - const { description, title } = dataType; - - const leftLabel = dataType.label?.left ?? ""; - const rightLabel = dataType.label?.right ?? ""; - - return ( - (description && - description.toLowerCase().includes(lowercaseInput)) || - title.toLowerCase().includes(lowercaseInput) || - leftLabel.toLowerCase().includes(lowercaseInput) || - rightLabel.toLowerCase().includes(lowercaseInput) - ); - }); - }} onFocus={() => { setAutocompleteFocused(true); }} onBlur={() => { expectedValuesField.onBlur(); - setAutocompleteFocused(false); }} onChange={(_evt, data, reason) => { if (reason !== "createOption") { expectedValuesField.onChange(data); + setInputValue(""); } return false; }} @@ -290,9 +342,8 @@ export const ExpectedValueSelector = ({ setInputValue(value); } }} - freeSolo - renderTags={(expectedValues, getTagProps) => - expectedValues.map((expectedValue, index) => { + renderTags={(expectedValues, getTagProps) => { + return expectedValues.map((expectedValue, index) => { const typeId = typeof expectedValue === "object" ? expectedValue.typeId @@ -317,8 +368,8 @@ export const ExpectedValueSelector = ({ }} /> ); - }) - } + }); + }} renderInput={(inputProps) => ( )} sx={{ width: "70%" }} - options={dataTypeOptions} - getOptionLabel={(opt) => - getExpectedValueDisplay( - typeof opt === "object" ? opt.typeId : (opt as VersionedUrl), - ).title - } + options={[]} disableCloseOnSelect - renderOption={(optProps, opt) => { - const typeId = typeof opt === "object" ? opt.typeId : opt; - - return ( - - ({ color: theme.palette.gray[50] })} - /> - theme.palette.gray[80]} - > - {getExpectedValueDisplay(typeId).title} - - - - ); - }} componentsProps={{ popper: { className: fluidFontClassName, sx: { minWidth: 520 }, placement: "bottom-start", modifiers: [ + { + name: "computeStyles", + enabled: true, + }, { name: "preventOverflow", enabled: true, @@ -382,6 +409,6 @@ export const ExpectedValueSelector = ({ }} /> - + ); }; diff --git a/libs/@hashintel/type-editor/src/entity-type-editor/property-list-card/property-type-form/expected-value-selector/custom-expected-value-builder.tsx b/libs/@hashintel/type-editor/src/entity-type-editor/property-list-card/property-type-form/expected-value-selector/custom-expected-value-builder.tsx index 0c4f036f498..bdf03b489d5 100644 --- a/libs/@hashintel/type-editor/src/entity-type-editor/property-list-card/property-type-form/expected-value-selector/custom-expected-value-builder.tsx +++ b/libs/@hashintel/type-editor/src/entity-type-editor/property-list-card/property-type-form/expected-value-selector/custom-expected-value-builder.tsx @@ -22,7 +22,7 @@ import { useFormContext, useWatch } from "react-hook-form"; import { getDefaultExpectedValue } from "../../shared/default-expected-value"; import { ArrayExpectedValueBuilder } from "./custom-expected-value-builder/array-expected-value-builder"; -import { useCustomExpectedValueBuilderContext } from "./shared/custom-expected-value-builder-context"; +import { useExpectedValueSelectorContext } from "./shared/expected-value-selector-context"; import type { ExpectedValueSelectorFormValues } from "./shared/expected-value-selector-form-values"; import { ObjectExpectedValueBuilder } from "./shared/object-expected-value-builder"; @@ -47,7 +47,7 @@ const ExpectedValueBuilder: FunctionComponent = ({ }) => { const { control } = useFormContext(); - const { handleCancel } = useCustomExpectedValueBuilderContext(); + const { handleCancelCustomBuilder } = useExpectedValueSelectorContext(); const customDataType = useWatch({ control, @@ -59,14 +59,14 @@ const ExpectedValueBuilder: FunctionComponent = ({ return ( ); case "object": return ( ); default: @@ -75,7 +75,8 @@ const ExpectedValueBuilder: FunctionComponent = ({ }; export const CustomExpectedValueBuilder: FunctionComponent = () => { - const { handleSave, handleCancel } = useCustomExpectedValueBuilderContext(); + const { handleSave, handleCancelCustomBuilder } = + useExpectedValueSelectorContext(); const { getValues, setValue, control } = useFormContext(); @@ -118,7 +119,7 @@ export const CustomExpectedValueBuilder: FunctionComponent = () => {