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/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 f1d8501385b3..3016a3f7d01c 100644 --- a/editor/src/components/canvas/commands/delete-properties-command.ts +++ b/editor/src/components/canvas/commands/delete-properties-command.ts @@ -1,13 +1,23 @@ -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, + UnsetPropertyValues, +} 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 { mapDropNulls, stripNulls } from '../../../core/shared/array-utils' +import { applyValuesAtPath } from './adjust-number-command' export interface DeleteProperties extends BaseCommand { type: 'DELETE_PROPERTIES' @@ -28,19 +38,82 @@ export function deleteProperties( } } -export const runDeleteProperties: CommandFunction = ( +type PropertiesToUnsetArray = Array<{ + prop: keyof UnsetPropertyValues + value: UnsetPropertyValues[keyof UnsetPropertyValues] + 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): UnsetPropertyValues { + let result: UnsetPropertyValues = {} + for (const { prop, value } of propertiesToUnset) { + result[prop] = value + } + return result +} + +function getPropertiesToUnsetPatches( editorState: EditorState, command: DeleteProperties, -) => { - // Apply the update to the properties. - const { editorStatePatch: propertyUpdatePatch } = deleteValuesAtPath( +): EditorStatePatch[] { + const unsetProperties = getUnsetProperties(command.properties) + const partialPropertiesToUnset = getPropertiesToUnset(unsetProperties) + const pathString = EP.toString(command.element) + const unsetPropertiesPatch: EditorStatePatch = { + canvas: { + propertiesToUnset: { + $set: { + ...editorState.canvas.propertiesToUnset, + [pathString]: { + ...editorState.canvas.propertiesToUnset[pathString], + ...partialPropertiesToUnset, + }, + }, + }, + }, + } + + if (command.whenToRun === 'on-complete') { + return [unsetPropertiesPatch] + } + + const { editorStatePatch: setPropertiesToUnsetValuePatch } = applyValuesAtPath( editorState, command.element, - command.properties, + unsetProperties.map(({ path, value }) => ({ + path: path, + value: jsExpressionValue(value, emptyComments), + })), ) + return [unsetPropertiesPatch, setPropertiesToUnsetValuePatch] +} + +export const runDeleteProperties = ( + editorState: EditorState, + command: DeleteProperties, +): CommandFunctionResult => { + const result = deleteValuesAtPath(editorState, command.element, command.properties) + + const updatedEditorState = result == null ? editorState : result.editorStateWithChanges + const propertiesToUnsetPatches = getPropertiesToUnsetPatches(updatedEditorState, command) return { - editorStatePatches: [propertyUpdatePatch], + editorStatePatches: stripNulls([result?.editorStatePatch, ...propertiesToUnsetPatches]), commandDescription: `Delete Properties ${command.properties .map(PP.toString) .join(',')} on ${EP.toUid(command.element)}`, @@ -51,6 +124,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/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 11bc7dfa873b..add9fd9bab41 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -2,11 +2,16 @@ 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' +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 +42,16 @@ export const InlineStylePlugin: StylePlugin = { flexDirection: flexDirection, } }, - normalizeFromInlineStyle: (editor) => editor, + normalizeFromInlineStyle: (editor, elementsToNormalize) => { + return foldAndApplyCommandsSimple( + editor, + Object.entries(editor.canvas.propertiesToUnset).map(([pathString, propertiesToUnset]) => + deleteProperties( + 'on-complete', + EP.fromString(pathString), + typedObjectKeys(propertiesToUnset).map((p) => PP.create('style', p)), + ), + ), + ) + }, } diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index edbf8ce5720c..91569acae4e5 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 diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 23e2e7413a9c..019708377927 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 { ElementPath, JSExpression, JSXAttributesEntry } from 'utopia-shared/src/types' import { isLeft } from '../../../core/shared/either' import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' +import type { EditorState, UnsetPropertyValues } from '../../editor/store/editor-state' import { getElementFromProjectContents, getJSXElementFromProjectContents, @@ -18,6 +19,8 @@ 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' +import * as EP from '../../../core/shared/element-path' function parseTailwindProperty(value: unknown, parse: Parser): WithPropertyTag | null { const parsed = parse(value, null) @@ -59,6 +62,98 @@ function getTailwindClassMapping(classes: string[], config: Config | null) { return mapping } +function getStylePropContents( + 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: 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: @@ -83,57 +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 styleAttribute = element.props.find( - (prop): prop is JSXAttributesEntry => - prop.type === 'JSX_ATTRIBUTES_ENTRY' && prop.key === 'style', - ) - if (styleAttribute == null) { - return [] - } + const commands = [ + ...getNormalizationCommands(elementsToNormalize, editorState), + ...getPropertyCleanupCommands(editorState), + ] - 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 - } - - return { property: TailwindPropertyMapping[key], value: valueString } - }, styleProps) - - return [ - deleteProperties( - 'always', - elementPath, - Object.keys(TailwindPropertyMapping).map((prop) => PP.create('style', prop)), - ), - updateClassListCommand( - 'always', - elementPath, - stylePropConversions.map(({ property, value }) => UCL.add({ property, value })), - ), - ] - }) if (commands.length === 0) { return editorState } 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 b70275861002..11d3ae0fa72a 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 @@ -970,6 +973,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, @@ -5864,7 +5868,23 @@ export const UPDATE_FNS = { } }, APPLY_COMMANDS: (action: ApplyCommandsAction, editor: EditorModel): EditorModel => { - return foldAndApplyCommandsSimple(editor, action.commands) + 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': + 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 { @@ -6133,6 +6153,14 @@ export const UPDATE_FNS = { }, } }, + RUN_NORMALIZATION_STEP: (action: RunNormalizationStep, editor: EditorModel): EditorModel => { + const activePlugin = getActivePlugin(editor) + const normalizedEditor = activePlugin.normalizeFromInlineStyle( + editor, + action.elementsToNormalize, + ) + return normalizedEditor + }, } function copySelectionToClipboardMutating( @@ -6582,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/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: {}, }, } } diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index 800ae1ae55a3..245719245462 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -869,6 +869,14 @@ export function internalClipboard( } } +export interface UnsetPropertyValues { + gap?: '0px' +} + +export interface PropertiesToUnset { + [pathString: string]: UnsetPropertyValues +} + export interface EditorStateCanvas { elementsToRerender: ElementsToRerender interactionSession: InteractionSession | null @@ -890,6 +898,7 @@ export interface EditorStateCanvas { resizeOptions: ResizeOptions domWalkerAdditionalElementsToUpdate: Array controls: EditorStateCanvasControls + propertiesToUnset: PropertiesToUnset } export function editorStateCanvas( @@ -913,6 +922,7 @@ export function editorStateCanvas( resizeOpts: ResizeOptions, domWalkerAdditionalElementsToUpdate: Array, controls: EditorStateCanvasControls, + propertiesToUnset: PropertiesToUnset, ): EditorStateCanvas { return { elementsToRerender: elementsToRerender, @@ -935,6 +945,7 @@ export function editorStateCanvas( resizeOptions: resizeOpts, domWalkerAdditionalElementsToUpdate: domWalkerAdditionalElementsToUpdate, controls: controls, + propertiesToUnset: propertiesToUnset, } } @@ -2629,6 +2640,7 @@ export function createEditorState(dispatch: EditorDispatch): EditorState { parentOutlineHighlight: null, gridControlData: null, }, + propertiesToUnset: {}, }, inspector: { visible: true, @@ -3004,6 +3016,7 @@ export function editorModelFromPersistentModel( parentOutlineHighlight: null, gridControlData: null, }, + propertiesToUnset: {}, }, inspector: { visible: true, 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 } 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..abba3e5b59aa 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,15 @@ export const EditorStateCanvasControlsKeepDeepEquality: KeepDeepEqualityCall = + objectDeepEquality( + combine1EqualityCall( + (p) => p.gap, + undefinableDeepEquality(createCallWithTripleEquals()), + (gap) => ({ gap }), + ), + ) + export const ModifiersKeepDeepEquality: KeepDeepEqualityCall = combine4EqualityCalls( (modifiers) => modifiers.alt, createCallWithTripleEquals(), @@ -3262,6 +3272,11 @@ export const EditorStateCanvasKeepDeepEquality: KeepDeepEqualityCall { + switch (command.type) { + case 'DELETE_PROPERTIES': + return command.element + default: + return null + } + }, commands), + ), + ), + ]) } } } diff --git a/editor/src/core/shared/object-utils.ts b/editor/src/core/shared/object-utils.ts index fff91d92de78..0b2820c70049 100644 --- a/editor/src/core/shared/object-utils.ts +++ b/editor/src/core/shared/object-utils.ts @@ -294,3 +294,7 @@ export function objectContainsAllKeys>( ): 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)