From e273da9d57e83e546e9cb2d1c0f8bc6d219a5cdf Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 7 Oct 2024 17:18:03 +0200 Subject: [PATCH 01/27] Style IR --- editor/src/components/canvas/canvas-types.ts | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 92033f498e91..88e60cfb51f9 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -26,6 +26,8 @@ import type { import { InteractionSession } from './canvas-strategies/interaction-state' import type { CanvasStrategyId } from './canvas-strategies/canvas-strategy-types' import type { MouseButtonsPressed } from '../../utils/mouse' +import type { FlexDirection } from '../inspector/common/css-utils' +import type { CSSNumberWithRenderedValue } from './controls/select-mode/controls-common' export const CanvasContainerID = 'canvas-container' @@ -533,3 +535,47 @@ export const EdgePositionBottomRight: EdgePosition = { x: 1, y: 1 } export const EdgePositionTopRight: EdgePosition = { x: 1, y: 0 } export type SelectionLocked = 'locked' | 'locked-hierarchy' | 'selectable' + +export type PropertyTag = { type: 'hover' } | { type: 'breakpoint'; name: string } + +export interface FlexGapInfo { + name: 'gap' + gap: CSSNumberWithRenderedValue +} + +export const flexGapInfo = (gap: CSSNumberWithRenderedValue): FlexGapInfo => ({ + name: 'gap', + gap: gap, +}) + +export interface FlexDirectionInfo { + name: 'flexDirection' + flexDirection: FlexDirection +} + +export const flexDirectionInfo = (flexDirection: FlexDirection): FlexDirectionInfo => ({ + name: 'flexDirection', + flexDirection: flexDirection, +}) + +export type StylePropInfo = FlexGapInfo | FlexDirectionInfo + +export interface StyleProperty { + tags: PropertyTag[] + value: T +} + +export const stylePropertyWithTags = ( + tags: PropertyTag[], + value: T, +): StyleProperty => ({ + tags: tags, + value: value, +}) + +export const styleProperty = (value: T): StyleProperty => ({ + tags: [], + value: value, +}) + +export type StyleInfo = Array> From d71097e19ff9926dbe0d4f0d331a3392b40cd0a4 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 7 Oct 2024 17:18:18 +0200 Subject: [PATCH 02/27] plugins --- .../canvas/plugins/inline-style-plugin.ts | 66 ++++++ .../canvas/plugins/style-plugins.ts | 32 +++ .../canvas/plugins/tailwind-style-plugin.ts | 197 ++++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 editor/src/components/canvas/plugins/inline-style-plugin.ts create mode 100644 editor/src/components/canvas/plugins/style-plugins.ts create mode 100644 editor/src/components/canvas/plugins/tailwind-style-plugin.ts diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts new file mode 100644 index 000000000000..492aa4b1466c --- /dev/null +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -0,0 +1,66 @@ +import type { ElementPath } from 'utopia-shared/src/types' +import { getLayoutProperty } from '../../../core/layout/getLayoutProperty' +import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import { defaultEither, isLeft, right } from '../../../core/shared/either' +import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template' +import { isJSXElement } from '../../../core/shared/element-template' +import { styleStringInArray } from '../../../utils/common-constants' +import type { CSSNumber } from '../../inspector/common/css-utils' +import type { FlexGapData } from '../gap-utils' +import type { StylePlugin } from './style-plugins' +import { stripNulls } from '../../../core/shared/array-utils' +import { optionalMap } from '../../../core/shared/optional-utils' +import { flexDirectionInfo, flexGapInfo, styleProperty } from '../canvas-types' + +function maybeFlexGapData( + metadata: ElementInstanceMetadataMap, + elementPath: ElementPath, +): FlexGapData | null { + const element = MetadataUtils.findElementByElementPath(metadata, elementPath) + if ( + element == null || + element.specialSizeMeasurements.display !== 'flex' || + isLeft(element.element) || + !isJSXElement(element.element.value) + ) { + return null + } + + if (element.specialSizeMeasurements.justifyContent?.startsWith('space')) { + return null + } + + const gap = element.specialSizeMeasurements.gap ?? 0 + + const gapFromProps: CSSNumber | undefined = defaultEither( + undefined, + getLayoutProperty('gap', right(element.element.value.props), styleStringInArray), + ) + + const flexDirection = element.specialSizeMeasurements.flexDirection ?? 'row' + + return { + value: { + renderedValuePx: gap, + value: gapFromProps ?? null, + }, + direction: flexDirection, + } +} + +export const InlineStylePlugin: StylePlugin = { + name: 'Inline Style', + styleInfoFactory: + ({ metadata }) => + (elementPath) => { + const flexGapData = maybeFlexGapData(metadata, elementPath) + return stripNulls([ + optionalMap((gap) => styleProperty(flexGapInfo(gap)), flexGapData?.value), + optionalMap( + (direction) => styleProperty(flexDirectionInfo(direction)), + flexGapData?.direction, + ), + ]) + }, + normalizeFromInlineStyle: (editor) => editor, +} diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts new file mode 100644 index 000000000000..bc5fd92fc0bf --- /dev/null +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -0,0 +1,32 @@ +import type { ElementPath } from 'utopia-shared/src/types' +import type { EditorState } from '../../editor/store/editor-state' +import type { StyleInfoFactory } from '../canvas-strategies/canvas-strategy-types' +import { InlineStylePlugin } from './inline-style-plugin' +import { TailwindPlugin } from './tailwind-style-plugin' +import { isTailwindEnabled } from '../../../core/tailwind/tailwind-compilation' + +export interface StylePlugin { + name: string + styleInfoFactory: StyleInfoFactory + normalizeFromInlineStyle: ( + editorState: EditorState, + elementsToNormalize: ElementPath[], + ) => EditorState +} + +export const Plugins = { + InlineStyle: InlineStylePlugin, + Tailwind: TailwindPlugin, +} as const + +export function getActivePlugin( + // The `_editorState` is not used now because `isTailwindEnabled` reads a + // global behind the scenes, it's still passed to make it easy to add new + // cases here without a big refactor + _editorState: EditorState, +): StylePlugin { + if (isTailwindEnabled()) { + return TailwindPlugin + } + return InlineStylePlugin +} diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts new file mode 100644 index 000000000000..61e0e47715ff --- /dev/null +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -0,0 +1,197 @@ +import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' +import type { ElementPath, JSXAttributesEntry } from 'utopia-shared/src/types' +import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import { isLeft } from '../../../core/shared/either' +import type { + ElementInstanceMetadata, + ElementInstanceMetadataMap, +} from '../../../core/shared/element-template' +import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' +import { + getElementFromProjectContents, + getJSXElementFromProjectContents, +} from '../../editor/store/editor-state' +import { cssParsers } from '../../inspector/common/css-utils' +import { foldAndApplyCommandsSimple } from './../commands/commands' +import { mapDropNulls } from '../../../core/shared/array-utils' +import { deleteProperties } from './../commands/delete-properties-command' +import * as PP from '../../../core/shared/property-path' +import { updateClassListCommand } from './../commands/update-class-list-command' +import * as UCL from './../commands/update-class-list-command' +import type { StylePlugin } from './style-plugins' +import type { FlexDirectionInfo, FlexGapInfo, StyleProperty, StylePropInfo } from '../canvas-types' +import { flexDirectionInfo, flexGapInfo, styleProperty } from '../canvas-types' +import { cssNumberWithRenderedValue } from '../controls/select-mode/controls-common' + +function parseTailwindGap( + gapValue: string | null, + instanceMetadata: ElementInstanceMetadata | null, +): StyleProperty | null { + if ( + instanceMetadata == null || + instanceMetadata.specialSizeMeasurements.gap == null || + gapValue == null + ) { + return null + } + + const parsedGapValue = cssParsers.gap(gapValue, null) + if (isLeft(parsedGapValue)) { + return null + } + + return styleProperty( + flexGapInfo( + cssNumberWithRenderedValue( + parsedGapValue.value, + instanceMetadata.specialSizeMeasurements.gap, + ), + ), + ) +} + +function parseTailwindFlexDirection( + directionValue: string | null, +): StyleProperty | null { + if (directionValue == null) { + return null + } + const parsed = cssParsers.flexDirection(directionValue, null) + if (isLeft(parsed)) { + return null + } + + return styleProperty(flexDirectionInfo(parsed.value)) +} + +function parseTailwindClass( + metadata: ElementInstanceMetadataMap, + elementPath: ElementPath, + { property, value }: { property: string; value: string }, +): StyleProperty | null { + switch (property) { + case 'gap': + return parseTailwindGap(value, MetadataUtils.findElementByElementPath(metadata, elementPath)) + case 'flexDirection': + return parseTailwindFlexDirection(value) + default: + return null + } +} + +const TailwindPropertyMapping = { + gap: 'gap', + flexDirection: 'flexDirection', +} as const + +function isSupportedTailwindProperty(prop: unknown): prop is keyof typeof TailwindPropertyMapping { + return typeof prop === 'string' && prop in TailwindPropertyMapping +} + +function stringifiedStylePropValue(value: unknown): string | null { + if (typeof value === 'string') { + return value + } + if (typeof value === 'number') { + return `${value}px` + } + + return null +} + +// TODO: pass down the tailwind config +export const TailwindPlugin: StylePlugin = { + name: 'Tailwind', + styleInfoFactory: + ({ metadata, projectContents }) => + (elementPath) => { + const classList = getClassNameAttribute( + getElementFromProjectContents(elementPath, projectContents), + )?.value + + if (classList == null || typeof classList !== 'string') { + return [] + } + + const classes = classList.split(' ') + + const styleProps = mapDropNulls((tailwindClass) => { + const parsed = TailwindClassParser.parse( + tailwindClass /* TODO pass the tailwind config here */, + ) + if (parsed.kind === 'error' || !isSupportedTailwindProperty(parsed.property)) { + return null + } + + return parseTailwindClass(metadata, elementPath, { + property: parsed.property, + value: parsed.value, + }) + }, classes) + + return styleProps + }, + normalizeFromInlineStyle: (editorState, elementsToNormalize) => { + const commands = elementsToNormalize.flatMap((elementPath) => { + const element = getJSXElementFromProjectContents(elementPath, editorState.projectContents) + if (element == null) { + return [] + } + + const styleAttribute = element.props.find( + (prop): prop is JSXAttributesEntry => + prop.type === 'JSX_ATTRIBUTES_ENTRY' && prop.key === 'style', + ) + if (styleAttribute == null) { + return [] + } + + const styleValue = styleAttribute.value + if (styleValue.type !== 'ATTRIBUTE_NESTED_OBJECT') { + return [] + } + + const styleProps = mapDropNulls( + (c): { key: keyof typeof TailwindPropertyMapping; value: unknown } | null => + c.type === 'PROPERTY_ASSIGNMENT' && + c.value.type === 'ATTRIBUTE_VALUE' && + isSupportedTailwindProperty(c.key) + ? { key: c.key, value: c.value.value } + : null, + styleValue.content, + ) + + const stylePropConversions = mapDropNulls(({ key, value }) => { + const valueString = stringifiedStylePropValue(value) + if (valueString == null) { + return null + } + + const tailwindClass = TailwindClassParser.classname( + { property: TailwindPropertyMapping[key], value: valueString }, + /* TODO pass the tailwind config here */ + ) + if (tailwindClass == null) { + return null + } + return { tailwindClass, key } + }, styleProps) + + return [ + deleteProperties( + 'always', + elementPath, + Object.keys(TailwindPropertyMapping).map((prop) => PP.create('style', prop)), + ), + ...stylePropConversions.map(({ tailwindClass }) => + updateClassListCommand('always', elementPath, UCL.add(tailwindClass)), + ), + ] + }) + if (commands.length === 0) { + return editorState + } + + return foldAndApplyCommandsSimple(editorState, commands) + }, +} From 45dc5b2deda726c14d49c55fd88c4a5e32bca201 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Wed, 9 Oct 2024 17:01:57 +0200 Subject: [PATCH 03/27] plugins --- .../canvas-strategies/canvas-strategies.tsx | 14 ++ .../canvas-strategy-types.ts | 10 ++ .../strategies/set-flex-gap-strategy.tsx | 8 +- editor/src/components/canvas/canvas-types.ts | 48 ++----- .../controls/select-mode/flex-gap-control.tsx | 19 ++- .../select-mode/subdued-flex-gap-controls.tsx | 33 +++-- editor/src/components/canvas/gap-utils.ts | 40 ++---- .../canvas/plugins/inline-style-plugin.ts | 75 ++++------ .../canvas/plugins/style-plugins.ts | 14 +- .../canvas/plugins/tailwind-style-plugin.ts | 128 +++++------------- .../editor/store/dispatch-strategies.tsx | 11 +- .../components/inspector/common/css-utils.ts | 5 +- 12 files changed, 176 insertions(+), 229 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index 29d4ae683ffc..97f5895519ff 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -82,6 +82,7 @@ import { reparentSubjectsForInteractionTarget } from './strategies/reparent-help import { getReparentTargetUnified } from './strategies/reparent-helpers/reparent-strategy-parent-lookup' import { gridRearrangeResizeKeyboardStrategy } from './strategies/grid-rearrange-keyboard-strategy' import createCachedSelector from 're-reselect' +import { getActivePlugin } from '../plugins/style-plugins' export type CanvasStrategyFactory = ( canvasState: InteractionCanvasState, @@ -202,6 +203,7 @@ export function pickCanvasStateFromEditorState( editorState: EditorState, builtInDependencies: BuiltInDependencies, ): InteractionCanvasState { + const activePlugin = getActivePlugin(editorState) return { builtInDependencies: builtInDependencies, interactionTarget: getInteractionTargetFromEditorState(editorState, localSelectedViews), @@ -214,6 +216,11 @@ export function pickCanvasStateFromEditorState( startingElementPathTree: editorState.elementPathTree, startingAllElementProps: editorState.allElementProps, propertyControlsInfo: editorState.propertyControlsInfo, + styleInfoReader: activePlugin.styleInfoFactory({ + projectContents: editorState.projectContents, + metadata: editorState.jsxMetadata, + elementPathTree: editorState.elementPathTree, + }), } } @@ -224,6 +231,8 @@ export function pickCanvasStateFromEditorStateWithMetadata( metadata: ElementInstanceMetadataMap, allElementProps?: AllElementProps, ): InteractionCanvasState { + const activePlugin = getActivePlugin(editorState) + return { builtInDependencies: builtInDependencies, interactionTarget: getInteractionTargetFromEditorState(editorState, localSelectedViews), @@ -236,6 +245,11 @@ export function pickCanvasStateFromEditorStateWithMetadata( startingElementPathTree: editorState.elementPathTree, // IMPORTANT! This isn't based on the passed in metadata startingAllElementProps: allElementProps ?? editorState.allElementProps, propertyControlsInfo: editorState.propertyControlsInfo, + styleInfoReader: activePlugin.styleInfoFactory({ + projectContents: editorState.projectContents, + metadata: metadata, + elementPathTree: editorState.elementPathTree, + }), } } diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts b/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts index 709fc7d5c7dc..e2ab2905af65 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts @@ -14,6 +14,7 @@ import type { AllElementProps } from '../../editor/store/editor-state' import type { CanvasCommand } from '../commands/commands' import type { ActiveFrameAction } from '../commands/set-active-frames-command' import type { StrategyApplicationStatus } from './interaction-state' +import type { StyleInfo } from '../canvas-types' // TODO: fill this in, maybe make it an ADT for different strategies export interface CustomStrategyState { @@ -105,6 +106,14 @@ export function controlWithProps

(value: ControlWithProps

): ControlWithProp return value } +export type StyleInfoReader = (elementPath: ElementPath) => StyleInfo | null + +export type StyleInfoFactory = (context: { + projectContents: ProjectContentTreeRoot + metadata: ElementInstanceMetadataMap + elementPathTree: ElementPathTrees +}) => StyleInfoReader + export interface InteractionCanvasState { interactionTarget: InteractionTarget projectContents: ProjectContentTreeRoot @@ -117,6 +126,7 @@ export interface InteractionCanvasState { startingElementPathTree: ElementPathTrees startingAllElementProps: AllElementProps propertyControlsInfo: PropertyControlsInfo + styleInfoReader: StyleInfoReader } export type InteractionTarget = TargetPaths | InsertionSubjects diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx index e247092ba7be..169734720b0d 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx @@ -29,7 +29,7 @@ import type { FlexGapData } from '../../gap-utils' import { cursorFromFlexDirection, dragDeltaForOrientation, - maybeFlexGapData, + getFlexGapData, recurseIntoChildrenOfMapOrFragment, } from '../../gap-utils' import type { CanvasStrategyFactory } from '../canvas-strategies' @@ -95,7 +95,11 @@ export const setFlexGapStrategy: CanvasStrategyFactory = ( return null } - const flexGap = maybeFlexGapData(canvasState.startingMetadata, selectedElement) + const flexGap = getFlexGapData( + canvasState.styleInfoReader(selectedElement), + MetadataUtils.findElementByElementPath(canvasState.startingMetadata, selectedElement), + ) + if (flexGap == null) { return null } diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 88e60cfb51f9..8477786c2cab 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -26,8 +26,7 @@ import type { import { InteractionSession } from './canvas-strategies/interaction-state' import type { CanvasStrategyId } from './canvas-strategies/canvas-strategy-types' import type { MouseButtonsPressed } from '../../utils/mouse' -import type { FlexDirection } from '../inspector/common/css-utils' -import type { CSSNumberWithRenderedValue } from './controls/select-mode/controls-common' +import type { CSSNumber, FlexDirection } from '../inspector/common/css-utils' export const CanvasContainerID = 'canvas-container' @@ -538,44 +537,21 @@ export type SelectionLocked = 'locked' | 'locked-hierarchy' | 'selectable' export type PropertyTag = { type: 'hover' } | { type: 'breakpoint'; name: string } -export interface FlexGapInfo { - name: 'gap' - gap: CSSNumberWithRenderedValue -} - -export const flexGapInfo = (gap: CSSNumberWithRenderedValue): FlexGapInfo => ({ - name: 'gap', - gap: gap, -}) - -export interface FlexDirectionInfo { - name: 'flexDirection' - flexDirection: FlexDirection -} - -export const flexDirectionInfo = (flexDirection: FlexDirection): FlexDirectionInfo => ({ - name: 'flexDirection', - flexDirection: flexDirection, -}) - -export type StylePropInfo = FlexGapInfo | FlexDirectionInfo - -export interface StyleProperty { - tags: PropertyTag[] +export interface WithPropertyTag { + tag: PropertyTag | null value: T } -export const stylePropertyWithTags = ( - tags: PropertyTag[], - value: T, -): StyleProperty => ({ - tags: tags, +export const withPropertyTag = (value: T): WithPropertyTag => ({ + tag: null, value: value, }) -export const styleProperty = (value: T): StyleProperty => ({ - tags: [], - value: value, -}) +export type FlexGapInfo = WithPropertyTag + +export type FlexDirectionInfo = WithPropertyTag -export type StyleInfo = Array> +export interface StyleInfo { + gap: FlexGapInfo | null + flexDirection: FlexDirectionInfo | null +} diff --git a/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx b/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx index 0e83aff83ffb..8ec4efa3b60d 100644 --- a/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx @@ -24,8 +24,8 @@ import { createInteractionViaMouse, flexGapHandle } from '../../canvas-strategie import { windowToCanvasCoordinates } from '../../dom-lookup' import { cursorFromFlexDirection, - maybeFlexGapData, gapControlBoundsFromMetadata, + getFlexGapData, recurseIntoChildrenOfMapOrFragment, } from '../../gap-utils' import { CanvasOffsetWrapper } from '../canvas-offset-wrapper' @@ -46,6 +46,7 @@ import { reverseJustifyContent, } from '../../../../core/model/flex-utils' import { optionalMap } from '../../../../core/shared/optional-utils' +import { getActivePlugin } from '../../plugins/style-plugins' interface FlexGapControlProps { selectedElement: ElementPath @@ -130,7 +131,21 @@ export const FlexGapControl = controlForStrategyMemoized((p elementPathTrees, selectedElement, ) - const flexGap = maybeFlexGapData(metadata, selectedElement) + + const flexGap = useEditorState( + Substores.fullStore, + (store) => + getFlexGapData( + getActivePlugin(store.editor).styleInfoFactory({ + projectContents: store.editor.projectContents, + metadata: metadata, + elementPathTree: store.editor.elementPathTree, + })(selectedElement), + MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, selectedElement), + ), + 'FlexGapControl flexGap', + ) + if (flexGap == null) { return null } diff --git a/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx b/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx index ed5037d454df..be1d238f029b 100644 --- a/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx +++ b/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx @@ -3,14 +3,16 @@ import { useColorTheme } from '../../../../uuiui' import { Substores, useEditorState, useRefEditorState } from '../../../editor/store/store-hook' import { useBoundingBox } from '../bounding-box-hooks' import { CanvasOffsetWrapper } from '../canvas-offset-wrapper' -import type { PathWithBounds } from '../../gap-utils' +import type { FlexGapData, PathWithBounds } from '../../gap-utils' import { gapControlBoundsFromMetadata, - maybeFlexGapData, + getFlexGapData, recurseIntoChildrenOfMapOrFragment, } from '../../gap-utils' import type { ElementPath } from 'utopia-shared/src/types' import type { ElementInstanceMetadata } from '../../../../core/shared/element-template' +import { getActivePlugin } from '../../plugins/style-plugins' +import { MetadataUtils } from '../../../../core/model/element-metadata-utils' export interface SubduedFlexGapControlProps { hoveredOrFocused: 'hovered' | 'focused' @@ -34,7 +36,21 @@ export const SubduedFlexGapControl = React.memo((pro elementPathTrees.current, targets[0], ) - const flexGap = maybeFlexGapData(metadata.current, selectedElement) + + const flexGap = useEditorState( + Substores.fullStore, + (store) => + getFlexGapData( + getActivePlugin(store.editor).styleInfoFactory({ + projectContents: store.editor.projectContents, + metadata: store.editor.jsxMetadata, + elementPathTree: store.editor.elementPathTree, + })(selectedElement), + MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, selectedElement), + ), + 'FlexGapControl flexGap', + ) + if (flexGap == null) { return null } @@ -54,7 +70,7 @@ export const SubduedFlexGapControl = React.memo((pro {controlBounds.map((controlBound, i) => ( ((pro }) function FlexGapControl({ - targets, + flexGap, selectedElement, hoveredOrFocused, controlBound, flexChildren, }: { - targets: Array selectedElement: ElementPath hoveredOrFocused: 'hovered' | 'focused' controlBound: PathWithBounds flexChildren: Array + flexGap: FlexGapData }) { const metadata = useRefEditorState((store) => store.editor.jsxMetadata) const sideRef = useBoundingBox([controlBound.path], (ref) => { - const flexGap = maybeFlexGapData(metadata.current, selectedElement) - if (flexGap == null) { - return - } - const flexGapValue = flexGap.value const controlBounds = gapControlBoundsFromMetadata( diff --git a/editor/src/components/canvas/gap-utils.ts b/editor/src/components/canvas/gap-utils.ts index a8cbe955a64c..94f9522e8f62 100644 --- a/editor/src/components/canvas/gap-utils.ts +++ b/editor/src/components/canvas/gap-utils.ts @@ -16,8 +16,12 @@ import type { CanvasRectangle, CanvasVector, Size } from '../../core/shared/math import { canvasRectangle, isInfinityRectangle } from '../../core/shared/math-utils' import type { ElementPath } from '../../core/shared/project-file-types' import { assertNever } from '../../core/shared/utils' +import type { StyleInfo } from './canvas-types' import { CSSCursor } from './canvas-types' -import type { CSSNumberWithRenderedValue } from './controls/select-mode/controls-common' +import { + cssNumberWithRenderedValue, + type CSSNumberWithRenderedValue, +} from './controls/select-mode/controls-common' import type { CSSNumber, FlexDirection } from '../inspector/common/css-utils' import type { Sides } from 'utopia-api/core' import { sides } from 'utopia-api/core' @@ -328,39 +332,23 @@ export interface FlexGapData { direction: FlexDirection } -export function maybeFlexGapData( - metadata: ElementInstanceMetadataMap, - elementPath: ElementPath, +export function getFlexGapData( + info: StyleInfo | null, + instance: ElementInstanceMetadata | null, ): FlexGapData | null { - const element = MetadataUtils.findElementByElementPath(metadata, elementPath) - if ( - element == null || - element.specialSizeMeasurements.display !== 'flex' || - isLeft(element.element) || - !isJSXElement(element.element.value) - ) { + if (instance == null || info == null) { return null } - if (element.specialSizeMeasurements.justifyContent?.startsWith('space')) { + const gap = info.gap?.value + const renderedValuePx = instance.specialSizeMeasurements.gap + if (gap == null || renderedValuePx == null) { return null } - const gap = element.specialSizeMeasurements.gap ?? 0 - - const gapFromProps: CSSNumber | undefined = defaultEither( - undefined, - getLayoutProperty('gap', right(element.element.value.props), styleStringInArray), - ) - - const flexDirection = element.specialSizeMeasurements.flexDirection ?? 'row' - return { - value: { - renderedValuePx: gap, - value: gapFromProps ?? null, - }, - direction: flexDirection, + value: cssNumberWithRenderedValue(gap, renderedValuePx), + direction: info.flexDirection?.value ?? 'row', } } diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 492aa4b1466c..11bc7dfa873b 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -1,51 +1,22 @@ -import type { ElementPath } from 'utopia-shared/src/types' import { getLayoutProperty } from '../../../core/layout/getLayoutProperty' +import type { StyleLayoutProp } from '../../../core/layout/layout-helpers-new' import { MetadataUtils } from '../../../core/model/element-metadata-utils' -import { defaultEither, isLeft, right } from '../../../core/shared/either' -import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template' +import { defaultEither, isLeft, mapEither, right } from '../../../core/shared/either' +import type { JSXElement } from '../../../core/shared/element-template' import { isJSXElement } from '../../../core/shared/element-template' import { styleStringInArray } from '../../../utils/common-constants' -import type { CSSNumber } from '../../inspector/common/css-utils' -import type { FlexGapData } from '../gap-utils' +import type { ParsedCSSProperties } from '../../inspector/common/css-utils' +import { withPropertyTag, type WithPropertyTag } from '../canvas-types' import type { StylePlugin } from './style-plugins' -import { stripNulls } from '../../../core/shared/array-utils' -import { optionalMap } from '../../../core/shared/optional-utils' -import { flexDirectionInfo, flexGapInfo, styleProperty } from '../canvas-types' -function maybeFlexGapData( - metadata: ElementInstanceMetadataMap, - elementPath: ElementPath, -): FlexGapData | null { - const element = MetadataUtils.findElementByElementPath(metadata, elementPath) - if ( - element == null || - element.specialSizeMeasurements.display !== 'flex' || - isLeft(element.element) || - !isJSXElement(element.element.value) - ) { - return null - } - - if (element.specialSizeMeasurements.justifyContent?.startsWith('space')) { - return null - } - - const gap = element.specialSizeMeasurements.gap ?? 0 - - const gapFromProps: CSSNumber | undefined = defaultEither( - undefined, - getLayoutProperty('gap', right(element.element.value.props), styleStringInArray), - ) - - const flexDirection = element.specialSizeMeasurements.flexDirection ?? 'row' - - return { - value: { - renderedValuePx: gap, - value: gapFromProps ?? null, - }, - direction: flexDirection, - } +function getPropertyFromInstance

( + prop: P, + element: JSXElement, +): WithPropertyTag | null { + return defaultEither( + null, + mapEither(withPropertyTag, getLayoutProperty(prop, right(element.props), styleStringInArray)), + ) as WithPropertyTag | null } export const InlineStylePlugin: StylePlugin = { @@ -53,14 +24,18 @@ export const InlineStylePlugin: StylePlugin = { styleInfoFactory: ({ metadata }) => (elementPath) => { - const flexGapData = maybeFlexGapData(metadata, elementPath) - return stripNulls([ - optionalMap((gap) => styleProperty(flexGapInfo(gap)), flexGapData?.value), - optionalMap( - (direction) => styleProperty(flexDirectionInfo(direction)), - flexGapData?.direction, - ), - ]) + const instance = MetadataUtils.findElementByElementPath(metadata, elementPath) + if (instance == null || isLeft(instance.element) || !isJSXElement(instance.element.value)) { + return null + } + + const gap = getPropertyFromInstance('gap', instance.element.value) + const flexDirection = getPropertyFromInstance('flexDirection', instance.element.value) + + return { + gap: gap, + flexDirection: flexDirection, + } }, normalizeFromInlineStyle: (editor) => editor, } diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index bc5fd92fc0bf..edbf8ce5720c 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -3,7 +3,10 @@ import type { EditorState } from '../../editor/store/editor-state' import type { StyleInfoFactory } from '../canvas-strategies/canvas-strategy-types' import { InlineStylePlugin } from './inline-style-plugin' import { TailwindPlugin } from './tailwind-style-plugin' -import { isTailwindEnabled } from '../../../core/tailwind/tailwind-compilation' +import { + getTailwindConfigCached, + isTailwindEnabled, +} from '../../../core/tailwind/tailwind-compilation' export interface StylePlugin { name: string @@ -19,14 +22,9 @@ export const Plugins = { Tailwind: TailwindPlugin, } as const -export function getActivePlugin( - // The `_editorState` is not used now because `isTailwindEnabled` reads a - // global behind the scenes, it's still passed to make it easy to add new - // cases here without a big refactor - _editorState: EditorState, -): StylePlugin { +export function getActivePlugin(editorState: EditorState): StylePlugin { if (isTailwindEnabled()) { - return TailwindPlugin + return TailwindPlugin(getTailwindConfigCached(editorState)) } return InlineStylePlugin } diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 61e0e47715ff..23e2e7413a9c 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -1,16 +1,12 @@ import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' -import type { ElementPath, JSXAttributesEntry } from 'utopia-shared/src/types' -import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import type { JSXAttributesEntry } from 'utopia-shared/src/types' import { isLeft } from '../../../core/shared/either' -import type { - ElementInstanceMetadata, - ElementInstanceMetadataMap, -} from '../../../core/shared/element-template' import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' import { getElementFromProjectContents, getJSXElementFromProjectContents, } from '../../editor/store/editor-state' +import type { Parser } from '../../inspector/common/css-utils' import { cssParsers } from '../../inspector/common/css-utils' import { foldAndApplyCommandsSimple } from './../commands/commands' import { mapDropNulls } from '../../../core/shared/array-utils' @@ -19,64 +15,16 @@ import * as PP from '../../../core/shared/property-path' import { updateClassListCommand } from './../commands/update-class-list-command' import * as UCL from './../commands/update-class-list-command' import type { StylePlugin } from './style-plugins' -import type { FlexDirectionInfo, FlexGapInfo, StyleProperty, StylePropInfo } from '../canvas-types' -import { flexDirectionInfo, flexGapInfo, styleProperty } from '../canvas-types' -import { cssNumberWithRenderedValue } from '../controls/select-mode/controls-common' - -function parseTailwindGap( - gapValue: string | null, - instanceMetadata: ElementInstanceMetadata | null, -): StyleProperty | null { - if ( - instanceMetadata == null || - instanceMetadata.specialSizeMeasurements.gap == null || - gapValue == null - ) { - return null - } - - const parsedGapValue = cssParsers.gap(gapValue, null) - if (isLeft(parsedGapValue)) { - return null - } - - return styleProperty( - flexGapInfo( - cssNumberWithRenderedValue( - parsedGapValue.value, - instanceMetadata.specialSizeMeasurements.gap, - ), - ), - ) -} +import type { WithPropertyTag } from '../canvas-types' +import { withPropertyTag } from '../canvas-types' +import type { Config } from 'tailwindcss/types/config' -function parseTailwindFlexDirection( - directionValue: string | null, -): StyleProperty | null { - if (directionValue == null) { - return null - } - const parsed = cssParsers.flexDirection(directionValue, null) +function parseTailwindProperty(value: unknown, parse: Parser): WithPropertyTag | null { + const parsed = parse(value, null) if (isLeft(parsed)) { return null } - - return styleProperty(flexDirectionInfo(parsed.value)) -} - -function parseTailwindClass( - metadata: ElementInstanceMetadataMap, - elementPath: ElementPath, - { property, value }: { property: string; value: string }, -): StyleProperty | null { - switch (property) { - case 'gap': - return parseTailwindGap(value, MetadataUtils.findElementByElementPath(metadata, elementPath)) - case 'flexDirection': - return parseTailwindFlexDirection(value) - default: - return null - } + return withPropertyTag(parsed.value) } const TailwindPropertyMapping = { @@ -99,37 +47,40 @@ function stringifiedStylePropValue(value: unknown): string | null { return null } -// TODO: pass down the tailwind config -export const TailwindPlugin: StylePlugin = { +function getTailwindClassMapping(classes: string[], config: Config | null) { + const mapping: Record = {} + classes.forEach((className) => { + const parsed = TailwindClassParser.parse(className, config ?? undefined) + if (parsed.kind === 'error' || !isSupportedTailwindProperty(parsed.property)) { + return + } + mapping[parsed.property] = parsed.value + }) + return mapping +} + +export const TailwindPlugin = (config: Config | null): StylePlugin => ({ name: 'Tailwind', styleInfoFactory: - ({ metadata, projectContents }) => + ({ projectContents }) => (elementPath) => { const classList = getClassNameAttribute( getElementFromProjectContents(elementPath, projectContents), )?.value if (classList == null || typeof classList !== 'string') { - return [] + return null } - const classes = classList.split(' ') - - const styleProps = mapDropNulls((tailwindClass) => { - const parsed = TailwindClassParser.parse( - tailwindClass /* TODO pass the tailwind config here */, - ) - if (parsed.kind === 'error' || !isSupportedTailwindProperty(parsed.property)) { - return null - } - - return parseTailwindClass(metadata, elementPath, { - property: parsed.property, - value: parsed.value, - }) - }, classes) + const mapping = getTailwindClassMapping(classList.split(' '), config) - return styleProps + return { + gap: parseTailwindProperty(mapping[TailwindPropertyMapping.gap], cssParsers.gap), + flexDirection: parseTailwindProperty( + mapping[TailwindPropertyMapping.flexDirection], + cssParsers.flexDirection, + ), + } }, normalizeFromInlineStyle: (editorState, elementsToNormalize) => { const commands = elementsToNormalize.flatMap((elementPath) => { @@ -167,14 +118,7 @@ export const TailwindPlugin: StylePlugin = { return null } - const tailwindClass = TailwindClassParser.classname( - { property: TailwindPropertyMapping[key], value: valueString }, - /* TODO pass the tailwind config here */ - ) - if (tailwindClass == null) { - return null - } - return { tailwindClass, key } + return { property: TailwindPropertyMapping[key], value: valueString } }, styleProps) return [ @@ -183,8 +127,10 @@ export const TailwindPlugin: StylePlugin = { elementPath, Object.keys(TailwindPropertyMapping).map((prop) => PP.create('style', prop)), ), - ...stylePropConversions.map(({ tailwindClass }) => - updateClassListCommand('always', elementPath, UCL.add(tailwindClass)), + updateClassListCommand( + 'always', + elementPath, + stylePropConversions.map(({ property, value }) => UCL.add({ property, value })), ), ] }) @@ -194,4 +140,4 @@ export const TailwindPlugin: StylePlugin = { return foldAndApplyCommandsSimple(editorState, commands) }, -} +}) diff --git a/editor/src/components/editor/store/dispatch-strategies.tsx b/editor/src/components/editor/store/dispatch-strategies.tsx index 8d2c0f818fdb..0180914754a7 100644 --- a/editor/src/components/editor/store/dispatch-strategies.tsx +++ b/editor/src/components/editor/store/dispatch-strategies.tsx @@ -57,6 +57,7 @@ import { isInsertMode } from '../editor-modes' import { patchedCreateRemixDerivedDataMemo } from './remix-derived-data' import { allowedToEditProject } from './collaborative-editing' import { canMeasurePerformance } from '../../../core/performance/performance-utils' +import { getActivePlugin } from '../../canvas/plugins/style-plugins' interface HandleStrategiesResult { unpatchedEditorState: EditorState @@ -103,7 +104,8 @@ export function interactionFinished( 'end-interaction', ) : strategyApplicationResult([], []) - const commandResult = foldAndApplyCommands( + + const { editorState } = foldAndApplyCommands( newEditorState, storedState.patchedEditor, [], @@ -111,9 +113,14 @@ export function interactionFinished( 'end-interaction', ) + const normalizedEditor = getActivePlugin(editorState).normalizeFromInlineStyle( + editorState, + strategyResult.elementsToRerender, + ) + const finalEditor: EditorState = applyElementsToRerenderFromStrategyResult( { - ...commandResult.editorState, + ...normalizedEditor, // TODO instead of clearing the metadata, we should save the latest valid metadata here to save a dom-walker run jsxMetadata: {}, domMetadata: {}, diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index b2c28db037f9..2f5ebadc2f04 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -5445,7 +5445,10 @@ export const computedStyleKeys: Array = Object.keys({ ...layoutEmptyValuesNew, }) -type Parser = (simpleValue: unknown, rawValue: ModifiableAttribute | null) => Either +export type Parser = ( + simpleValue: unknown, + rawValue: ModifiableAttribute | null, +) => Either type ParseFunction = ( prop: K, From b2fc141d411d684c618792c6b4963e11c7a00f54 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 10 Oct 2024 10:35:05 +0200 Subject: [PATCH 04/27] properties to unset --- .../commands/delete-properties-command.ts | 79 +++++++++++++++++-- .../canvas/plugins/tailwind-style-plugin.ts | 52 +++++++++--- .../src/components/editor/actions/actions.tsx | 1 + .../components/editor/store/editor-state.ts | 9 +++ .../store/store-deep-equality-instances.ts | 17 +++- .../store/store-hook-substore-helpers.ts | 1 + editor/src/core/shared/object-utils.ts | 4 + editor/src/templates/editor-canvas.tsx | 1 + editor/src/templates/editor.tsx | 1 + 9 files changed, 147 insertions(+), 18 deletions(-) diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index f1d8501385b3..8ad9a3bb145f 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -1,13 +1,24 @@ -import type { JSXElement } from '../../../core/shared/element-template' -import type { EditorState, EditorStatePatch } from '../../../components/editor/store/editor-state' +import { + emptyComments, + jsExpressionValue, + type JSXElement, +} from '../../../core/shared/element-template' +import type { + EditorState, + EditorStatePatch, + PropertiesToUnset, +} from '../../../components/editor/store/editor-state' import { modifyUnderlyingElementForOpenFile } from '../../../components/editor/store/editor-state' import { foldEither } from '../../../core/shared/either' import { unsetJSXValuesAtPaths } from '../../../core/shared/jsx-attributes' import type { ElementPath, PropertyPath } from '../../../core/shared/project-file-types' -import type { BaseCommand, CommandFunction, WhenToRun } from './commands' +import type { BaseCommand, CommandFunctionResult, WhenToRun } from './commands' import * as EP from '../../../core/shared/element-path' import * as PP from '../../../core/shared/property-path' import { patchParseSuccessAtElementPath } from './patch-utils' +import type { InteractionLifecycle } from '../canvas-strategies/canvas-strategy-types' +import { mapDropNulls } from '../../../core/shared/array-utils' +import { applyValuesAtPath } from './adjust-number-command' export interface DeleteProperties extends BaseCommand { type: 'DELETE_PROPERTIES' @@ -28,19 +39,73 @@ export function deleteProperties( } } -export const runDeleteProperties: CommandFunction = ( +type PropertiesToUnsetArray = Array<{ + prop: keyof PropertiesToUnset + value: PropertiesToUnset[keyof PropertiesToUnset] + path: PropertyPath +}> + +function getUnsetProperties(properties: Array): PropertiesToUnsetArray { + return mapDropNulls((property) => { + if (property.propertyElements.at(0) !== 'style') { + return null + } + + switch (property.propertyElements.at(1)) { + case 'gap': + return { prop: 'gap', value: '0px', path: property } + default: + return null + } + }, properties) +} + +function getPropertiesToUnset(propertiesToUnset: PropertiesToUnsetArray): PropertiesToUnset { + let result: Partial = {} + for (const { prop, value } of propertiesToUnset) { + result[prop] = value + } + return result +} + +function getPropertiesToUnsetPatches( + editorState: EditorState, + command: DeleteProperties, +): EditorStatePatch[] { + if (command.whenToRun === 'on-complete') { + return [] + } + + const unsetProperties = getUnsetProperties(command.properties) + const partialPropertiesToUnset = getPropertiesToUnset(unsetProperties) + const unsetPropertiesPatch: EditorStatePatch = { + canvas: { propertiesToUnset: { $set: partialPropertiesToUnset } }, + } + const { editorStatePatch: setPropertiesToUnsetValuePatch } = applyValuesAtPath( + editorState, + command.element, + unsetProperties.map(({ path, value }) => ({ + path: path, + value: jsExpressionValue(value, emptyComments), + })), + ) + return [unsetPropertiesPatch, setPropertiesToUnsetValuePatch] +} + +export const runDeleteProperties = ( editorState: EditorState, command: DeleteProperties, -) => { - // Apply the update to the properties. +): CommandFunctionResult => { const { editorStatePatch: propertyUpdatePatch } = deleteValuesAtPath( editorState, command.element, command.properties, ) + const propertiesToUnsetPatch = getPropertiesToUnsetPatches(editorState, command) + return { - editorStatePatches: [propertyUpdatePatch], + editorStatePatches: [propertyUpdatePatch, ...propertiesToUnsetPatch], commandDescription: `Delete Properties ${command.properties .map(PP.toString) .join(',')} on ${EP.toUid(command.element)}`, diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 23e2e7413a9c..994c4fdfca8c 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -1,7 +1,8 @@ import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' -import type { JSXAttributesEntry } from 'utopia-shared/src/types' +import type { JSExpression, JSXAttributesEntry } from 'utopia-shared/src/types' import { isLeft } from '../../../core/shared/either' import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' +import type { PropertiesToUnset } from '../../editor/store/editor-state' import { getElementFromProjectContents, getJSXElementFromProjectContents, @@ -18,6 +19,7 @@ import type { StylePlugin } from './style-plugins' import type { WithPropertyTag } from '../canvas-types' import { withPropertyTag } from '../canvas-types' import type { Config } from 'tailwindcss/types/config' +import { typedObjectKeys } from '../../../core/shared/object-utils' function parseTailwindProperty(value: unknown, parse: Parser): WithPropertyTag | null { const parsed = parse(value, null) @@ -59,6 +61,37 @@ function getTailwindClassMapping(classes: string[], config: Config | null) { return mapping } +function getStylPropContents( + styleProp: JSExpression, +): Array<{ key: string; value: unknown }> | null { + if (styleProp.type === 'ATTRIBUTE_NESTED_OBJECT') { + return mapDropNulls( + (c): { key: string; value: unknown } | null => + c.type === 'PROPERTY_ASSIGNMENT' && + c.value.type === 'ATTRIBUTE_VALUE' && + typeof c.key === 'string' + ? { key: c.key, value: c.value.value } + : null, + styleProp.content, + ) + } + + if (styleProp.type === 'ATTRIBUTE_VALUE' && typeof styleProp.value === 'object') { + return mapDropNulls( + ([key, value]) => (typeof key !== 'object' ? null : { key, value }), + Object.entries(styleProp.value), + ) + } + + return null +} + +function getRemoveUpdates(propertiesToUnset: PropertiesToUnset): UCL.ClassListUpdate[] { + return typedObjectKeys(propertiesToUnset).map((property) => + UCL.remove(TailwindPropertyMapping[property]), + ) +} + export const TailwindPlugin = (config: Config | null): StylePlugin => ({ name: 'Tailwind', styleInfoFactory: @@ -97,19 +130,15 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ return [] } - const styleValue = styleAttribute.value - if (styleValue.type !== 'ATTRIBUTE_NESTED_OBJECT') { + const styleValue = getStylPropContents(styleAttribute.value) + if (styleValue == null) { return [] } const styleProps = mapDropNulls( (c): { key: keyof typeof TailwindPropertyMapping; value: unknown } | null => - c.type === 'PROPERTY_ASSIGNMENT' && - c.value.type === 'ATTRIBUTE_VALUE' && - isSupportedTailwindProperty(c.key) - ? { key: c.key, value: c.value.value } - : null, - styleValue.content, + isSupportedTailwindProperty(c.key) ? { key: c.key, value: c.value } : null, + styleValue, ) const stylePropConversions = mapDropNulls(({ key, value }) => { @@ -121,9 +150,11 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ return { property: TailwindPropertyMapping[key], value: valueString } }, styleProps) + const remo = getRemoveUpdates(editorState.canvas.propertiesToUnset) + return [ deleteProperties( - 'always', + 'on-complete', elementPath, Object.keys(TailwindPropertyMapping).map((prop) => PP.create('style', prop)), ), @@ -132,6 +163,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ elementPath, stylePropConversions.map(({ property, value }) => UCL.add({ property, value })), ), + updateClassListCommand('always', elementPath, remo), ] }) if (commands.length === 0) { diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index b70275861002..0b7b707739ad 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -970,6 +970,7 @@ export function restoreEditorState( resizeOptions: currentEditor.canvas.resizeOptions, domWalkerAdditionalElementsToUpdate: currentEditor.canvas.domWalkerAdditionalElementsToUpdate, controls: currentEditor.canvas.controls, + propertiesToUnset: currentEditor.canvas.propertiesToUnset, }, inspector: { visible: currentEditor.inspector.visible, diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index 800ae1ae55a3..24d015177c59 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -869,6 +869,10 @@ export function internalClipboard( } } +export interface PropertiesToUnset { + gap?: '0px' +} + export interface EditorStateCanvas { elementsToRerender: ElementsToRerender interactionSession: InteractionSession | null @@ -890,6 +894,7 @@ export interface EditorStateCanvas { resizeOptions: ResizeOptions domWalkerAdditionalElementsToUpdate: Array controls: EditorStateCanvasControls + propertiesToUnset: PropertiesToUnset } export function editorStateCanvas( @@ -913,6 +918,7 @@ export function editorStateCanvas( resizeOpts: ResizeOptions, domWalkerAdditionalElementsToUpdate: Array, controls: EditorStateCanvasControls, + propertiesToUnset: Partial, ): EditorStateCanvas { return { elementsToRerender: elementsToRerender, @@ -935,6 +941,7 @@ export function editorStateCanvas( resizeOptions: resizeOpts, domWalkerAdditionalElementsToUpdate: domWalkerAdditionalElementsToUpdate, controls: controls, + propertiesToUnset: propertiesToUnset, } } @@ -2629,6 +2636,7 @@ export function createEditorState(dispatch: EditorDispatch): EditorState { parentOutlineHighlight: null, gridControlData: null, }, + propertiesToUnset: {}, }, inspector: { visible: true, @@ -3004,6 +3012,7 @@ export function editorModelFromPersistentModel( parentOutlineHighlight: null, gridControlData: null, }, + propertiesToUnset: {}, }, inspector: { visible: true, diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index f22431510ddb..02e77362a682 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -362,6 +362,7 @@ import type { EditorRemixConfig, ErrorBoundaryHandling, GridControlData, + PropertiesToUnset, } from './editor-state' import { trueUpGroupElementChanged, @@ -2767,6 +2768,13 @@ export const EditorStateCanvasControlsKeepDeepEquality: KeepDeepEqualityCall = + combine1EqualityCall( + (p) => p.gap, + undefinableDeepEquality(createCallWithTripleEquals()), + (gap) => ({ gap }), + ) + export const ModifiersKeepDeepEquality: KeepDeepEqualityCall = combine4EqualityCalls( (modifiers) => modifiers.alt, createCallWithTripleEquals(), @@ -3262,6 +3270,11 @@ export const EditorStateCanvasKeepDeepEquality: KeepDeepEqualityCall>( ): boolean { return keys.every((key) => objectContainsKey(obj, key)) } + +export function typedObjectKeys(obj: T): Array { + return Object.keys(obj) as Array +} diff --git a/editor/src/templates/editor-canvas.tsx b/editor/src/templates/editor-canvas.tsx index 6ad444843b3c..9a0ca3a7549a 100644 --- a/editor/src/templates/editor-canvas.tsx +++ b/editor/src/templates/editor-canvas.tsx @@ -468,6 +468,7 @@ export function runLocalCanvasAction( null, null, ), + propertiesToUnset: {}, }, } case 'UPDATE_INTERACTION_SESSION': diff --git a/editor/src/templates/editor.tsx b/editor/src/templates/editor.tsx index c31c2a933d5d..eb66d9076e3d 100644 --- a/editor/src/templates/editor.tsx +++ b/editor/src/templates/editor.tsx @@ -903,6 +903,7 @@ export function shouldRunDOMWalker( 'selectionControlsVisible', 'snappingThreshold', 'textEditor', + 'propertiesToUnset', ], })(patchedEditorBefore.canvas, patchedEditorAfter.canvas) From 67be0445116107600e0b191fa78b7c798e650773 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 10 Oct 2024 10:53:35 +0200 Subject: [PATCH 05/27] reset propertiesToUnset in resetUnpatchedEditorTransientFields --- editor/src/components/editor/store/dispatch.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/components/editor/store/dispatch.tsx b/editor/src/components/editor/store/dispatch.tsx index e8a522d2d7f1..81c12ab42593 100644 --- a/editor/src/components/editor/store/dispatch.tsx +++ b/editor/src/components/editor/store/dispatch.tsx @@ -1069,6 +1069,7 @@ function resetUnpatchedEditorTransientFields(editor: EditorState): EditorState { canvas: { ...editor.canvas, elementsToRerender: 'rerender-all-elements', + propertiesToUnset: {}, }, } } From e2bde110c58196e17353b402c8974b89753266c6 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 10 Oct 2024 16:12:03 +0200 Subject: [PATCH 06/27] cleanup --- .../canvas/commands/delete-properties-command.ts | 5 ++--- .../canvas/plugins/tailwind-style-plugin.ts | 16 +++++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index 8ad9a3bb145f..66959673561d 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -16,7 +16,6 @@ import type { BaseCommand, CommandFunctionResult, WhenToRun } from './commands' import * as EP from '../../../core/shared/element-path' import * as PP from '../../../core/shared/property-path' import { patchParseSuccessAtElementPath } from './patch-utils' -import type { InteractionLifecycle } from '../canvas-strategies/canvas-strategy-types' import { mapDropNulls } from '../../../core/shared/array-utils' import { applyValuesAtPath } from './adjust-number-command' @@ -102,10 +101,10 @@ export const runDeleteProperties = ( command.properties, ) - const propertiesToUnsetPatch = getPropertiesToUnsetPatches(editorState, command) + const propertiesToUnsetPatches = getPropertiesToUnsetPatches(editorState, command) return { - editorStatePatches: [propertyUpdatePatch, ...propertiesToUnsetPatch], + editorStatePatches: [propertyUpdatePatch, ...propertiesToUnsetPatches], commandDescription: `Delete Properties ${command.properties .map(PP.toString) .join(',')} on ${EP.toUid(command.element)}`, diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 46a100e4ca4f..13473902f205 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -152,14 +152,20 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ return [ deleteProperties( - 'always', + 'on-complete', elementPath, Object.keys(TailwindPropertyMapping).map((prop) => PP.create('style', prop)), ), - updateClassListCommand('always', elementPath, [ - ...stylePropConversions.map(({ property, value }) => UCL.add({ property, value })), - ...getRemoveUpdates(editorState.canvas.propertiesToUnset), - ]), + updateClassListCommand( + 'always', + elementPath, + stylePropConversions.map(({ property, value }) => UCL.add({ property, value })), + ), + updateClassListCommand( + 'always', + elementPath, + getRemoveUpdates(editorState.canvas.propertiesToUnset), + ), ] }) if (commands.length === 0) { From 19c59fc87e38c50710535bc541572fdffe5c6baf Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 10 Oct 2024 16:31:12 +0200 Subject: [PATCH 07/27] remove props in inline style plugin --- editor/src/components/canvas/gap-utils.ts | 24 +++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/editor/src/components/canvas/gap-utils.ts b/editor/src/components/canvas/gap-utils.ts index 7e32f001a39e..da1588fc72b2 100644 --- a/editor/src/components/canvas/gap-utils.ts +++ b/editor/src/components/canvas/gap-utils.ts @@ -336,19 +336,31 @@ export function maybeFlexGapData( info: StyleInfo | null, element: ElementInstanceMetadata | null, ): FlexGapData | null { - if (element == null || info == null) { + if ( + element == null || + element.specialSizeMeasurements.display !== 'flex' || + isLeft(element.element) || + !isJSXElement(element.element.value) || + info == null + ) { return null } - const gap = info.gap?.value - const renderedValuePx = element.specialSizeMeasurements.gap - if (gap == null || renderedValuePx == null) { + if (element.specialSizeMeasurements.justifyContent?.startsWith('space')) { return null } + const gap = element.specialSizeMeasurements.gap ?? 0 + const gapFromReader = info.gap?.value + + const flexDirection = element.specialSizeMeasurements.flexDirection ?? 'row' + return { - value: cssNumberWithRenderedValue(gap, renderedValuePx), - direction: info.flexDirection?.value ?? 'row', + value: { + renderedValuePx: gap, + value: gapFromReader ?? null, + }, + direction: flexDirection, } } From 44df8a72173dd320fc464d259965f77c2959568a Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 10 Oct 2024 16:31:20 +0200 Subject: [PATCH 08/27] tests --- .../set-flex-gap-strategy.spec.browser2.tsx | 8 ++++++++ .../canvas/plugins/inline-style-plugin.ts | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx index 72a3e7b527cf..bd91e06ef3d4 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx @@ -772,6 +772,14 @@ export var storyboard = ( const div = editor.renderedDOM.getByTestId(DivTestId) expect(div.className).toEqual('top-10 left-10 absolute flex flex-row gap-16') }) + + it('can remove tailwind gap by dragging past threshold', async () => { + const editor = await renderTestEditorWithModel(Project, 'await-first-dom-report') + await selectComponentsForTest(editor, [EP.fromString('sb/scene/div')]) + await doGapResize(editor, canvasPoint({ x: -100, y: 0 })) + const div = editor.renderedDOM.getByTestId(DivTestId) + expect(div.className).toEqual('top-10 left-10 absolute flex flex-row') + }) }) }) diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 11bc7dfa873b..77b3f320e91b 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -4,9 +4,13 @@ import { MetadataUtils } from '../../../core/model/element-metadata-utils' import { defaultEither, isLeft, mapEither, right } from '../../../core/shared/either' import type { JSXElement } from '../../../core/shared/element-template' import { isJSXElement } from '../../../core/shared/element-template' +import { typedObjectKeys } from '../../../core/shared/object-utils' +import * as PP from '../../../core/shared/property-path' import { styleStringInArray } from '../../../utils/common-constants' import type { ParsedCSSProperties } from '../../inspector/common/css-utils' import { withPropertyTag, type WithPropertyTag } from '../canvas-types' +import { foldAndApplyCommandsSimple } from '../commands/commands' +import { deleteProperties } from '../commands/delete-properties-command' import type { StylePlugin } from './style-plugins' function getPropertyFromInstance

( @@ -37,5 +41,16 @@ export const InlineStylePlugin: StylePlugin = { flexDirection: flexDirection, } }, - normalizeFromInlineStyle: (editor) => editor, + normalizeFromInlineStyle: (editor, elementsToNormalize) => { + return foldAndApplyCommandsSimple( + editor, + elementsToNormalize.map((element) => + deleteProperties( + 'on-complete', + element, + typedObjectKeys(editor.canvas.propertiesToUnset).map((p) => PP.create('style', p)), + ), + ), + ) + }, } From 28739818409a2ffa8d70cc7e42848d5e30ef0d4a Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 10 Oct 2024 17:02:26 +0200 Subject: [PATCH 09/27] that was subtle --- .../canvas/commands/delete-properties-command.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index 66959673561d..416350232076 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -95,13 +95,10 @@ export const runDeleteProperties = ( editorState: EditorState, command: DeleteProperties, ): CommandFunctionResult => { - const { editorStatePatch: propertyUpdatePatch } = deleteValuesAtPath( - editorState, - command.element, - command.properties, - ) + const { editorStatePatch: propertyUpdatePatch, editorStateWithChanges: editorStateWithChanges } = + deleteValuesAtPath(editorState, command.element, command.properties) - const propertiesToUnsetPatches = getPropertiesToUnsetPatches(editorState, command) + const propertiesToUnsetPatches = getPropertiesToUnsetPatches(editorStateWithChanges, command) return { editorStatePatches: [propertyUpdatePatch, ...propertiesToUnsetPatches], From c17162f1fa34af33958711691b0fe8e1697e42db Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 10 Oct 2024 17:33:33 +0200 Subject: [PATCH 10/27] wip --- .../strategies/grid-reparent-strategies.spec.browser2.tsx | 1 + .../strategies/set-flex-gap-strategy.spec.browser2.tsx | 1 + .../strategies/set-flex-gap-strategy.tsx | 2 +- .../canvas/commands/delete-properties-command.ts | 8 +++++++- editor/src/components/canvas/gap-utils.ts | 5 +---- .../src/components/canvas/plugins/inline-style-plugin.ts | 1 + 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx index ad60b02414c2..c87562ae57a2 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx @@ -23,6 +23,7 @@ import { renderTestEditorWithCode, } from '../../ui-jsx.test-utils' +// describe.only('grid reparent strategies', () => { describe('grid reparent strategies', () => { describe('reparent into a grid', () => { it('from the storyboard', async () => { diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx index bd91e06ef3d4..cdda1ab2d898 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx @@ -32,6 +32,7 @@ import { TailwindConfigPath } from '../../../../core/tailwind/tailwind-config' const DivTestId = 'mydiv' +// describe.only('Flex gap strategy', () => { describe('Flex gap strategy', () => { it('gap controls are not present when element has no children', async () => { const editor = await renderTestEditorWithCode( diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx index b7740ceeb0e8..870fc0b37d95 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx @@ -177,7 +177,7 @@ export const setFlexGapStrategy: CanvasStrategyFactory = ( if (shouldTearOffGap) { return strategyApplicationResult( - [deleteProperties('always', selectedElement, [StyleGapProp])], + [deleteProperties('always', selectedElement, [StyleGapProp], 'note-unset-properties')], selectedElements, ) } diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index 416350232076..d674d27e2c87 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -23,18 +23,21 @@ export interface DeleteProperties extends BaseCommand { type: 'DELETE_PROPERTIES' element: ElementPath properties: Array + propertiesToUnsetBehaviour: 'note-unset-properties' | 'remove-properties' } export function deleteProperties( whenToRun: WhenToRun, element: ElementPath, properties: Array, + propertiesToUnsetBehaviour: 'note-unset-properties' | 'remove-properties' = 'remove-properties', ): DeleteProperties { return { type: 'DELETE_PROPERTIES', whenToRun: whenToRun, element: element, properties: properties, + propertiesToUnsetBehaviour: propertiesToUnsetBehaviour, } } @@ -71,7 +74,10 @@ function getPropertiesToUnsetPatches( editorState: EditorState, command: DeleteProperties, ): EditorStatePatch[] { - if (command.whenToRun === 'on-complete') { + if ( + command.whenToRun === 'on-complete' || + command.propertiesToUnsetBehaviour === 'remove-properties' + ) { return [] } diff --git a/editor/src/components/canvas/gap-utils.ts b/editor/src/components/canvas/gap-utils.ts index da1588fc72b2..ff735048e77c 100644 --- a/editor/src/components/canvas/gap-utils.ts +++ b/editor/src/components/canvas/gap-utils.ts @@ -18,10 +18,7 @@ import type { ElementPath } from '../../core/shared/project-file-types' import { assertNever } from '../../core/shared/utils' import type { StyleInfo } from './canvas-types' import { CSSCursor } from './canvas-types' -import { - cssNumberWithRenderedValue, - type CSSNumberWithRenderedValue, -} from './controls/select-mode/controls-common' +import type { CSSNumberWithRenderedValue } from './controls/select-mode/controls-common' import type { CSSNumber, FlexDirection } from '../inspector/common/css-utils' import type { Sides } from 'utopia-api/core' import { sides } from 'utopia-api/core' diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 77b3f320e91b..3031080bcb8a 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -42,6 +42,7 @@ export const InlineStylePlugin: StylePlugin = { } }, normalizeFromInlineStyle: (editor, elementsToNormalize) => { + return editor return foldAndApplyCommandsSimple( editor, elementsToNormalize.map((element) => From 7a0963382d32a2855b3db7e7e538a85ca968e4f4 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 11 Oct 2024 16:35:05 +0200 Subject: [PATCH 11/27] a band-aid solution --- .../src/components/canvas/plugins/inline-style-plugin.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 3031080bcb8a..234fc22dd850 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -42,14 +42,11 @@ export const InlineStylePlugin: StylePlugin = { } }, normalizeFromInlineStyle: (editor, elementsToNormalize) => { - return editor return foldAndApplyCommandsSimple( editor, - elementsToNormalize.map((element) => - deleteProperties( - 'on-complete', - element, - typedObjectKeys(editor.canvas.propertiesToUnset).map((p) => PP.create('style', p)), + typedObjectKeys(editor.canvas.propertiesToUnset).flatMap((p) => + elementsToNormalize.map((element) => + deleteProperties('on-complete', element, [PP.create('style', p)]), ), ), ) From d629bb7108c23fa9120c00b77269c507e31c7e47 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 12:12:15 +0200 Subject: [PATCH 12/27] undo bandaid fix --- .../src/components/canvas/plugins/inline-style-plugin.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 234fc22dd850..9a4e7f20c3d4 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -44,9 +44,11 @@ export const InlineStylePlugin: StylePlugin = { normalizeFromInlineStyle: (editor, elementsToNormalize) => { return foldAndApplyCommandsSimple( editor, - typedObjectKeys(editor.canvas.propertiesToUnset).flatMap((p) => - elementsToNormalize.map((element) => - deleteProperties('on-complete', element, [PP.create('style', p)]), + elementsToNormalize.flatMap((element) => + deleteProperties( + 'on-complete', + element, + typedObjectKeys(editor.canvas.propertiesToUnset).map((p) => PP.create('style', p)), ), ), ) From 88efe01cb76d70170652bb50ddb79564f6f466b1 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 12:12:48 +0200 Subject: [PATCH 13/27] deleteValuesAtPath is no-op if no values are to be deleted --- .../commands/adjust-css-length-command.ts | 12 +++++----- .../commands/delete-properties-command.ts | 23 +++++++++++++++---- .../canvas/commands/set-property-command.ts | 8 +++++++ .../src/components/editor/actions/actions.tsx | 7 ++++-- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/editor/src/components/canvas/commands/adjust-css-length-command.ts b/editor/src/components/canvas/commands/adjust-css-length-command.ts index 90daf3c850e6..3a3056223e1d 100644 --- a/editor/src/components/canvas/commands/adjust-css-length-command.ts +++ b/editor/src/components/canvas/commands/adjust-css-length-command.ts @@ -442,12 +442,12 @@ export function deleteConflictingPropsForWidthHeight( if (propertiesToDelete.length === 0) { return editorState } else { - const { editorStateWithChanges: editorStateWithPropsDeleted } = deleteValuesAtPath( - editorState, - target, - propertiesToDelete, - ) + const result = deleteValuesAtPath(editorState, target, propertiesToDelete) + + if (result == null) { + return editorState + } - return editorStateWithPropsDeleted + return result.editorStateWithChanges } } diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index d674d27e2c87..5a5e3e1b4fbd 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -101,13 +101,23 @@ export const runDeleteProperties = ( editorState: EditorState, command: DeleteProperties, ): CommandFunctionResult => { - const { editorStatePatch: propertyUpdatePatch, editorStateWithChanges: editorStateWithChanges } = - deleteValuesAtPath(editorState, command.element, command.properties) + const result = deleteValuesAtPath(editorState, command.element, command.properties) + if (result == null) { + return { + editorStatePatches: [], + commandDescription: `Delete Properties ${command.properties + .map(PP.toString) + .join(',')} on ${EP.toUid(command.element)}`, + } + } - const propertiesToUnsetPatches = getPropertiesToUnsetPatches(editorStateWithChanges, command) + const propertiesToUnsetPatches = getPropertiesToUnsetPatches( + result.editorStateWithChanges, + command, + ) return { - editorStatePatches: [propertyUpdatePatch, ...propertiesToUnsetPatches], + editorStatePatches: [result.editorStatePatch, ...propertiesToUnsetPatches], commandDescription: `Delete Properties ${command.properties .map(PP.toString) .join(',')} on ${EP.toUid(command.element)}`, @@ -118,7 +128,10 @@ export function deleteValuesAtPath( editorState: EditorState, target: ElementPath, properties: Array, -): { editorStateWithChanges: EditorState; editorStatePatch: EditorStatePatch } { +): { editorStateWithChanges: EditorState; editorStatePatch: EditorStatePatch } | null { + if (properties.length === 0) { + return null + } const workingEditorState = modifyUnderlyingElementForOpenFile( target, editorState, diff --git a/editor/src/components/canvas/commands/set-property-command.ts b/editor/src/components/canvas/commands/set-property-command.ts index a7b06f27de13..ccdcf4cd04aa 100644 --- a/editor/src/components/canvas/commands/set-property-command.ts +++ b/editor/src/components/canvas/commands/set-property-command.ts @@ -123,6 +123,14 @@ export const runBulkUpdateProperties: CommandFunction = ( command.element, propsToDelete.map((d) => d.path), ) + if (withDeletedProps == null) { + return { + editorStatePatches: [], + commandDescription: `Delete Properties ${command.properties + .map((p) => PP.toString(p.path)) + .join(',')} on ${EP.toUid(command.element)}`, + } + } // 2. Apply SET updates const propsToSet: PropertyToSet[] = mapDropNulls( diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 0b7b707739ad..76540f880595 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -6583,9 +6583,12 @@ function alignFlexOrGridChildren(editor: EditorState, views: ElementPath[], alig ) { let working = { ...editorState } - working = deleteValuesAtPath(working, view, [styleP(target)]).editorStateWithChanges + const result = deleteValuesAtPath(working, view, [styleP(target)]) + if (result == null) { + return working + } - working = applyValuesAtPath(working, view, [ + working = applyValuesAtPath(result.editorStateWithChanges, view, [ { path: styleP(dimension), value: jsExpressionValue(zeroRectIfNullOrInfinity(frame)[dimension], emptyComments), From 7ee418925302e9b245e59ecdd8dde5ab4cbab647 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 12:24:05 +0200 Subject: [PATCH 14/27] type-level `deleteValuesAtPath` --- .../commands/adjust-css-length-command.ts | 19 +++++++++------ .../commands/delete-properties-command.ts | 15 ++++++------ .../canvas/commands/set-property-command.ts | 23 +++++++------------ .../src/components/editor/actions/actions.tsx | 7 ++---- .../components/inspector/inspector-common.ts | 2 ++ 5 files changed, 31 insertions(+), 35 deletions(-) diff --git a/editor/src/components/canvas/commands/adjust-css-length-command.ts b/editor/src/components/canvas/commands/adjust-css-length-command.ts index 3a3056223e1d..1c961bdb15eb 100644 --- a/editor/src/components/canvas/commands/adjust-css-length-command.ts +++ b/editor/src/components/canvas/commands/adjust-css-length-command.ts @@ -24,6 +24,7 @@ import { parseCSSPercent, parseCSSPx, printCSSNumber } from '../../inspector/com import type { BaseCommand, CommandFunction, WhenToRun } from './commands' import { deleteValuesAtPath } from './delete-properties-command' import { patchParseSuccessAtElementPath } from './patch-utils' +import { isNonEmptyArray } from '../../../core/shared/array-utils' export type CreateIfNotExistant = 'create-if-not-existing' | 'do-not-create-if-doesnt-exist' @@ -441,13 +442,17 @@ export function deleteConflictingPropsForWidthHeight( if (propertiesToDelete.length === 0) { return editorState - } else { - const result = deleteValuesAtPath(editorState, target, propertiesToDelete) - - if (result == null) { - return editorState - } + } - return result.editorStateWithChanges + if (!isNonEmptyArray(propertiesToDelete)) { + return editorState } + + const { editorStateWithChanges: editorStateWithPropsDeleted } = deleteValuesAtPath( + editorState, + target, + propertiesToDelete, + ) + + return editorStateWithPropsDeleted } diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index 5a5e3e1b4fbd..49ac1670f547 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -16,7 +16,8 @@ import type { BaseCommand, CommandFunctionResult, WhenToRun } from './commands' import * as EP from '../../../core/shared/element-path' import * as PP from '../../../core/shared/property-path' import { patchParseSuccessAtElementPath } from './patch-utils' -import { mapDropNulls } from '../../../core/shared/array-utils' +import type { NonEmptyArray } from '../../../core/shared/array-utils' +import { isNonEmptyArray, mapDropNulls } from '../../../core/shared/array-utils' import { applyValuesAtPath } from './adjust-number-command' export interface DeleteProperties extends BaseCommand { @@ -101,8 +102,7 @@ export const runDeleteProperties = ( editorState: EditorState, command: DeleteProperties, ): CommandFunctionResult => { - const result = deleteValuesAtPath(editorState, command.element, command.properties) - if (result == null) { + if (!isNonEmptyArray(command.properties)) { return { editorStatePatches: [], commandDescription: `Delete Properties ${command.properties @@ -111,6 +111,8 @@ export const runDeleteProperties = ( } } + const result = deleteValuesAtPath(editorState, command.element, command.properties) + const propertiesToUnsetPatches = getPropertiesToUnsetPatches( result.editorStateWithChanges, command, @@ -127,11 +129,8 @@ export const runDeleteProperties = ( export function deleteValuesAtPath( editorState: EditorState, target: ElementPath, - properties: Array, -): { editorStateWithChanges: EditorState; editorStatePatch: EditorStatePatch } | null { - if (properties.length === 0) { - return null - } + properties: NonEmptyArray, +): { editorStateWithChanges: EditorState; editorStatePatch: EditorStatePatch } { const workingEditorState = modifyUnderlyingElementForOpenFile( target, editorState, diff --git a/editor/src/components/canvas/commands/set-property-command.ts b/editor/src/components/canvas/commands/set-property-command.ts index ccdcf4cd04aa..dda0dbffb671 100644 --- a/editor/src/components/canvas/commands/set-property-command.ts +++ b/editor/src/components/canvas/commands/set-property-command.ts @@ -1,4 +1,4 @@ -import { mapDropNulls } from '../../../core/shared/array-utils' +import { isNonEmptyArray, mapDropNulls } from '../../../core/shared/array-utils' import * as EP from '../../../core/shared/element-path' import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template' import type { @@ -118,19 +118,12 @@ export const runBulkUpdateProperties: CommandFunction = ( (p) => (p.type === 'DELETE' ? p : null), command.properties, ) - const withDeletedProps = deleteValuesAtPath( - editorState, - command.element, - propsToDelete.map((d) => d.path), - ) - if (withDeletedProps == null) { - return { - editorStatePatches: [], - commandDescription: `Delete Properties ${command.properties - .map((p) => PP.toString(p.path)) - .join(',')} on ${EP.toUid(command.element)}`, - } - } + + const propertyPathsToDelete = propsToDelete.map((d) => d.path) + + const withDeletedProps = isNonEmptyArray(propertyPathsToDelete) + ? deleteValuesAtPath(editorState, command.element, propertyPathsToDelete).editorStateWithChanges + : editorState // 2. Apply SET updates const propsToSet: PropertyToSet[] = mapDropNulls( @@ -138,7 +131,7 @@ export const runBulkUpdateProperties: CommandFunction = ( command.properties, ) const withSetProps = applyValuesAtPath( - withDeletedProps.editorStateWithChanges, + withDeletedProps, command.element, propsToSet.map((property) => ({ path: property.path, diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 76540f880595..0b7b707739ad 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -6583,12 +6583,9 @@ function alignFlexOrGridChildren(editor: EditorState, views: ElementPath[], alig ) { let working = { ...editorState } - const result = deleteValuesAtPath(working, view, [styleP(target)]) - if (result == null) { - return working - } + working = deleteValuesAtPath(working, view, [styleP(target)]).editorStateWithChanges - working = applyValuesAtPath(result.editorStateWithChanges, view, [ + working = applyValuesAtPath(working, view, [ { path: styleP(dimension), value: jsExpressionValue(zeroRectIfNullOrInfinity(frame)[dimension], emptyComments), diff --git a/editor/src/components/inspector/inspector-common.ts b/editor/src/components/inspector/inspector-common.ts index 32eeadabddf6..cd2d21789d26 100644 --- a/editor/src/components/inspector/inspector-common.ts +++ b/editor/src/components/inspector/inspector-common.ts @@ -3,7 +3,9 @@ import * as EP from '../../core/shared/element-path' import { getSimpleAttributeAtPath, MetadataUtils } from '../../core/model/element-metadata-utils' import { allElemsEqual, + isNonEmptyArray, mapDropNulls, + NonEmptyArray, safeIndex, strictEvery, stripNulls, From 01a4b58c0fd1c3ab9663a4fcc0f55f96f55ed643 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 12:28:28 +0200 Subject: [PATCH 15/27] remove flag --- .../strategies/grid-reparent-strategies.spec.browser2.tsx | 1 - .../strategies/set-flex-gap-strategy.spec.browser2.tsx | 1 - .../strategies/set-flex-gap-strategy.tsx | 2 +- .../canvas/commands/delete-properties-command.ts | 8 +------- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx index 3d60339e64a6..15cebb6d6195 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-reparent-strategies.spec.browser2.tsx @@ -23,7 +23,6 @@ import { renderTestEditorWithCode, } from '../../ui-jsx.test-utils' -// describe.only('grid reparent strategies', () => { describe('grid reparent strategies', () => { describe('reparent into a grid', () => { it('from the storyboard', async () => { diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx index cdda1ab2d898..bd91e06ef3d4 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.spec.browser2.tsx @@ -32,7 +32,6 @@ import { TailwindConfigPath } from '../../../../core/tailwind/tailwind-config' const DivTestId = 'mydiv' -// describe.only('Flex gap strategy', () => { describe('Flex gap strategy', () => { it('gap controls are not present when element has no children', async () => { const editor = await renderTestEditorWithCode( diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx index 870fc0b37d95..b7740ceeb0e8 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx @@ -177,7 +177,7 @@ export const setFlexGapStrategy: CanvasStrategyFactory = ( if (shouldTearOffGap) { return strategyApplicationResult( - [deleteProperties('always', selectedElement, [StyleGapProp], 'note-unset-properties')], + [deleteProperties('always', selectedElement, [StyleGapProp])], selectedElements, ) } diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index 49ac1670f547..77ac78ba44b1 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -24,21 +24,18 @@ export interface DeleteProperties extends BaseCommand { type: 'DELETE_PROPERTIES' element: ElementPath properties: Array - propertiesToUnsetBehaviour: 'note-unset-properties' | 'remove-properties' } export function deleteProperties( whenToRun: WhenToRun, element: ElementPath, properties: Array, - propertiesToUnsetBehaviour: 'note-unset-properties' | 'remove-properties' = 'remove-properties', ): DeleteProperties { return { type: 'DELETE_PROPERTIES', whenToRun: whenToRun, element: element, properties: properties, - propertiesToUnsetBehaviour: propertiesToUnsetBehaviour, } } @@ -75,10 +72,7 @@ function getPropertiesToUnsetPatches( editorState: EditorState, command: DeleteProperties, ): EditorStatePatch[] { - if ( - command.whenToRun === 'on-complete' || - command.propertiesToUnsetBehaviour === 'remove-properties' - ) { + if (command.whenToRun === 'on-complete') { return [] } From be3e2573cf82218072dc5054038dea3cc8043785 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 14:13:55 +0200 Subject: [PATCH 16/27] normalization step action + application --- editor/src/components/editor/action-types.ts | 6 +++++ .../editor/actions/action-creators.ts | 10 +++++++ .../components/editor/actions/action-utils.ts | 1 + .../src/components/editor/actions/actions.tsx | 26 ++++++++++++++++++- .../components/editor/store/editor-update.tsx | 2 ++ 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index f841a7836ecd..8bfcc9a9bfb0 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -1191,6 +1191,11 @@ export interface SetErrorBoundaryHandling { errorBoundaryHandling: ErrorBoundaryHandling } +export interface RunNormalizationStep { + action: 'RUN_NORMALIZATION_STEP' + elementsToNormalize: Array +} + export type EditorAction = | ClearSelection | InsertJSXElement @@ -1380,6 +1385,7 @@ export type EditorAction = | ResetOnlineState | IncreaseOnlineStateFailureCount | SetErrorBoundaryHandling + | RunNormalizationStep function actionForEach(action: EditorAction, fn: (action: EditorAction) => void): void { fn(action) diff --git a/editor/src/components/editor/actions/action-creators.ts b/editor/src/components/editor/actions/action-creators.ts index 5061e880e8aa..aaa389909752 100644 --- a/editor/src/components/editor/actions/action-creators.ts +++ b/editor/src/components/editor/actions/action-creators.ts @@ -237,6 +237,7 @@ import type { ToggleDataCanCondense, UpdateMetadataInEditorState, SetErrorBoundaryHandling, + RunNormalizationStep, } from '../action-types' import type { InsertionSubjectWrapper, Mode } from '../editor-modes' import { EditorModes, insertionSubject } from '../editor-modes' @@ -1896,3 +1897,12 @@ export function setErrorBoundaryHandling( errorBoundaryHandling: errorBoundaryHandling, } } + +export function runNormalizationStep( + elementsToNormalize: Array, +): RunNormalizationStep { + return { + action: 'RUN_NORMALIZATION_STEP', + elementsToNormalize: elementsToNormalize, + } +} diff --git a/editor/src/components/editor/actions/action-utils.ts b/editor/src/components/editor/actions/action-utils.ts index a695b30ad16a..7adabbb964e4 100644 --- a/editor/src/components/editor/actions/action-utils.ts +++ b/editor/src/components/editor/actions/action-utils.ts @@ -137,6 +137,7 @@ export function isTransientAction(action: EditorAction): boolean { case 'RESET_ONLINE_STATE': case 'INCREASE_ONLINE_STATE_FAILURE_COUNT': case 'SET_ERROR_BOUNDARY_HANDLING': + case 'RUN_NORMALIZATION_STEP': return true case 'TRUE_UP_ELEMENTS': diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 0b7b707739ad..4c9ce1d8b70b 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -352,6 +352,7 @@ import type { ToggleDataCanCondense, UpdateMetadataInEditorState, SetErrorBoundaryHandling, + RunNormalizationStep, } from '../action-types' import { isAlignment, isLoggedIn } from '../action-types' import type { Mode } from '../editor-modes' @@ -501,6 +502,7 @@ import { insertJSXElement, openCodeEditorFile, replaceMappedElement, + runNormalizationStep, scrollToPosition, selectComponents, setCodeEditorBuildErrors, @@ -630,6 +632,7 @@ import { getNavigatorTargetsFromEditorState } from '../../navigator/navigator-ut import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils' import { applyValuesAtPath } from '../../canvas/commands/adjust-number-command' import { styleP } from '../../inspector/inspector-common' +import { getActivePlugin } from '../../canvas/plugins/style-plugins' export const MIN_CODE_PANE_REOPEN_WIDTH = 100 @@ -5865,7 +5868,21 @@ export const UPDATE_FNS = { } }, APPLY_COMMANDS: (action: ApplyCommandsAction, editor: EditorModel): EditorModel => { - return foldAndApplyCommandsSimple(editor, action.commands) + const withCommandsApplied = foldAndApplyCommandsSimple(editor, action.commands) + + const affectedElements = mapDropNulls((command) => { + switch (command.type) { + case 'DELETE_PROPERTIES': + return command.element + default: + return null + } + }, action.commands) + + return UPDATE_FNS.RUN_NORMALIZATION_STEP( + runNormalizationStep(affectedElements), + withCommandsApplied, + ) }, UPDATE_COLOR_SWATCHES: (action: UpdateColorSwatches, editor: EditorModel): EditorModel => { return { @@ -6134,6 +6151,13 @@ export const UPDATE_FNS = { }, } }, + RUN_NORMALIZATION_STEP: (action: RunNormalizationStep, editor: EditorModel): EditorModel => { + const normalizedEditor = getActivePlugin(editor).normalizeFromInlineStyle( + editor, + action.elementsToNormalize, + ) + return normalizedEditor + }, } function copySelectionToClipboardMutating( diff --git a/editor/src/components/editor/store/editor-update.tsx b/editor/src/components/editor/store/editor-update.tsx index e663daa121e8..0b55ffda86c9 100644 --- a/editor/src/components/editor/store/editor-update.tsx +++ b/editor/src/components/editor/store/editor-update.tsx @@ -493,6 +493,8 @@ export function runSimpleLocalEditorAction( return UPDATE_FNS.REPLACE_ELEMENT_IN_SCOPE(action, state) case 'SET_ERROR_BOUNDARY_HANDLING': return UPDATE_FNS.SET_ERROR_BOUNDARY_HANDLING(action, state) + case 'RUN_NORMALIZATION_STEP': + return UPDATE_FNS.RUN_NORMALIZATION_STEP(action, state) default: return state } From 9dbcdc3e595cdc1725ddb94e5b3676f4c5db3e8c Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 14:49:43 +0200 Subject: [PATCH 17/27] more permissive transformJSXComponentAtElementPath --- .../commands/adjust-css-length-command.ts | 19 +++++++------------ .../commands/delete-properties-command.ts | 2 +- .../canvas/commands/set-property-command.ts | 13 ++++++------- .../components/inspector/inspector-common.ts | 2 -- .../src/core/model/element-template-utils.ts | 2 +- 5 files changed, 15 insertions(+), 23 deletions(-) diff --git a/editor/src/components/canvas/commands/adjust-css-length-command.ts b/editor/src/components/canvas/commands/adjust-css-length-command.ts index 1c961bdb15eb..90daf3c850e6 100644 --- a/editor/src/components/canvas/commands/adjust-css-length-command.ts +++ b/editor/src/components/canvas/commands/adjust-css-length-command.ts @@ -24,7 +24,6 @@ import { parseCSSPercent, parseCSSPx, printCSSNumber } from '../../inspector/com import type { BaseCommand, CommandFunction, WhenToRun } from './commands' import { deleteValuesAtPath } from './delete-properties-command' import { patchParseSuccessAtElementPath } from './patch-utils' -import { isNonEmptyArray } from '../../../core/shared/array-utils' export type CreateIfNotExistant = 'create-if-not-existing' | 'do-not-create-if-doesnt-exist' @@ -442,17 +441,13 @@ export function deleteConflictingPropsForWidthHeight( if (propertiesToDelete.length === 0) { return editorState - } + } else { + const { editorStateWithChanges: editorStateWithPropsDeleted } = deleteValuesAtPath( + editorState, + target, + propertiesToDelete, + ) - if (!isNonEmptyArray(propertiesToDelete)) { - return editorState + return editorStateWithPropsDeleted } - - const { editorStateWithChanges: editorStateWithPropsDeleted } = deleteValuesAtPath( - editorState, - target, - propertiesToDelete, - ) - - return editorStateWithPropsDeleted } diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index 77ac78ba44b1..03601e9d2084 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -123,7 +123,7 @@ export const runDeleteProperties = ( export function deleteValuesAtPath( editorState: EditorState, target: ElementPath, - properties: NonEmptyArray, + properties: Array, ): { editorStateWithChanges: EditorState; editorStatePatch: EditorStatePatch } { const workingEditorState = modifyUnderlyingElementForOpenFile( target, diff --git a/editor/src/components/canvas/commands/set-property-command.ts b/editor/src/components/canvas/commands/set-property-command.ts index dda0dbffb671..0a3635cfd0f5 100644 --- a/editor/src/components/canvas/commands/set-property-command.ts +++ b/editor/src/components/canvas/commands/set-property-command.ts @@ -118,12 +118,11 @@ export const runBulkUpdateProperties: CommandFunction = ( (p) => (p.type === 'DELETE' ? p : null), command.properties, ) - - const propertyPathsToDelete = propsToDelete.map((d) => d.path) - - const withDeletedProps = isNonEmptyArray(propertyPathsToDelete) - ? deleteValuesAtPath(editorState, command.element, propertyPathsToDelete).editorStateWithChanges - : editorState + const withDeletedProps = deleteValuesAtPath( + editorState, + command.element, + propsToDelete.map((d) => d.path), + ) // 2. Apply SET updates const propsToSet: PropertyToSet[] = mapDropNulls( @@ -131,7 +130,7 @@ export const runBulkUpdateProperties: CommandFunction = ( command.properties, ) const withSetProps = applyValuesAtPath( - withDeletedProps, + withDeletedProps.editorStateWithChanges, command.element, propsToSet.map((property) => ({ path: property.path, diff --git a/editor/src/components/inspector/inspector-common.ts b/editor/src/components/inspector/inspector-common.ts index cd2d21789d26..32eeadabddf6 100644 --- a/editor/src/components/inspector/inspector-common.ts +++ b/editor/src/components/inspector/inspector-common.ts @@ -3,9 +3,7 @@ import * as EP from '../../core/shared/element-path' import { getSimpleAttributeAtPath, MetadataUtils } from '../../core/model/element-metadata-utils' import { allElemsEqual, - isNonEmptyArray, mapDropNulls, - NonEmptyArray, safeIndex, strictEvery, stripNulls, diff --git a/editor/src/core/model/element-template-utils.ts b/editor/src/core/model/element-template-utils.ts index f5274b447419..0bd3f1f618c4 100644 --- a/editor/src/core/model/element-template-utils.ts +++ b/editor/src/core/model/element-template-utils.ts @@ -166,7 +166,7 @@ export function transformJSXComponentAtElementPath( const transformResult = transformAtPathOptionally(components, path, transform) if (transformResult.transformedElement == null) { - throw new Error(`Did not find element to transform ${EP.elementPathPartToString(path)}`) + return components } else { return transformResult.elements } From e0d6379766039a4580060c51663264b667168926 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 15:46:56 +0200 Subject: [PATCH 18/27] respect properties to unset from previous command --- .../components/canvas/commands/delete-properties-command.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index 03601e9d2084..8d97b6282c72 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -79,7 +79,11 @@ function getPropertiesToUnsetPatches( const unsetProperties = getUnsetProperties(command.properties) const partialPropertiesToUnset = getPropertiesToUnset(unsetProperties) const unsetPropertiesPatch: EditorStatePatch = { - canvas: { propertiesToUnset: { $set: partialPropertiesToUnset } }, + canvas: { + propertiesToUnset: { + $set: { ...partialPropertiesToUnset, ...editorState.canvas.propertiesToUnset }, + }, + }, } const { editorStatePatch: setPropertiesToUnsetValuePatch } = applyValuesAtPath( editorState, From fbc4f977496bfdd72d1a84cbf3a51f7806de4421 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 15:47:19 +0200 Subject: [PATCH 19/27] normalize in `executeFirstApplicableStrategyForContinuousInteraction` --- .../src/components/editor/actions/actions.tsx | 3 ++- .../inspector-strategy.ts | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 4c9ce1d8b70b..a68017b974b3 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -6152,7 +6152,8 @@ export const UPDATE_FNS = { } }, RUN_NORMALIZATION_STEP: (action: RunNormalizationStep, editor: EditorModel): EditorModel => { - const normalizedEditor = getActivePlugin(editor).normalizeFromInlineStyle( + const activePlugin = getActivePlugin(editor) + const normalizedEditor = activePlugin.normalizeFromInlineStyle( editor, action.elementsToNormalize, ) diff --git a/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts b/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts index 38af9cf837a9..e9438cec5639 100644 --- a/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts +++ b/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts @@ -1,6 +1,12 @@ +import { mapDropNulls } from '../../../core/shared/array-utils' +import * as EP from '../../../core/shared/element-path' import type { CanvasCommand } from '../../canvas/commands/commands' import type { EditorDispatch } from '../../editor/action-types' -import { applyCommandsAction, transientActions } from '../../editor/actions/action-creators' +import { + applyCommandsAction, + runNormalizationStep, + transientActions, +} from '../../editor/actions/action-creators' import type { ElementsToRerender } from '../../editor/store/editor-state' interface CustomInspectorStrategyResultBase { @@ -67,7 +73,21 @@ export function executeFirstApplicableStrategyForContinuousInteraction( if (elementsToRerenderTransient !== 'rerender-all-elements') { dispatch([transientActions([applyCommandsAction(commands)], elementsToRerenderTransient)]) } else { - dispatch([applyCommandsAction(commands)]) + dispatch([ + applyCommandsAction(commands), + runNormalizationStep( + EP.uniqueElementPaths( + mapDropNulls((command) => { + switch (command.type) { + case 'DELETE_PROPERTIES': + return command.element + default: + return null + } + }, commands), + ), + ), + ]) } } } From e1fd3b65326c922b007a8ef945d1f1dec72f6ebe Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 16:03:33 +0200 Subject: [PATCH 20/27] check tailwind feature flag for tests --- editor/src/components/canvas/plugins/style-plugins.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index edbf8ce5720c..cde29ef3fc21 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -7,6 +7,7 @@ import { getTailwindConfigCached, isTailwindEnabled, } from '../../../core/tailwind/tailwind-compilation' +import { isFeatureEnabled } from '../../../utils/feature-switches' export interface StylePlugin { name: string @@ -23,7 +24,7 @@ export const Plugins = { } as const export function getActivePlugin(editorState: EditorState): StylePlugin { - if (isTailwindEnabled()) { + if (isFeatureEnabled('Tailwind') || isTailwindEnabled()) { return TailwindPlugin(getTailwindConfigCached(editorState)) } return InlineStylePlugin From f725e9287b5591de0412ad0b07c3a953c7ebc221 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 16:06:38 +0200 Subject: [PATCH 21/27] update `transformJSXComponentAtPath` tests --- .../src/core/model/element-template-utils.spec.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/editor/src/core/model/element-template-utils.spec.tsx b/editor/src/core/model/element-template-utils.spec.tsx index f8235813ddb1..936ab645c1e8 100644 --- a/editor/src/core/model/element-template-utils.spec.tsx +++ b/editor/src/core/model/element-template-utils.spec.tsx @@ -1646,7 +1646,7 @@ describe('transformJSXComponentAtPath', () => { return getComponentsFromTopLevelElements(file.lastParseSuccess.topLevelElements) } - it('throws exception on missing child of jsx element', () => { + it('returns the component array unchanges on missing child of jsx element', () => { const components = createTestComponentsForSnippet(`

@@ -1662,7 +1662,7 @@ describe('transformJSXComponentAtPath', () => { const pathToModify = 'utopia-storyboard-uid/scene-aaa/app-entity:aaa/parent/child-b/xxx' - expect(() => + expect( transformJSXComponentAtPath( components, EP.dynamicPathToStaticPath(EP.fromString(pathToModify)), @@ -1676,7 +1676,7 @@ describe('transformJSXComponentAtPath', () => { return element }, ), - ).toThrow() + ).toEqual(components) }) // TODO activate this after removing absolutely stupid way of falling back to the conditional when the branch doesn't exist xit('throws exception on missing branch of conditional', () => { @@ -1712,7 +1712,7 @@ describe('transformJSXComponentAtPath', () => { ), ).toThrow() }) - it('throws exception on missing elementsWithin of expression', () => { + it('returns the component array unchanges on missing elementsWithin of expression', () => { const components = createTestComponentsForSnippet(`
@@ -1726,7 +1726,7 @@ describe('transformJSXComponentAtPath', () => { const pathToModify = 'utopia-storyboard-uid/scene-aaa/app-entity:aaa/parent/2f2/xxx' - expect(() => + expect( transformJSXComponentAtPath( components, EP.dynamicPathToStaticPath(EP.fromString(pathToModify)), @@ -1740,7 +1740,7 @@ describe('transformJSXComponentAtPath', () => { return element }, ), - ).toThrow() + ).toEqual(components) }) it('updates a jsx element with jsx element parent', () => { const components = createTestComponentsForSnippet(` From 211f57bdeccd3d71c05448d4b48806c32f477c18 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 16:20:05 +0200 Subject: [PATCH 22/27] whoops --- editor/src/components/canvas/plugins/style-plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index cde29ef3fc21..91569acae4e5 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -24,7 +24,7 @@ export const Plugins = { } as const export function getActivePlugin(editorState: EditorState): StylePlugin { - if (isFeatureEnabled('Tailwind') || isTailwindEnabled()) { + if (isFeatureEnabled('Tailwind') && isTailwindEnabled()) { return TailwindPlugin(getTailwindConfigCached(editorState)) } return InlineStylePlugin From 6cd836089039455f597189ebcb96e08496680d79 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 17:03:16 +0200 Subject: [PATCH 23/27] add todo --- editor/src/components/editor/actions/actions.tsx | 2 ++ .../inspector/inspector-strategies/inspector-strategy.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index a68017b974b3..95951841d429 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -5870,6 +5870,8 @@ export const UPDATE_FNS = { APPLY_COMMANDS: (action: ApplyCommandsAction, editor: EditorModel): EditorModel => { const withCommandsApplied = foldAndApplyCommandsSimple(editor, action.commands) + // TODO: extract this into a helper/extend the switch when adding Tailwind + // support for the inspector controls const affectedElements = mapDropNulls((command) => { switch (command.type) { case 'DELETE_PROPERTIES': diff --git a/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts b/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts index e9438cec5639..5cd995183b48 100644 --- a/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts +++ b/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts @@ -76,6 +76,8 @@ export function executeFirstApplicableStrategyForContinuousInteraction( dispatch([ applyCommandsAction(commands), runNormalizationStep( + // TODO: extract this into a helper/extend the switch when adding Tailwind + // support for the inspector controls EP.uniqueElementPaths( mapDropNulls((command) => { switch (command.type) { From 4fc40a5bd68e183c0e11172d633ff50549076d98 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 17:12:26 +0200 Subject: [PATCH 24/27] remove leftover code --- .../commands/delete-properties-command.ts | 22 +++++-------------- .../canvas/commands/set-property-command.ts | 2 +- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index 8d97b6282c72..0951d622a38f 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -16,8 +16,7 @@ import type { BaseCommand, CommandFunctionResult, WhenToRun } from './commands' import * as EP from '../../../core/shared/element-path' import * as PP from '../../../core/shared/property-path' import { patchParseSuccessAtElementPath } from './patch-utils' -import type { NonEmptyArray } from '../../../core/shared/array-utils' -import { isNonEmptyArray, mapDropNulls } from '../../../core/shared/array-utils' +import { mapDropNulls } from '../../../core/shared/array-utils' import { applyValuesAtPath } from './adjust-number-command' export interface DeleteProperties extends BaseCommand { @@ -100,24 +99,13 @@ export const runDeleteProperties = ( editorState: EditorState, command: DeleteProperties, ): CommandFunctionResult => { - if (!isNonEmptyArray(command.properties)) { - return { - editorStatePatches: [], - commandDescription: `Delete Properties ${command.properties - .map(PP.toString) - .join(',')} on ${EP.toUid(command.element)}`, - } - } + const { editorStatePatch: propertyUpdatePatch, editorStateWithChanges: withPropertiesRemoved } = + deleteValuesAtPath(editorState, command.element, command.properties) - const result = deleteValuesAtPath(editorState, command.element, command.properties) - - const propertiesToUnsetPatches = getPropertiesToUnsetPatches( - result.editorStateWithChanges, - command, - ) + const propertiesToUnsetPatches = getPropertiesToUnsetPatches(withPropertiesRemoved, command) return { - editorStatePatches: [result.editorStatePatch, ...propertiesToUnsetPatches], + editorStatePatches: [propertyUpdatePatch, ...propertiesToUnsetPatches], commandDescription: `Delete Properties ${command.properties .map(PP.toString) .join(',')} on ${EP.toUid(command.element)}`, diff --git a/editor/src/components/canvas/commands/set-property-command.ts b/editor/src/components/canvas/commands/set-property-command.ts index 0a3635cfd0f5..a7b06f27de13 100644 --- a/editor/src/components/canvas/commands/set-property-command.ts +++ b/editor/src/components/canvas/commands/set-property-command.ts @@ -1,4 +1,4 @@ -import { isNonEmptyArray, mapDropNulls } from '../../../core/shared/array-utils' +import { mapDropNulls } from '../../../core/shared/array-utils' import * as EP from '../../../core/shared/element-path' import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template' import type { From b75db382286f5c01e3217a9b63c4541ad4a41726 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 14 Oct 2024 17:51:22 +0200 Subject: [PATCH 25/27] more granular propertiesToUnset --- .../commands/delete-properties-command.ts | 18 ++- .../canvas/plugins/inline-style-plugin.ts | 7 +- .../canvas/plugins/tailwind-style-plugin.ts | 123 ++++++++++-------- .../components/editor/store/editor-state.ts | 8 +- .../store/store-deep-equality-instances.ts | 10 +- 5 files changed, 98 insertions(+), 68 deletions(-) diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index 0951d622a38f..afd63857e550 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -7,6 +7,7 @@ import type { EditorState, EditorStatePatch, PropertiesToUnset, + UnsetPropertyValues, } from '../../../components/editor/store/editor-state' import { modifyUnderlyingElementForOpenFile } from '../../../components/editor/store/editor-state' import { foldEither } from '../../../core/shared/either' @@ -39,8 +40,8 @@ export function deleteProperties( } type PropertiesToUnsetArray = Array<{ - prop: keyof PropertiesToUnset - value: PropertiesToUnset[keyof PropertiesToUnset] + prop: keyof UnsetPropertyValues + value: UnsetPropertyValues[keyof UnsetPropertyValues] path: PropertyPath }> @@ -59,8 +60,8 @@ function getUnsetProperties(properties: Array): PropertiesToUnsetA }, properties) } -function getPropertiesToUnset(propertiesToUnset: PropertiesToUnsetArray): PropertiesToUnset { - let result: Partial = {} +function getPropertiesToUnset(propertiesToUnset: PropertiesToUnsetArray): UnsetPropertyValues { + let result: UnsetPropertyValues = {} for (const { prop, value } of propertiesToUnset) { result[prop] = value } @@ -77,10 +78,17 @@ function getPropertiesToUnsetPatches( const unsetProperties = getUnsetProperties(command.properties) const partialPropertiesToUnset = getPropertiesToUnset(unsetProperties) + const pathString = EP.toString(command.element) const unsetPropertiesPatch: EditorStatePatch = { canvas: { propertiesToUnset: { - $set: { ...partialPropertiesToUnset, ...editorState.canvas.propertiesToUnset }, + $set: { + ...editorState.canvas.propertiesToUnset, + [pathString]: { + ...editorState.canvas.propertiesToUnset[pathString], + ...partialPropertiesToUnset, + }, + }, }, }, } diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 9a4e7f20c3d4..add9fd9bab41 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -2,6 +2,7 @@ import { getLayoutProperty } from '../../../core/layout/getLayoutProperty' import type { StyleLayoutProp } from '../../../core/layout/layout-helpers-new' import { MetadataUtils } from '../../../core/model/element-metadata-utils' import { defaultEither, isLeft, mapEither, right } from '../../../core/shared/either' +import * as EP from '../../../core/shared/element-path' import type { JSXElement } from '../../../core/shared/element-template' import { isJSXElement } from '../../../core/shared/element-template' import { typedObjectKeys } from '../../../core/shared/object-utils' @@ -44,11 +45,11 @@ export const InlineStylePlugin: StylePlugin = { normalizeFromInlineStyle: (editor, elementsToNormalize) => { return foldAndApplyCommandsSimple( editor, - elementsToNormalize.flatMap((element) => + Object.entries(editor.canvas.propertiesToUnset).map(([pathString, propertiesToUnset]) => deleteProperties( 'on-complete', - element, - typedObjectKeys(editor.canvas.propertiesToUnset).map((p) => PP.create('style', p)), + EP.fromString(pathString), + typedObjectKeys(propertiesToUnset).map((p) => PP.create('style', p)), ), ), ) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 13473902f205..019708377927 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -1,8 +1,8 @@ import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' -import type { JSExpression, JSXAttributesEntry } from 'utopia-shared/src/types' +import type { ElementPath, JSExpression, JSXAttributesEntry } from 'utopia-shared/src/types' import { isLeft } from '../../../core/shared/either' import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' -import type { PropertiesToUnset } from '../../editor/store/editor-state' +import type { EditorState, UnsetPropertyValues } from '../../editor/store/editor-state' import { getElementFromProjectContents, getJSXElementFromProjectContents, @@ -20,6 +20,7 @@ import type { WithPropertyTag } from '../canvas-types' import { withPropertyTag } from '../canvas-types' import type { Config } from 'tailwindcss/types/config' import { typedObjectKeys } from '../../../core/shared/object-utils' +import * as EP from '../../../core/shared/element-path' function parseTailwindProperty(value: unknown, parse: Parser): WithPropertyTag | null { const parsed = parse(value, null) @@ -86,12 +87,73 @@ function getStylePropContents( return null } -function getRemoveUpdates(propertiesToUnset: PropertiesToUnset): UCL.ClassListUpdate[] { +function getRemoveUpdates(propertiesToUnset: UnsetPropertyValues): UCL.ClassListUpdate[] { return typedObjectKeys(propertiesToUnset).map((property) => UCL.remove(TailwindPropertyMapping[property]), ) } +function getNormalizationCommands(elementsToNormalize: ElementPath[], editorState: EditorState) { + return elementsToNormalize.flatMap((elementPath) => { + const element = getJSXElementFromProjectContents(elementPath, editorState.projectContents) + if (element == null) { + return [] + } + + const styleAttribute = element.props.find( + (prop): prop is JSXAttributesEntry => + prop.type === 'JSX_ATTRIBUTES_ENTRY' && prop.key === 'style', + ) + if (styleAttribute == null) { + return [] + } + + const styleValue = getStylePropContents(styleAttribute.value) + if (styleValue == null) { + return [] + } + + const styleProps = mapDropNulls( + (c): { key: keyof typeof TailwindPropertyMapping; value: unknown } | null => + isSupportedTailwindProperty(c.key) ? { key: c.key, value: c.value } : null, + styleValue, + ) + + const stylePropConversions = mapDropNulls(({ key, value }) => { + const valueString = stringifiedStylePropValue(value) + if (valueString == null) { + return null + } + + return { property: TailwindPropertyMapping[key], value: valueString } + }, styleProps) + + return [ + deleteProperties( + 'on-complete', + elementPath, + Object.keys(TailwindPropertyMapping).map((prop) => PP.create('style', prop)), + ), + updateClassListCommand( + 'always', + elementPath, + stylePropConversions.map(({ property, value }) => UCL.add({ property, value })), + ), + ] + }) +} + +function getPropertyCleanupCommands(editorState: EditorState) { + return Object.entries(editorState.canvas.propertiesToUnset).map( + ([pathString, propertiesToUnset]) => + updateClassListCommand( + 'always', + EP.fromString(pathString), + getRemoveUpdates(propertiesToUnset), + ), + ) +} + export const TailwindPlugin = (config: Config | null): StylePlugin => ({ name: 'Tailwind', styleInfoFactory: @@ -116,58 +178,11 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ } }, normalizeFromInlineStyle: (editorState, elementsToNormalize) => { - const commands = elementsToNormalize.flatMap((elementPath) => { - const element = getJSXElementFromProjectContents(elementPath, editorState.projectContents) - if (element == null) { - return [] - } + const commands = [ + ...getNormalizationCommands(elementsToNormalize, editorState), + ...getPropertyCleanupCommands(editorState), + ] - const styleAttribute = element.props.find( - (prop): prop is JSXAttributesEntry => - prop.type === 'JSX_ATTRIBUTES_ENTRY' && prop.key === 'style', - ) - if (styleAttribute == null) { - return [] - } - - const styleValue = getStylePropContents(styleAttribute.value) - if (styleValue == null) { - return [] - } - - const styleProps = mapDropNulls( - (c): { key: keyof typeof TailwindPropertyMapping; value: unknown } | null => - isSupportedTailwindProperty(c.key) ? { key: c.key, value: c.value } : null, - styleValue, - ) - - const stylePropConversions = mapDropNulls(({ key, value }) => { - const valueString = stringifiedStylePropValue(value) - if (valueString == null) { - return null - } - - return { property: TailwindPropertyMapping[key], value: valueString } - }, styleProps) - - return [ - deleteProperties( - 'on-complete', - elementPath, - Object.keys(TailwindPropertyMapping).map((prop) => PP.create('style', prop)), - ), - updateClassListCommand( - 'always', - elementPath, - stylePropConversions.map(({ property, value }) => UCL.add({ property, value })), - ), - updateClassListCommand( - 'always', - elementPath, - getRemoveUpdates(editorState.canvas.propertiesToUnset), - ), - ] - }) if (commands.length === 0) { return editorState } diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index 24d015177c59..245719245462 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -869,10 +869,14 @@ export function internalClipboard( } } -export interface PropertiesToUnset { +export interface UnsetPropertyValues { gap?: '0px' } +export interface PropertiesToUnset { + [pathString: string]: UnsetPropertyValues +} + export interface EditorStateCanvas { elementsToRerender: ElementsToRerender interactionSession: InteractionSession | null @@ -918,7 +922,7 @@ export function editorStateCanvas( resizeOpts: ResizeOptions, domWalkerAdditionalElementsToUpdate: Array, controls: EditorStateCanvasControls, - propertiesToUnset: Partial, + propertiesToUnset: PropertiesToUnset, ): EditorStateCanvas { return { elementsToRerender: elementsToRerender, diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index 02e77362a682..abba3e5b59aa 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -2769,10 +2769,12 @@ export const EditorStateCanvasControlsKeepDeepEquality: KeepDeepEqualityCall = - combine1EqualityCall( - (p) => p.gap, - undefinableDeepEquality(createCallWithTripleEquals()), - (gap) => ({ gap }), + objectDeepEquality( + combine1EqualityCall( + (p) => p.gap, + undefinableDeepEquality(createCallWithTripleEquals()), + (gap) => ({ gap }), + ), ) export const ModifiersKeepDeepEquality: KeepDeepEqualityCall = combine4EqualityCalls( From d9676962d90e8a9e70b1517cf84227743056891c Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 15 Oct 2024 11:02:00 +0200 Subject: [PATCH 26/27] safe deleteValuesAtPath --- .../commands/adjust-css-length-command.ts | 12 ++++----- .../commands/delete-properties-command.ts | 27 ++++++++++++++----- .../canvas/commands/set-property-command.ts | 2 +- .../src/components/editor/actions/actions.tsx | 3 ++- .../model/element-template-utils.spec.tsx | 12 ++++----- .../src/core/model/element-template-utils.ts | 2 +- 6 files changed, 37 insertions(+), 21 deletions(-) diff --git a/editor/src/components/canvas/commands/adjust-css-length-command.ts b/editor/src/components/canvas/commands/adjust-css-length-command.ts index 90daf3c850e6..3a3056223e1d 100644 --- a/editor/src/components/canvas/commands/adjust-css-length-command.ts +++ b/editor/src/components/canvas/commands/adjust-css-length-command.ts @@ -442,12 +442,12 @@ export function deleteConflictingPropsForWidthHeight( if (propertiesToDelete.length === 0) { return editorState } else { - const { editorStateWithChanges: editorStateWithPropsDeleted } = deleteValuesAtPath( - editorState, - target, - propertiesToDelete, - ) + const result = deleteValuesAtPath(editorState, target, propertiesToDelete) + + if (result == null) { + return editorState + } - return editorStateWithPropsDeleted + return result.editorStateWithChanges } } diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index afd63857e550..844f7ecb06e8 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -6,7 +6,6 @@ import { import type { EditorState, EditorStatePatch, - PropertiesToUnset, UnsetPropertyValues, } from '../../../components/editor/store/editor-state' import { modifyUnderlyingElementForOpenFile } from '../../../components/editor/store/editor-state' @@ -17,7 +16,7 @@ import type { BaseCommand, CommandFunctionResult, WhenToRun } from './commands' import * as EP from '../../../core/shared/element-path' import * as PP from '../../../core/shared/property-path' import { patchParseSuccessAtElementPath } from './patch-utils' -import { mapDropNulls } from '../../../core/shared/array-utils' +import { mapDropNulls, stripNulls } from '../../../core/shared/array-utils' import { applyValuesAtPath } from './adjust-number-command' export interface DeleteProperties extends BaseCommand { @@ -107,13 +106,13 @@ export const runDeleteProperties = ( editorState: EditorState, command: DeleteProperties, ): CommandFunctionResult => { - const { editorStatePatch: propertyUpdatePatch, editorStateWithChanges: withPropertiesRemoved } = - deleteValuesAtPath(editorState, command.element, command.properties) + const result = deleteValuesAtPath(editorState, command.element, command.properties) - const propertiesToUnsetPatches = getPropertiesToUnsetPatches(withPropertiesRemoved, command) + const updatedEditorState = result == null ? editorState : result.editorStateWithChanges + const propertiesToUnsetPatches = getPropertiesToUnsetPatches(updatedEditorState, command) return { - editorStatePatches: [propertyUpdatePatch, ...propertiesToUnsetPatches], + editorStatePatches: stripNulls([result?.editorStatePatch, ...propertiesToUnsetPatches]), commandDescription: `Delete Properties ${command.properties .map(PP.toString) .join(',')} on ${EP.toUid(command.element)}`, @@ -124,6 +123,22 @@ export function deleteValuesAtPath( editorState: EditorState, target: ElementPath, properties: Array, +): { editorStateWithChanges: EditorState; editorStatePatch: EditorStatePatch } | null { + try { + return deleteValuesAtPathUnsafe(editorState, target, properties) + } catch { + return null + } +} + +// This function is unsafe, because it calls +// `transformJSXComponentAtElementPath` internally, and +// `transformJSXComponentAtElementPath` throws an error if it cannot find an +// element at the element path passed to it +function deleteValuesAtPathUnsafe( + editorState: EditorState, + target: ElementPath, + properties: Array, ): { editorStateWithChanges: EditorState; editorStatePatch: EditorStatePatch } { const workingEditorState = modifyUnderlyingElementForOpenFile( target, diff --git a/editor/src/components/canvas/commands/set-property-command.ts b/editor/src/components/canvas/commands/set-property-command.ts index a7b06f27de13..e804dee073eb 100644 --- a/editor/src/components/canvas/commands/set-property-command.ts +++ b/editor/src/components/canvas/commands/set-property-command.ts @@ -130,7 +130,7 @@ export const runBulkUpdateProperties: CommandFunction = ( command.properties, ) const withSetProps = applyValuesAtPath( - withDeletedProps.editorStateWithChanges, + withDeletedProps == null ? editorState : withDeletedProps.editorStateWithChanges, command.element, propsToSet.map((property) => ({ path: property.path, diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 95951841d429..11d3ae0fa72a 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -6610,7 +6610,8 @@ function alignFlexOrGridChildren(editor: EditorState, views: ElementPath[], alig ) { let working = { ...editorState } - working = deleteValuesAtPath(working, view, [styleP(target)]).editorStateWithChanges + const withDeletedProps = deleteValuesAtPath(working, view, [styleP(target)]) + working = withDeletedProps == null ? working : withDeletedProps.editorStateWithChanges working = applyValuesAtPath(working, view, [ { diff --git a/editor/src/core/model/element-template-utils.spec.tsx b/editor/src/core/model/element-template-utils.spec.tsx index 936ab645c1e8..f8235813ddb1 100644 --- a/editor/src/core/model/element-template-utils.spec.tsx +++ b/editor/src/core/model/element-template-utils.spec.tsx @@ -1646,7 +1646,7 @@ describe('transformJSXComponentAtPath', () => { return getComponentsFromTopLevelElements(file.lastParseSuccess.topLevelElements) } - it('returns the component array unchanges on missing child of jsx element', () => { + it('throws exception on missing child of jsx element', () => { const components = createTestComponentsForSnippet(`
@@ -1662,7 +1662,7 @@ describe('transformJSXComponentAtPath', () => { const pathToModify = 'utopia-storyboard-uid/scene-aaa/app-entity:aaa/parent/child-b/xxx' - expect( + expect(() => transformJSXComponentAtPath( components, EP.dynamicPathToStaticPath(EP.fromString(pathToModify)), @@ -1676,7 +1676,7 @@ describe('transformJSXComponentAtPath', () => { return element }, ), - ).toEqual(components) + ).toThrow() }) // TODO activate this after removing absolutely stupid way of falling back to the conditional when the branch doesn't exist xit('throws exception on missing branch of conditional', () => { @@ -1712,7 +1712,7 @@ describe('transformJSXComponentAtPath', () => { ), ).toThrow() }) - it('returns the component array unchanges on missing elementsWithin of expression', () => { + it('throws exception on missing elementsWithin of expression', () => { const components = createTestComponentsForSnippet(`
@@ -1726,7 +1726,7 @@ describe('transformJSXComponentAtPath', () => { const pathToModify = 'utopia-storyboard-uid/scene-aaa/app-entity:aaa/parent/2f2/xxx' - expect( + expect(() => transformJSXComponentAtPath( components, EP.dynamicPathToStaticPath(EP.fromString(pathToModify)), @@ -1740,7 +1740,7 @@ describe('transformJSXComponentAtPath', () => { return element }, ), - ).toEqual(components) + ).toThrow() }) it('updates a jsx element with jsx element parent', () => { const components = createTestComponentsForSnippet(` diff --git a/editor/src/core/model/element-template-utils.ts b/editor/src/core/model/element-template-utils.ts index 0bd3f1f618c4..f5274b447419 100644 --- a/editor/src/core/model/element-template-utils.ts +++ b/editor/src/core/model/element-template-utils.ts @@ -166,7 +166,7 @@ export function transformJSXComponentAtElementPath( const transformResult = transformAtPathOptionally(components, path, transform) if (transformResult.transformedElement == null) { - return components + throw new Error(`Did not find element to transform ${EP.elementPathPartToString(path)}`) } else { return transformResult.elements } From b9d46eb05c207bff4b991893e66c4d9e02b30a9d Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 15 Oct 2024 15:23:05 +0200 Subject: [PATCH 27/27] always apply the unsetProperties patch --- .../canvas/commands/delete-properties-command.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/editor/src/components/canvas/commands/delete-properties-command.ts b/editor/src/components/canvas/commands/delete-properties-command.ts index 844f7ecb06e8..3016a3f7d01c 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -71,10 +71,6 @@ function getPropertiesToUnsetPatches( editorState: EditorState, command: DeleteProperties, ): EditorStatePatch[] { - if (command.whenToRun === 'on-complete') { - return [] - } - const unsetProperties = getUnsetProperties(command.properties) const partialPropertiesToUnset = getPropertiesToUnset(unsetProperties) const pathString = EP.toString(command.element) @@ -91,6 +87,11 @@ function getPropertiesToUnsetPatches( }, }, } + + if (command.whenToRun === 'on-complete') { + return [unsetPropertiesPatch] + } + const { editorStatePatch: setPropertiesToUnsetValuePatch } = applyValuesAtPath( editorState, command.element,