From d49199815e5526a56a055e5d60dcfbfc7572c64c Mon Sep 17 00:00:00 2001 From: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:11:32 +0200 Subject: [PATCH] Fix/data picker fixes more (#5832) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An assortment of small data picker fixes: - flipping breadcrumbs and the "data path input field", because the logical scope is breadcrumb -> data path, for example: `Reviews > data.reviews[0].quote`. How it is on master makes it look like we are inside data.reviews[0].quote and we have options File > Reviews to further narrow the scope, which is backwards. - adding an empty state - various minor css tweaks - not allowing clicking on breadcrumbs that point at an empty scope slice, these breadcrumbs are also subdued - fixing vite hot reload for tweaking the data modal by unexporting `function pathBreadcrumbs` - When opening the data picker with an already selected data, navigate into the object hierarchy to be able to show the selected Cartouche - Outlet Name Hack – if we find that the Scope Breadcrumb would be named Outlet, we try to manually look up the component name from the parsed model --- .../component-section/data-picker-utils.tsx | 40 ++++-- .../component-section/data-selector-modal.tsx | 117 +++++++++++++----- editor/src/core/data-tracing/data-tracing.ts | 26 ++-- .../src/core/model/element-template-utils.ts | 14 +++ 4 files changed, 135 insertions(+), 62 deletions(-) diff --git a/editor/src/components/inspector/sections/component-section/data-picker-utils.tsx b/editor/src/components/inspector/sections/component-section/data-picker-utils.tsx index 544a3d578f61..50a0b31cb8b3 100644 --- a/editor/src/components/inspector/sections/component-section/data-picker-utils.tsx +++ b/editor/src/components/inspector/sections/component-section/data-picker-utils.tsx @@ -1,6 +1,7 @@ import { atom } from 'jotai' import { processJSPropertyAccessors } from '../../../../core/data-tracing/data-tracing' import { MetadataUtils } from '../../../../core/model/element-metadata-utils' +import { findContainingComponentForPathInProjectContents } from '../../../../core/model/element-template-utils' import { foldEither } from '../../../../core/shared/either' import * as EP from '../../../../core/shared/element-path' import type { ElementPathTrees } from '../../../../core/shared/element-path-tree' @@ -12,9 +13,10 @@ import { } from '../../../../core/shared/element-template' import type { ElementPath } from '../../../../core/shared/project-file-types' import { assertNever } from '../../../../core/shared/utils' +import type { ProjectContentTreeRoot } from '../../../assets' +import { insertionCeilingToString, type FileRootPath } from '../../../canvas/ui-jsx-canvas' import type { AllElementProps } from '../../../editor/store/editor-state' import type { ArrayInfo, JSXInfo, ObjectInfo, PrimitiveInfo } from './variables-in-scope-utils' -import { insertionCeilingToString, type FileRootPath } from '../../../canvas/ui-jsx-canvas' interface VariableOptionBase { depth: number @@ -82,6 +84,7 @@ export function getEnclosingScopes( metadata: ElementInstanceMetadataMap, allElementProps: AllElementProps, elementPathTree: ElementPathTrees, + projectContents: ProjectContentTreeRoot, buckets: Array, lowestInsertionCeiling: ElementPath, ): Array<{ @@ -105,12 +108,7 @@ export function getEnclosingScopes( ) { result.unshift({ insertionCeiling: current, - label: MetadataUtils.getElementLabel( - allElementProps, - parentOfCurrent, - elementPathTree, - metadata, - ), + label: outletNameHack(metadata, allElementProps, elementPathTree, projectContents, current), hasContent: buckets.includes(insertionCeilingToString(current)), }) continue @@ -120,12 +118,7 @@ export function getEnclosingScopes( if (buckets.includes(insertionCeilingToString(current))) { result.unshift({ insertionCeiling: current, - label: MetadataUtils.getElementLabel( - allElementProps, - parentOfCurrent, - elementPathTree, - metadata, - ), + label: outletNameHack(metadata, allElementProps, elementPathTree, projectContents, current), hasContent: true, }) continue @@ -141,3 +134,24 @@ export function getEnclosingScopes( return result } + +function outletNameHack( + metadata: ElementInstanceMetadataMap, + allElementProps: AllElementProps, + elementPathTree: ElementPathTrees, + projectContents: ProjectContentTreeRoot, + target: ElementPath, +): string { + const namePossiblyOutlet = MetadataUtils.getElementLabel( + allElementProps, + EP.parentPath(target), + elementPathTree, + metadata, + ) + if (namePossiblyOutlet !== 'Outlet') { + return namePossiblyOutlet + } + // if getElementLabel returned Outlet, we try to find the actual component name by hand – this is a hack and should be removed once the Navigator is capable of showing the correct name + const component = findContainingComponentForPathInProjectContents(target, projectContents) + return component?.name ?? namePossiblyOutlet +} diff --git a/editor/src/components/inspector/sections/component-section/data-selector-modal.tsx b/editor/src/components/inspector/sections/component-section/data-selector-modal.tsx index 154fed1ccbfe..5e38b17d4988 100644 --- a/editor/src/components/inspector/sections/component-section/data-selector-modal.tsx +++ b/editor/src/components/inspector/sections/component-section/data-selector-modal.tsx @@ -3,6 +3,7 @@ import { groupBy, isPrefixOf, last } from '../../../../core/shared/array-utils' import { jsExpressionOtherJavaScriptSimple } from '../../../../core/shared/element-template' import { CanvasContextMenuPortalTargetID, + NO_OP, arrayEqualsByReference, assertNever, } from '../../../../core/shared/utils' @@ -41,6 +42,7 @@ import { Substores, useEditorState } from '../../../editor/store/store-hook' import { optionalMap } from '../../../../core/shared/optional-utils' import type { FileRootPath } from '../../../canvas/ui-jsx-canvas' import { insertionCeilingToString, insertionCeilingsEqual } from '../../../canvas/ui-jsx-canvas' +import { set } from 'objectPath' export const DataSelectorPopupBreadCrumbsTestId = 'data-selector-modal-top-bar' @@ -158,7 +160,14 @@ export const DataSelectorModal = React.memo( lowestMatchingScope, ) const setSelectedScopeCurried = React.useCallback( - (name: ElementPath) => () => setSelectedScope(name), + (name: ElementPath, hasContent: boolean) => () => { + if (hasContent) { + setSelectedScope(name) + setSelectedPath(null) + setHoveredPath(null) + setNavigatedToPath([]) + } + }, [], ) @@ -168,6 +177,8 @@ export const DataSelectorModal = React.memo( selectedScope, ) + const processedVariablesInScope = useProcessVariablesInScope(filteredVariablesInScope) + const elementLabelsWithScopes = useEditorState( Substores.fullStore, (store) => { @@ -175,18 +186,22 @@ export const DataSelectorModal = React.memo( store.editor.jsxMetadata, store.editor.allElementProps, store.editor.elementPathTree, + store.editor.projectContents, Object.keys(scopeBuckets), lowestInsertionCeiling ?? EP.emptyElementPath, ) return scopes.map(({ insertionCeiling, label, hasContent }) => ({ label: label, scope: insertionCeiling, + hasContent: hasContent, })) }, 'DataSelectorModal elementLabelsWithScopes', ) - const [navigatedToPath, setNavigatedToPath] = React.useState([]) + const [navigatedToPath, setNavigatedToPath] = React.useState( + findFirstObjectPathToNavigateTo(processedVariablesInScope, startingSelectedValuePath) ?? [], + ) const [selectedPath, setSelectedPath] = React.useState( startingSelectedValuePath, @@ -237,8 +252,6 @@ export const DataSelectorModal = React.memo( [], ) - const processedVariablesInScope = useProcessVariablesInScope(filteredVariablesInScope) - const focusedVariableChildren = React.useMemo(() => { if (navigatedToPath.length === 0) { return filteredVariablesInScope @@ -398,6 +411,32 @@ export const DataSelectorModal = React.memo( ...style, }} > + {/* Scope Selector Breadcrumbs */} + + {elementLabelsWithScopes.map(({ label, scope, hasContent }, idx, a) => ( + + + {label} + + {idx < a.length - 1 ? ( + {'/'} + ) : null} + + ))} + {/* top bar */} @@ -456,6 +495,7 @@ export const DataSelectorModal = React.memo( {/* Value preview */} {valuePreviewText} - - {elementLabelsWithScopes.map(({ label, scope }, idx, a) => ( - - - {label} - - {idx < a.length - 1 ? ( - {'/'} - ) : null} - - ))} - + {/* detail view */} ( ( ( ))} + {/* Empty State */} + {when( + focusedVariableChildren.length === 0, + + We did not find any insertable data + , + )} @@ -716,7 +747,7 @@ function childVars(option: DataPickerOption, indices: ArrayIndexLookup): DataPic } } -export function pathBreadcrumbs( +function pathBreadcrumbs( valuePath: DataPickerOption['valuePath'], processedVariablesInScope: ProcessedVariablesInScope, ): Array<{ @@ -797,3 +828,29 @@ function getSelectedScopeFromBuckets( return null } + +function findFirstObjectPathToNavigateTo( + processedVariablesInScope: ProcessedVariablesInScope, + selectedValuePath: ObjectPath | null, +): ObjectPath | null { + if (selectedValuePath == null) { + return null + } + + let currentPath = selectedValuePath + while (currentPath.length > 0) { + const parentPath = currentPath.slice(0, -1) + const parentOption = processedVariablesInScope[parentPath.toString()] + const grandParentPath = currentPath.slice(0, -2) + const grandParentOption = processedVariablesInScope[grandParentPath.toString()] + if (grandParentOption != null && grandParentOption.type === 'array') { + return grandParentPath.slice(0, -1) + } + if (parentOption != null && parentOption.type === 'object') { + return parentPath.slice(0, -1) + } + currentPath = currentPath.slice(0, -1) + } + + return null +} diff --git a/editor/src/core/data-tracing/data-tracing.ts b/editor/src/core/data-tracing/data-tracing.ts index 4c4b2fccd52c..e42825b7e93a 100644 --- a/editor/src/core/data-tracing/data-tracing.ts +++ b/editor/src/core/data-tracing/data-tracing.ts @@ -4,7 +4,10 @@ import { findUnderlyingTargetComponentImplementationFromImportInfo } from '../.. import { withUnderlyingTarget } from '../../components/editor/store/editor-state' import * as TPP from '../../components/template-property-path' import { MetadataUtils } from '../model/element-metadata-utils' -import { findContainingComponentForPath } from '../model/element-template-utils' +import { + findContainingComponentForPath, + findContainingComponentForPathInProjectContents, +} from '../model/element-template-utils' import { mapFirstApplicable } from '../shared/array-utils' import type { Either } from '../shared/either' import { isLeft, isRight, left, mapEither, maybeEitherToMaybe, right } from '../shared/either' @@ -214,19 +217,6 @@ export type DataTracingResult = | DataTracingToElementAtScope | DataTracingFailed -function findContainingComponentForElementPath( - elementPath: ElementPath, - projectContents: ProjectContentTreeRoot, -): UtopiaJSXComponent | null { - return withUnderlyingTarget(elementPath, projectContents, null, (success) => { - const containingComponent = findContainingComponentForPath( - success.topLevelElements, - elementPath, - ) - return containingComponent - }) -} - export function processJSPropertyAccessors( expression: JSExpression, ): Either }> { @@ -438,7 +428,7 @@ export function traceDataFromVariableName( if (enclosingScope.type === 'file-root') { return dataTracingFailed('Cannot trace data from variable name in file root') } - const componentHoldingElement = findContainingComponentForElementPath( + const componentHoldingElement = findContainingComponentForPathInProjectContents( enclosingScope, projectContents, ) @@ -466,10 +456,8 @@ function traceDataFromIdentifierOrAccess( projectContents: ProjectContentTreeRoot, pathDrillSoFar: DataPathPositiveResult, ): DataTracingResult { - const componentHoldingElement: UtopiaJSXComponent | null = findContainingComponentForElementPath( - enclosingScope, - projectContents, - ) + const componentHoldingElement: UtopiaJSXComponent | null = + findContainingComponentForPathInProjectContents(enclosingScope, projectContents) if (componentHoldingElement == null) { return dataTracingFailed('Could not find containing component') diff --git a/editor/src/core/model/element-template-utils.ts b/editor/src/core/model/element-template-utils.ts index 67419ccb6dc9..aaee617e0269 100644 --- a/editor/src/core/model/element-template-utils.ts +++ b/editor/src/core/model/element-template-utils.ts @@ -88,6 +88,7 @@ import { MetadataUtils } from './element-metadata-utils' import { mapValues } from '../shared/object-utils' import type { PropertyControlsInfo } from '../../components/custom-code/code-file' import { getComponentDescriptorForTarget } from '../property-controls/property-controls-utils' +import { withUnderlyingTarget } from '../../components/editor/store/editor-state' export function generateUidWithExistingComponents(projectContents: ProjectContentTreeRoot): string { const mockUID = generateMockNextGeneratedUID() @@ -1657,3 +1658,16 @@ export function findContainingComponentForPath( return null } + +export function findContainingComponentForPathInProjectContents( + elementPath: ElementPath, + projectContents: ProjectContentTreeRoot, +): UtopiaJSXComponent | null { + return withUnderlyingTarget(elementPath, projectContents, null, (success) => { + const containingComponent = findContainingComponentForPath( + success.topLevelElements, + elementPath, + ) + return containingComponent + }) +}