Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/concrete-utopia/utopia in…
Browse files Browse the repository at this point in the history
…to spike/plugin-prop-setter
  • Loading branch information
bkrmendy committed Oct 24, 2024
2 parents ea8ad1b + a689345 commit c4748e7
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ const gridDrawToInsertStrategyInner =
),
updateHighlightedViews('mid-interaction', [targetParent]),
],
[targetParent],
[],
)
}

Expand Down
83 changes: 81 additions & 2 deletions editor/src/components/canvas/canvas-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ import type {
HighlightBoundsForUids,
ExportsDetail,
} from '../../core/shared/project-file-types'
import { isExportDefault, isParseSuccess, isTextFile } from '../../core/shared/project-file-types'
import {
importsEquals,
isExportDefault,
isParseSuccess,
isTextFile,
} from '../../core/shared/project-file-types'
import {
applyUtopiaJSXComponentsChanges,
getDefaultExportedTopLevelElement,
Expand Down Expand Up @@ -140,7 +145,11 @@ import { getStoryboardUID } from '../../core/model/scene-utils'
import { optionalMap } from '../../core/shared/optional-utils'
import { assertNever, fastForEach } from '../../core/shared/utils'
import type { ProjectContentTreeRoot } from '../assets'
import { getProjectFileByFilePath } from '../assets'
import {
getProjectFileByFilePath,
isProjectContentDirectory,
isProjectContentFile,
} from '../assets'
import type { CSSNumber } from '../inspector/common/css-utils'
import { parseCSSLengthPercent, printCSSNumber } from '../inspector/common/css-utils'
import { getTopLevelName, importedFromWhere } from '../editor/import-utils'
Expand Down Expand Up @@ -2180,3 +2189,73 @@ export function canvasPanelOffsets(): {
right: inspector?.clientWidth ?? 0,
}
}

export function projectContentsSameForRefreshRequire(
oldProjectContents: ProjectContentTreeRoot,
newProjectContents: ProjectContentTreeRoot,
): boolean {
if (oldProjectContents === newProjectContents) {
// Identical references, so the imports are the same.
return true
} else {
for (const [filename, oldProjectTree] of Object.entries(oldProjectContents)) {
const newProjectTree = newProjectContents[filename]
// No need to check these further if they have the same reference.
if (oldProjectTree === newProjectTree) {
continue
}
// If the file can't be found in the other tree, the imports are not the same.
if (newProjectTree == null) {
return false
}
if (isProjectContentFile(oldProjectTree) && isProjectContentFile(newProjectTree)) {
// Both entries are files.
const oldContent = oldProjectTree.content
const newContent = newProjectTree.content
if (isTextFile(oldContent) || isTextFile(newContent)) {
if (isTextFile(oldContent) && isTextFile(newContent)) {
const oldParsed = oldContent.fileContents.parsed
const newParsed = newContent.fileContents.parsed
if (isParseSuccess(oldParsed) || isParseSuccess(newParsed)) {
if (isParseSuccess(oldParsed) && isParseSuccess(newParsed)) {
if (
!importsEquals(oldParsed.imports, newParsed.imports) ||
oldParsed.combinedTopLevelArbitraryBlock !==
newParsed.combinedTopLevelArbitraryBlock ||
oldParsed.exportsDetail !== newParsed.exportsDetail
) {
// For the same file the imports, combined top
// level arbitrary block or exports have changed.
return false
}
} else {
// One of the files is a parse success but the other is not.
return false
}
}
} else {
// One of the files is a text file but the other is not.
return false
}
}
} else if (
isProjectContentDirectory(oldProjectTree) &&
isProjectContentDirectory(newProjectTree)
) {
// Both entries are directories.
if (
!projectContentsSameForRefreshRequire(oldProjectTree.children, newProjectTree.children)
) {
// The imports of the subdirectories differ.
return false
}
} else {
// One of the entries is a file and the other is a directory.
return false
}
}
}

// If nothing differs, return true.
return true
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,18 @@ describe('Re-mounting is avoided when', () => {

await switchToLiveMode(renderResult)

function checkClicky(expectedContentText: string): void {
const clicky = renderResult.renderedDOM.getByTestId('clicky')
expect(clicky.innerText).toEqual(expectedContentText)
}

// Ensure we can find the original text
expect(renderResult.renderedDOM.queryByText('Clicked 0 times')).not.toBeNull()
checkClicky('Clicked 0 times')

await clickButton(renderResult)

// Ensure it has been updated
expect(renderResult.renderedDOM.queryByText('Clicked 1 times')).not.toBeNull()
checkClicky('Clicked 1 times')

// Update the top level arbitrary JS block
await updateCode(
Expand All @@ -231,7 +236,7 @@ describe('Re-mounting is avoided when', () => {
)

// Check that it has updated without resetting the state
expect(renderResult.renderedDOM.queryByText('Clicked: 1 times')).not.toBeNull()
checkClicky('Clicked: 1 times')

// Update the component itself
await updateCode(
Expand All @@ -241,7 +246,7 @@ describe('Re-mounting is avoided when', () => {
)

// Check again that it has updated without resetting the state
expect(renderResult.renderedDOM.queryByText('Clicked: 1 times!')).not.toBeNull()
checkClicky('Clicked: 1 times!')
})

it('arbitrary JS or a component is edited in a remix project', async () => {
Expand Down
25 changes: 11 additions & 14 deletions editor/src/components/canvas/ui-jsx-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ import {
} from './ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope'
import { applyUIDMonkeyPatch } from '../../utils/canvas-react-utils'
import type { RemixValidPathsGenerationContext } from './canvas-utils'
import { getParseSuccessForFilePath, getValidElementPaths } from './canvas-utils'
import {
projectContentsSameForRefreshRequire,
getParseSuccessForFilePath,
getValidElementPaths,
} from './canvas-utils'
import { arrayEqualsByValue, fastForEach, NO_OP } from '../../core/shared/utils'
import {
AlwaysFalse,
Expand Down Expand Up @@ -360,20 +364,13 @@ export const UiJsxCanvas = React.memo<UiJsxCanvasPropsWithErrorCallback>((props)

useClearSpyMetadataOnRemount(props.invalidatedCanvasData, isRemounted, metadataContext)

const elementsToRerenderRef = React.useRef(ElementsToRerenderGLOBAL.current)
const shouldRerenderRef = React.useRef(false)
shouldRerenderRef.current =
ElementsToRerenderGLOBAL.current === 'rerender-all-elements' ||
elementsToRerenderRef.current === 'rerender-all-elements' || // TODO this means the first drag frame will still be slow, figure out a nicer way to immediately switch to true. probably this should live in a dedicated a function
!arrayEqualsByValue(
ElementsToRerenderGLOBAL.current,
elementsToRerenderRef.current,
EP.pathsEqual,
) // once we get here, we know that both `ElementsToRerenderGLOBAL.current` and `elementsToRerenderRef.current` are arrays
elementsToRerenderRef.current = ElementsToRerenderGLOBAL.current

const maybeOldProjectContents = React.useRef(projectContents)
if (shouldRerenderRef.current) {

const projectContentsSimilarEnough = projectContentsSameForRefreshRequire(
maybeOldProjectContents.current,
projectContents,
)
if (!projectContentsSimilarEnough) {
maybeOldProjectContents.current = projectContents
}

Expand Down
39 changes: 33 additions & 6 deletions editor/src/components/inspector/flex-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { jsx } from '@emotion/react'
import React from 'react'
import { createSelector } from 'reselect'
import { unless, when } from '../../utils/react-conditionals'
import { when } from '../../utils/react-conditionals'
import { Substores, useEditorState, useRefEditorState } from '../editor/store/store-hook'
import { AddRemoveLayoutSystemControl } from './add-remove-layout-system-control'
import { FlexDirectionToggle } from './flex-direction-control'
Expand Down Expand Up @@ -554,9 +554,15 @@ function AxisDimensionControl({
opener: (isOpen: boolean) => React.ReactElement
}) {
const testId = `grid-dimension-${axis}-${index}`
const [isOpen, setIsOpen] = React.useState(false)
const onOpenChange = React.useCallback((isDropdownOpen: boolean) => {
setIsOpen(isDropdownOpen)
const [isDotsMenuOpen, setDotsMenuOpen] = React.useState(false)
const [isTitleMenuOpen, setTitleMenuOpen] = React.useState(false)

const onOpenChangeDotsMenu = React.useCallback((isDropdownOpen: boolean) => {
setDotsMenuOpen(isDropdownOpen)
}, [])

const onOpenChangeTitleMenu = React.useCallback(() => {
setTitleMenuOpen(false)
}, [])

const isDynamic = React.useMemo(() => {
Expand Down Expand Up @@ -584,6 +590,14 @@ function AxisDimensionControl({
setIsHovered(false)
}, [])

const onContextMenuTitle = React.useCallback((e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setTitleMenuOpen(true)
}, [])

const invisibleOpener = React.useCallback(() => null, [])

return (
<div
key={`col-${value}-${index}`}
Expand Down Expand Up @@ -611,8 +625,16 @@ function AxisDimensionControl({
whiteSpace: 'nowrap',
}}
title={isDynamic ? dynamicIndexTitle : undefined}
onContextMenu={onContextMenuTitle}
>
{title}
<DropdownMenu
align='start'
items={items}
opener={invisibleOpener}
onOpenChange={onOpenChangeTitleMenu}
forceOpen={isTitleMenuOpen}
/>
</Subdued>
<GridExpressionInput
testId={testId}
Expand All @@ -625,9 +647,14 @@ function AxisDimensionControl({
defaultValue={gridCSSKeyword(cssKeyword('auto'), null)}
/>
{when(
(isHovered && !gridExpressionInputFocused.focused) || isOpen,
(isHovered && !gridExpressionInputFocused.focused) || isDotsMenuOpen,
<SquareButton>
<DropdownMenu align='end' items={items} opener={opener} onOpenChange={onOpenChange} />
<DropdownMenu
align='end'
items={items}
opener={opener}
onOpenChange={onOpenChangeDotsMenu}
/>
</SquareButton>,
)}
</div>
Expand Down
15 changes: 9 additions & 6 deletions editor/src/uuiui/inputs/grid-expression-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import { Icons, SmallerIcons } from '../icons'
import { NO_OP } from '../../core/shared/utils'
import { unless } from '../../utils/react-conditionals'
import { useColorTheme } from '../styles/theme'
import { useColorTheme, UtopiaTheme } from '../styles/theme'

interface GridExpressionInputProps {
testId: string
Expand Down Expand Up @@ -164,23 +164,25 @@ export const GridExpressionInput = React.memo(
return gridDimensionsAreEqual(value, defaultValue)
}, [value, defaultValue])

const highlightBorder = dropdownOpen || inputFocused

return (
<div
style={style}
css={{
borderRadius: 2,
borderRadius: UtopiaTheme.inputBorderRadius,
display: 'flex',
alignItems: 'center',
flexGrow: 1,
flexDirection: 'row',
boxShadow: `inset 0px 0px 0px 1px ${
highlightBorder ? colorTheme.dynamicBlue.value : 'transparent'
}`,
'&:hover': {
boxShadow: `inset 0px 0px 0px 1px ${
dropdownOpen ? colorTheme.dynamicBlue.value : colorTheme.fg7.value
highlightBorder ? colorTheme.dynamicBlue.value : colorTheme.fg7.value
}`,
},
'&:focus-within': {
boxShadow: `inset 0px 0px 0px 1px ${colorTheme.dynamicBlue.value}`,
},
}}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
Expand All @@ -198,6 +200,7 @@ export const GridExpressionInput = React.memo(
width: inputFocused ? '100%' : `calc(100% - ${DropdownWidth}px)`,
}}
css={{ color: isDefault ? colorTheme.fg6.value : colorTheme.fg0.value }}
ellipsize={true}
/>
{unless(
inputFocused,
Expand Down
11 changes: 11 additions & 0 deletions editor/src/uuiui/inputs/string-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { jsx } from '@emotion/react'
import styled from '@emotion/styled'
import composeRefs from '@seznam/compose-react-refs'
import type { CSSProperties } from 'react'
import React from 'react'
import type { ControlStatus } from '../../components/inspector/common/control-status'
import type { ControlStyles } from '../../components/inspector/common/control-styles'
Expand Down Expand Up @@ -34,6 +35,7 @@ export interface StringInputProps
pasteHandler?: boolean
showBorder?: boolean
innerStyle?: React.CSSProperties
ellipsize?: boolean
}

export const StringInput = React.memo(
Expand All @@ -49,6 +51,7 @@ export const StringInput = React.memo(
DEPRECATED_labelBelow: labelBelow,
testId,
showBorder,
ellipsize,
...inputProps
},
propsRef,
Expand Down Expand Up @@ -88,6 +91,13 @@ export const StringInput = React.memo(

const placeholder = getControlStylesAwarePlaceholder(controlStyles) ?? initialPlaceHolder

let inputStyle: CSSProperties = {}
if (ellipsize) {
inputStyle.textOverflow = 'ellipsis'
inputStyle.whiteSpace = 'nowrap'
inputStyle.overflow = 'hidden'
}

return (
<form
autoComplete='off'
Expand Down Expand Up @@ -142,6 +152,7 @@ export const StringInput = React.memo(
autoComplete='off'
spellCheck={false}
growInputAutomatically={inputProps.growInputAutomatically}
style={inputStyle}
/>
{labelBelow == null ? null : (
<LabelBelow htmlFor={inputProps.id} style={{ color: controlStyles.secondaryColor }}>
Expand Down
4 changes: 3 additions & 1 deletion editor/src/uuiui/radix-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Icons, SmallerIcons } from './icons'
import { when } from '../utils/react-conditionals'
import { Icn, type IcnProps } from './icn'
import { forceNotNull } from '../core/shared/optional-utils'
import { usePropControlledStateV2 } from '../components/inspector/common/inspector-utils'

// Keep this in sync with the radix-components-portal div in index.html.
export const RadixComponentsPortalId = 'radix-components-portal'
Expand Down Expand Up @@ -90,6 +91,7 @@ export interface DropdownMenuProps {
alignOffset?: number
onOpenChange?: (open: boolean) => void
style?: CSSProperties
forceOpen?: boolean
}

export const ItemContainerTestId = (id: string) => `item-container-${id}`
Expand All @@ -103,7 +105,7 @@ export const DropdownMenu = React.memo<DropdownMenuProps>((props) => {
}, [])
const onEscapeKeyDown = React.useCallback((e: KeyboardEvent) => e.stopPropagation(), [])

const [open, setOpen] = React.useState(false)
const [open, setOpen] = usePropControlledStateV2(props.forceOpen || false)

const shouldShowCheckboxes = props.items.some(
(i) => !isSeparatorDropdownMenuItem(i) && i.checked != null,
Expand Down

0 comments on commit c4748e7

Please sign in to comment.