-
Notifications
You must be signed in to change notification settings - Fork 172
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
**Problem:** The data picker is still somewhat crummy for exploring multiple deep objects in the scope **Fix:** First version of the multi-column data picker! <img width="717" alt="image" src="https://github.com/concrete-utopia/utopia/assets/2226774/ec87b233-a233-4aac-bce2-35efdc55537d"> <img width="716" alt="image" src="https://github.com/concrete-utopia/utopia/assets/2226774/7a75d93d-4ed3-472f-acdc-e1a112fcbcf4"> <img width="715" alt="image" src="https://github.com/concrete-utopia/utopia/assets/2226774/73cb6619-24f6-48a4-be62-457b11982051"> Array picker mode: <img width="713" alt="image" src="https://github.com/concrete-utopia/utopia/assets/2226774/dcd91689-00d5-404a-a1b7-7f12b98ddfa4"> **Commit Details:** - Added `DataSelectorColumns` component and file - Tried to only make minimal changes in `data-selector-modal.tsx` so I only have a small conflict with @bkrmendy - When picking a List Source, we show non-pickable options, but without background - Changed the CartoucheUI so it can have disabled background and disabled highlight **Manual Tests:** I hereby swear that: - [x] I opened a hydrogen project and it loaded - [x] I could navigate to various routes in Preview mode **TODOs for follow up work** - [ ] For some reason some objects are not traversable, they become leafs. I believe this is behavior of the existing code, needs investigation: <img width="729" alt="image" src="https://github.com/concrete-utopia/utopia/assets/2226774/5a1a931f-3e3d-40c1-b0f8-7d05a727e663"> - [ ] Right now we only filter for array type when picking a List Source, we should instead always filter for the correct type - [ ] the value preview column is a placeholder design at best - [ ] there is at least one example in the old demo store where we incorrectly show blue cartouches instead of green: <img width="776" alt="image" src="https://github.com/concrete-utopia/utopia/assets/2226774/cf39960e-55d7-44f5-9b7c-94eef397f393">
- Loading branch information
1 parent
a9b84ca
commit 8dbdfca
Showing
5 changed files
with
411 additions
and
156 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
338 changes: 338 additions & 0 deletions
338
editor/src/components/inspector/sections/component-section/data-selector-columns.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,338 @@ | ||
import styled from '@emotion/styled' | ||
import React from 'react' | ||
import { | ||
dataPathSuccess, | ||
traceDataFromVariableName, | ||
} from '../../../../core/data-tracing/data-tracing' | ||
import { isPrefixOf } from '../../../../core/shared/array-utils' | ||
import { arrayEqualsByReference, assertNever } from '../../../../core/shared/utils' | ||
import { unless, when } from '../../../../utils/react-conditionals' | ||
import { FlexColumn, FlexRow, colorTheme } from '../../../../uuiui' | ||
import { Substores, useEditorState } from '../../../editor/store/store-hook' | ||
import type { CartoucheSource, CartoucheUIProps } from './cartouche-ui' | ||
import { CartoucheUI } from './cartouche-ui' | ||
import type { ArrayOption, DataPickerOption, ObjectOption, ObjectPath } from './data-picker-utils' | ||
import { MapCounterUi } from '../../../navigator/navigator-item/map-counter' | ||
|
||
interface DataSelectorColumnsProps { | ||
activeScope: Array<DataPickerOption> | ||
targetPathInsideScope: ObjectPath | ||
onTargetPathChange: (newTargetPath: ObjectPath) => void | ||
} | ||
|
||
export const DataSelectorColumns = React.memo((props: DataSelectorColumnsProps) => { | ||
return ( | ||
<FlexRow | ||
style={{ | ||
flexGrow: 1, | ||
alignItems: 'flex-start', | ||
overflowX: 'scroll', | ||
overflowY: 'hidden', | ||
scrollbarWidth: 'auto', | ||
scrollbarColor: 'gray transparent', | ||
}} | ||
> | ||
<DataSelectorColumn | ||
activeScope={props.activeScope} | ||
targetPathInsideScope={props.targetPathInsideScope} | ||
onTargetPathChange={props.onTargetPathChange} | ||
currentlyShowingScopeForArray={false} | ||
originalDataForScope={null} | ||
/> | ||
</FlexRow> | ||
) | ||
}) | ||
|
||
interface DataSelectorColumnProps { | ||
activeScope: Array<DataPickerOption> | ||
originalDataForScope: DataPickerOption | null | ||
currentlyShowingScopeForArray: boolean | ||
targetPathInsideScope: ObjectPath | ||
onTargetPathChange: (newTargetPath: ObjectPath) => void | ||
} | ||
|
||
const DataSelectorColumn = React.memo((props: DataSelectorColumnProps) => { | ||
const { activeScope, targetPathInsideScope, currentlyShowingScopeForArray } = props | ||
|
||
const elementOnSelectedPath: DataPickerOption | null = | ||
activeScope.find((option) => isPrefixOf(option.valuePath, targetPathInsideScope)) ?? null | ||
|
||
// if the current scope is an array, we want to show not only the array indexes, but also the contents of the first element of the array | ||
const pseudoSelectedElementForArray: DataPickerOption | null = | ||
elementOnSelectedPath == null && currentlyShowingScopeForArray && activeScope.length > 0 | ||
? activeScope[0] | ||
: null | ||
|
||
const nextColumnScope: ArrayOption | ObjectOption | null = (() => { | ||
const elementToUseForNextColumn = elementOnSelectedPath ?? pseudoSelectedElementForArray | ||
if (elementToUseForNextColumn != null) { | ||
if ( | ||
(elementToUseForNextColumn.type === 'object' || | ||
elementToUseForNextColumn.type === 'array') && | ||
elementToUseForNextColumn.children.length > 0 | ||
) { | ||
return elementToUseForNextColumn | ||
} | ||
} | ||
return null | ||
})() | ||
|
||
const nextColumnScopeValue = nextColumnScope == null ? elementOnSelectedPath : null | ||
|
||
const dataSource = useVariableDataSource(props.originalDataForScope) | ||
|
||
const isLastColumn = nextColumnScope == null && nextColumnScopeValue == null | ||
const columnRef = useScrollIntoView(isLastColumn) | ||
|
||
return ( | ||
<> | ||
<DataSelectorFlexColumn ref={columnRef}> | ||
{activeScope.map((option, index) => { | ||
const selected = arrayEqualsByReference(option.valuePath, targetPathInsideScope) | ||
const onActivePath = isPrefixOf(option.valuePath, targetPathInsideScope) | ||
return ( | ||
<RowWithCartouche | ||
key={option.variableInfo.expression} | ||
data={option} | ||
currentlyShowingScopeForArray={currentlyShowingScopeForArray} | ||
isLeaf={nextColumnScope == null} | ||
selected={selected} | ||
onActivePath={onActivePath} | ||
forceShowArrow={pseudoSelectedElementForArray != null && index === 0} | ||
onTargetPathChange={props.onTargetPathChange} | ||
forcedDataSource={dataSource} | ||
/> | ||
) | ||
})} | ||
</DataSelectorFlexColumn> | ||
{nextColumnScope != null ? ( | ||
<DataSelectorColumn | ||
activeScope={nextColumnScope.children} | ||
targetPathInsideScope={targetPathInsideScope} | ||
onTargetPathChange={props.onTargetPathChange} | ||
currentlyShowingScopeForArray={nextColumnScope.type === 'array'} | ||
originalDataForScope={props.originalDataForScope ?? elementOnSelectedPath} | ||
/> | ||
) : null} | ||
{nextColumnScopeValue != null ? <ValuePreviewColumn data={nextColumnScopeValue} /> : null} | ||
</> | ||
) | ||
}) | ||
|
||
interface ValuePreviewColumnProps { | ||
data: DataPickerOption | ||
} | ||
|
||
const ValuePreviewColumn = React.memo((props: ValuePreviewColumnProps) => { | ||
const text = JSON.stringify(props.data.variableInfo.value, undefined, 2) | ||
const ref = useScrollIntoView(true) | ||
return ( | ||
<DataSelectorFlexColumn ref={ref}> | ||
<div | ||
style={{ | ||
padding: 4, | ||
border: '1px solid #ccc', | ||
borderRadius: 4, | ||
minHeight: 150, | ||
width: 200, | ||
overflowWrap: 'break-word', | ||
overflowX: 'hidden', | ||
overflowY: 'scroll', | ||
scrollbarWidth: 'auto', | ||
scrollbarColor: 'gray transparent', | ||
['textWrap' as any]: 'balance', // I think we need to update some typings here? | ||
}} | ||
> | ||
{text} | ||
</div> | ||
<span>{variableTypeForInfo(props.data)}</span> | ||
</DataSelectorFlexColumn> | ||
) | ||
}) | ||
|
||
function variableTypeForInfo(info: DataPickerOption): string { | ||
switch (info.type) { | ||
case 'array': | ||
return 'Array' | ||
case 'object': | ||
return 'Object' | ||
case 'primitive': | ||
return typeof info.variableInfo.value | ||
case 'jsx': | ||
return 'JSX' | ||
default: | ||
assertNever(info) | ||
} | ||
} | ||
|
||
interface RowWithCartoucheProps { | ||
data: DataPickerOption | ||
currentlyShowingScopeForArray: boolean | ||
selected: boolean | ||
onActivePath: boolean | ||
forceShowArrow: boolean | ||
isLeaf: boolean | ||
forcedDataSource: CartoucheSource | null | ||
onTargetPathChange: (newTargetPath: ObjectPath) => void | ||
} | ||
const RowWithCartouche = React.memo((props: RowWithCartoucheProps) => { | ||
const { | ||
onTargetPathChange, | ||
data, | ||
currentlyShowingScopeForArray, | ||
forcedDataSource, | ||
isLeaf, | ||
selected, | ||
onActivePath, | ||
forceShowArrow, | ||
} = props | ||
const targetPath = data.valuePath | ||
|
||
const dataSource = useVariableDataSource(data) | ||
|
||
const onClick: React.MouseEventHandler<HTMLDivElement> = React.useCallback( | ||
(e) => { | ||
e.stopPropagation() | ||
onTargetPathChange(targetPath) | ||
}, | ||
[targetPath, onTargetPathChange], | ||
) | ||
|
||
const ref = useScrollIntoView(selected) | ||
|
||
return ( | ||
<FlexRow | ||
onClick={onClick} | ||
ref={ref} | ||
style={{ | ||
alignSelf: 'stretch', | ||
justifyContent: 'space-between', | ||
fontSize: 10, | ||
borderRadius: 4, | ||
height: 24, | ||
backgroundColor: onActivePath ? colorTheme.bg4.value : undefined, | ||
padding: 5, | ||
cursor: 'pointer', | ||
}} | ||
> | ||
<span> | ||
<CartoucheUI | ||
key={data.valuePath.toString()} | ||
source={forcedDataSource ?? dataSource ?? 'internal'} | ||
datatype={childTypeToCartoucheDataType(data.type)} | ||
selected={!data.disabled && selected} | ||
highlight={data.disabled ? 'disabled' : null} | ||
role={data.disabled ? 'information' : 'selection'} | ||
testId={`data-selector-option-${data.variableInfo.expression}`} | ||
badge={ | ||
data.type === 'array' ? ( | ||
<MapCounterUi | ||
counterValue={data.variableInfo.elements.length} | ||
overrideStatus='no-override' | ||
selectedStatus={selected} | ||
/> | ||
) : undefined | ||
} | ||
> | ||
{currentlyShowingScopeForArray ? ( | ||
<> | ||
<span style={{ fontStyle: 'italic' }}>item </span> | ||
{data.variableInfo.expressionPathPart} | ||
</> | ||
) : ( | ||
data.variableInfo.expressionPathPart | ||
)} | ||
</CartoucheUI> | ||
</span> | ||
<span | ||
style={{ | ||
color: colorTheme.fg6.value, | ||
opacity: !isLeaf && (onActivePath || forceShowArrow) ? 1 : 0, | ||
}} | ||
> | ||
{'>'} | ||
</span> | ||
</FlexRow> | ||
) | ||
}) | ||
|
||
function useVariableDataSource(variable: DataPickerOption | null) { | ||
return useEditorState( | ||
Substores.projectContentsAndMetadata, | ||
(store) => { | ||
if (variable == null) { | ||
return null | ||
} | ||
const container = variable.variableInfo.insertionCeiling | ||
const trace = traceDataFromVariableName( | ||
container, | ||
variable.variableInfo.expression, | ||
store.editor.jsxMetadata, | ||
store.editor.projectContents, | ||
dataPathSuccess([]), | ||
) | ||
|
||
switch (trace.type) { | ||
case 'hook-result': | ||
return 'external' | ||
case 'literal-attribute': | ||
case 'literal-assignment': | ||
return 'literal' | ||
case 'component-prop': | ||
case 'element-at-scope': | ||
case 'failed': | ||
return 'internal' | ||
break | ||
default: | ||
assertNever(trace) | ||
} | ||
}, | ||
'useVariableDataSource', | ||
) | ||
} | ||
|
||
function childTypeToCartoucheDataType( | ||
childType: DataPickerOption['type'], | ||
): CartoucheUIProps['datatype'] { | ||
switch (childType) { | ||
case 'array': | ||
return 'array' | ||
case 'object': | ||
return 'object' | ||
case 'jsx': | ||
case 'primitive': | ||
return 'renderable' | ||
default: | ||
assertNever(childType) | ||
} | ||
} | ||
|
||
const DataSelectorFlexColumn = styled(FlexColumn)({ | ||
minWidth: 200, | ||
height: '100%', | ||
flexShrink: 0, | ||
overflowX: 'hidden', | ||
overflowY: 'scroll', | ||
scrollbarWidth: 'auto', | ||
scrollbarColor: 'gray transparent', | ||
paddingRight: 10, // to account for scrollbar | ||
paddingLeft: 6, | ||
paddingBottom: 10, | ||
borderRight: `1px solid ${colorTheme.subduedBorder.cssValue}`, | ||
}) | ||
|
||
function useScrollIntoView(shouldScroll: boolean) { | ||
const elementRef = React.useRef<HTMLDivElement>(null) | ||
|
||
React.useEffect(() => { | ||
if (shouldScroll && elementRef.current != null) { | ||
elementRef.current.scrollIntoView({ | ||
behavior: 'instant', | ||
block: 'nearest', | ||
inline: 'nearest', | ||
}) | ||
} | ||
}, [shouldScroll]) | ||
|
||
return elementRef | ||
} |
Oops, something went wrong.