Skip to content

Commit

Permalink
Fix/data picker fixes more (#5832)
Browse files Browse the repository at this point in the history
<img width="718" alt="image"
src="https://github.com/concrete-utopia/utopia/assets/2226774/ee46b634-613d-4e83-8021-f01692d529a3">


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
  • Loading branch information
balazsbajorics authored Jun 5, 2024
1 parent 661c34b commit d491998
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -82,6 +84,7 @@ export function getEnclosingScopes(
metadata: ElementInstanceMetadataMap,
allElementProps: AllElementProps,
elementPathTree: ElementPathTrees,
projectContents: ProjectContentTreeRoot,
buckets: Array<string>,
lowestInsertionCeiling: ElementPath,
): Array<{
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'

Expand Down Expand Up @@ -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([])
}
},
[],
)

Expand All @@ -168,25 +177,31 @@ export const DataSelectorModal = React.memo(
selectedScope,
)

const processedVariablesInScope = useProcessVariablesInScope(filteredVariablesInScope)

const elementLabelsWithScopes = useEditorState(
Substores.fullStore,
(store) => {
const scopes = getEnclosingScopes(
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<ObjectPath>([])
const [navigatedToPath, setNavigatedToPath] = React.useState<ObjectPath>(
findFirstObjectPathToNavigateTo(processedVariablesInScope, startingSelectedValuePath) ?? [],
)

const [selectedPath, setSelectedPath] = React.useState<ObjectPath | null>(
startingSelectedValuePath,
Expand Down Expand Up @@ -237,8 +252,6 @@ export const DataSelectorModal = React.memo(
[],
)

const processedVariablesInScope = useProcessVariablesInScope(filteredVariablesInScope)

const focusedVariableChildren = React.useMemo(() => {
if (navigatedToPath.length === 0) {
return filteredVariablesInScope
Expand Down Expand Up @@ -398,6 +411,32 @@ export const DataSelectorModal = React.memo(
...style,
}}
>
{/* Scope Selector Breadcrumbs */}
<FlexRow style={{ gap: 2, paddingBottom: 8 }}>
{elementLabelsWithScopes.map(({ label, scope, hasContent }, idx, a) => (
<React.Fragment key={`label-${idx}`}>
<div
onClick={setSelectedScopeCurried(scope, hasContent)}
style={{
width: 'max-content',
padding: '2px 4px',
borderRadius: 4,
cursor: hasContent ? 'pointer' : undefined,
color: hasContent
? colorTheme.neutralForeground.value
: colorTheme.subduedForeground.value,
fontSize: 12,
fontWeight: insertionCeilingsEqual(selectedScope, scope) ? 800 : undefined,
}}
>
{label}
</div>
{idx < a.length - 1 ? (
<span style={{ width: 'max-content', padding: '2px 4px' }}>{'/'}</span>
) : null}
</React.Fragment>
))}
</FlexRow>
{/* top bar */}
<FlexRow style={{ justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
<FlexRow style={{ gap: 8, flexWrap: 'wrap', flexGrow: 1 }}>
Expand Down Expand Up @@ -456,6 +495,7 @@ export const DataSelectorModal = React.memo(
{/* Value preview */}
<FlexRow
style={{
flexShrink: 0,
gridColumn: '3',
flexWrap: 'wrap',
gap: 4,
Expand All @@ -467,28 +507,7 @@ export const DataSelectorModal = React.memo(
>
{valuePreviewText}
</FlexRow>
<FlexRow style={{ gap: 2, paddingBottom: 4, paddingTop: 8 }}>
{elementLabelsWithScopes.map(({ label, scope }, idx, a) => (
<React.Fragment key={`label-${idx}`}>
<div
onClick={setSelectedScopeCurried(scope)}
style={{
width: 'max-content',
padding: '2px 4px',
borderRadius: 4,
cursor: 'pointer',
fontSize: 12,
fontWeight: insertionCeilingsEqual(selectedScope, scope) ? 800 : undefined,
}}
>
{label}
</div>
{idx < a.length - 1 ? (
<span style={{ width: 'max-content', padding: '2px 4px' }}>{'/'}</span>
) : null}
</React.Fragment>
))}
</FlexRow>

{/* detail view */}
<div
style={{
Expand Down Expand Up @@ -517,7 +536,6 @@ export const DataSelectorModal = React.memo(
{primitiveVars.map((variable) => (
<CartoucheUI
key={variable.valuePath.toString()}
tooltip={variableNameFromPath(variable)}
source={variableSources[variable.valuePath.toString()] ?? 'internal'}
datatype={childTypeToCartoucheDataType(variable.type)}
inverted={false}
Expand All @@ -543,7 +561,6 @@ export const DataSelectorModal = React.memo(
{folderVars.map((variable, idx) => (
<React.Fragment key={variable.valuePath.toString()}>
<CartoucheUI
tooltip={variableNameFromPath(variable)}
datatype={childTypeToCartoucheDataType(variable.type)}
source={variableSources[variable.valuePath.toString()] ?? 'internal'}
inverted={false}
Expand Down Expand Up @@ -573,7 +590,6 @@ export const DataSelectorModal = React.memo(
{childVars(variable, indexLookup).map((child) => (
<CartoucheUI
key={child.valuePath.toString()}
tooltip={variableNameFromPath(child)}
source={variableSources[variable.valuePath.toString()] ?? 'internal'}
inverted={false}
datatype={childTypeToCartoucheDataType(child.type)}
Expand Down Expand Up @@ -601,6 +617,21 @@ export const DataSelectorModal = React.memo(
) : null}
</React.Fragment>
))}
{/* Empty State */}
{when(
focusedVariableChildren.length === 0,
<div
style={{
gridColumn: '1 / span 3',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 100,
}}
>
We did not find any insertable data
</div>,
)}
</div>
</FlexColumn>
</div>
Expand Down Expand Up @@ -716,7 +747,7 @@ function childVars(option: DataPickerOption, indices: ArrayIndexLookup): DataPic
}
}

export function pathBreadcrumbs(
function pathBreadcrumbs(
valuePath: DataPickerOption['valuePath'],
processedVariablesInScope: ProcessedVariablesInScope,
): Array<{
Expand Down Expand Up @@ -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
}
26 changes: 7 additions & 19 deletions editor/src/core/data-tracing/data-tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, { originalIdentifier: JSIdentifier; path: Array<string | number> }> {
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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')
Expand Down
14 changes: 14 additions & 0 deletions editor/src/core/model/element-template-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
})
}

0 comments on commit d491998

Please sign in to comment.