From 519593609f156d192b8cde73d574d04e4d3ad7f6 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:45:03 +0200 Subject: [PATCH 1/4] Fix GridExpressionInput's border radius and highlight color conditions (#6576) **Problem:** The border highlight of selected `GridExpressionInput` components should stay active while the dropdown is open. Also, the border radius should use the theme's value. **Fix:** Fix the inset shadow conditions so that the border is shown as blue if the _text_ input is focused (not the container), or the dropdown is open, even if the mouse is not over the dropdown trigger. In addition, update the border radius size with `UtopiaTheme.inputBorderRadius`. | Before | After | |--------|----------| | ![Kapture 2024-10-22 at 13 30 43](https://github.com/user-attachments/assets/4a56bc21-3fcd-4531-9632-20eb2d3aa6f6) | ![Kapture 2024-10-22 at 13 29 21](https://github.com/user-attachments/assets/15c397d5-db82-4af9-9fd3-9b53ff3032d9) | Fixes #6575 --- editor/src/uuiui/inputs/grid-expression-input.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/editor/src/uuiui/inputs/grid-expression-input.tsx b/editor/src/uuiui/inputs/grid-expression-input.tsx index f4a82b1d7f4b..93701a9e060f 100644 --- a/editor/src/uuiui/inputs/grid-expression-input.tsx +++ b/editor/src/uuiui/inputs/grid-expression-input.tsx @@ -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 @@ -164,23 +164,25 @@ export const GridExpressionInput = React.memo( return gridDimensionsAreEqual(value, defaultValue) }, [value, defaultValue]) + const highlightBorder = dropdownOpen || inputFocused + return (
Date: Tue, 22 Oct 2024 14:45:48 +0200 Subject: [PATCH 2/4] Open grid template row/col inspector menu with right click on title (#6573) **Problem:** It should be possible to open the dropdown/context menu for grid template rows/cols in the inspector by right-clicking the item's title. **Fix:** Add an invisible context menu in the title component and trigger it conditionally by right clicking the title itself. This solution works better than conditionally opening the same dropdown menu as the three dots button because it will correctly position it. https://github.com/user-attachments/assets/2c25fc94-b039-4423-9a3f-0c6e0f71b562 Fixes #6572 --- .../src/components/inspector/flex-section.tsx | 39 ++++++++++++++++--- editor/src/uuiui/radix-components.tsx | 4 +- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/editor/src/components/inspector/flex-section.tsx b/editor/src/components/inspector/flex-section.tsx index 5114495c30a3..5d27487c3357 100644 --- a/editor/src/components/inspector/flex-section.tsx +++ b/editor/src/components/inspector/flex-section.tsx @@ -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' @@ -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(() => { @@ -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 (
{title} + {when( - (isHovered && !gridExpressionInputFocused.focused) || isOpen, + (isHovered && !gridExpressionInputFocused.focused) || isDotsMenuOpen, - + , )}
diff --git a/editor/src/uuiui/radix-components.tsx b/editor/src/uuiui/radix-components.tsx index 86ed787124af..189d596eac52 100644 --- a/editor/src/uuiui/radix-components.tsx +++ b/editor/src/uuiui/radix-components.tsx @@ -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' @@ -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}` @@ -103,7 +105,7 @@ export const DropdownMenu = React.memo((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, From 7ee1484f26b376e1be559b810079250cb2e8daa2 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:51:13 +0200 Subject: [PATCH 3/4] Ellipsize grid expression input (#6578) **Problem:** Overflowing template strings in `GridExpressionInput` components should be truncated and ellipsized. **Fix:** | Before | After | |-------|-------| | Screenshot 2024-10-22 at 15 00 02 | Screenshot 2024-10-22 at 14 59 53 | Fixes #6577 --- editor/src/uuiui/inputs/grid-expression-input.tsx | 1 + editor/src/uuiui/inputs/string-input.tsx | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/editor/src/uuiui/inputs/grid-expression-input.tsx b/editor/src/uuiui/inputs/grid-expression-input.tsx index 93701a9e060f..6407f163b6d2 100644 --- a/editor/src/uuiui/inputs/grid-expression-input.tsx +++ b/editor/src/uuiui/inputs/grid-expression-input.tsx @@ -200,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, diff --git a/editor/src/uuiui/inputs/string-input.tsx b/editor/src/uuiui/inputs/string-input.tsx index c38c162bb491..5cddfd0d280e 100644 --- a/editor/src/uuiui/inputs/string-input.tsx +++ b/editor/src/uuiui/inputs/string-input.tsx @@ -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' @@ -34,6 +35,7 @@ export interface StringInputProps pasteHandler?: boolean showBorder?: boolean innerStyle?: React.CSSProperties + ellipsize?: boolean } export const StringInput = React.memo( @@ -49,6 +51,7 @@ export const StringInput = React.memo( DEPRECATED_labelBelow: labelBelow, testId, showBorder, + ellipsize, ...inputProps }, propsRef, @@ -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 (
{labelBelow == null ? null : ( From a68934598175a6fab29d38abdb2a455188a46968 Mon Sep 17 00:00:00 2001 From: Sean Parsons <217400+seanparsons@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:00:35 +0100 Subject: [PATCH 4/4] fix(canvas) Handle Changing Imports In Canvas (#6567) - Removed the previous handling of `shouldRerenderRef`. - Implemented `projectContentsSameForRefreshRequire`. - Changed out old `haveProjectImportsChanged` call to use the new `projectContentsSameForRefreshRequire`. --- .../grid-draw-to-insert-strategy.tsx | 2 +- editor/src/components/canvas/canvas-utils.ts | 83 ++++++++++++++++++- ...sx-canvas-code-execution.spec.browser2.tsx | 13 ++- .../src/components/canvas/ui-jsx-canvas.tsx | 25 +++--- 4 files changed, 102 insertions(+), 21 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-draw-to-insert-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-draw-to-insert-strategy.tsx index fea428a411d8..e9eaad7b1255 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-draw-to-insert-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-draw-to-insert-strategy.tsx @@ -171,7 +171,7 @@ const gridDrawToInsertStrategyInner = ), updateHighlightedViews('mid-interaction', [targetParent]), ], - [targetParent], + [], ) } diff --git a/editor/src/components/canvas/canvas-utils.ts b/editor/src/components/canvas/canvas-utils.ts index 50b5f71e9ac6..a40fa5ee34b2 100644 --- a/editor/src/components/canvas/canvas-utils.ts +++ b/editor/src/components/canvas/canvas-utils.ts @@ -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, @@ -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' @@ -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 +} diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-code-execution.spec.browser2.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-code-execution.spec.browser2.tsx index f59ebb86be40..bf8132dc9f15 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-code-execution.spec.browser2.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-code-execution.spec.browser2.tsx @@ -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( @@ -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( @@ -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 () => { diff --git a/editor/src/components/canvas/ui-jsx-canvas.tsx b/editor/src/components/canvas/ui-jsx-canvas.tsx index 3f21fb976144..5912a6c1ca57 100644 --- a/editor/src/components/canvas/ui-jsx-canvas.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas.tsx @@ -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, @@ -360,20 +364,13 @@ export const UiJsxCanvas = React.memo((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 }