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/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index edbf8ce5720c..b4740be5ee80 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -7,6 +7,8 @@ import { getTailwindConfigCached, isTailwindEnabled, } from '../../../core/tailwind/tailwind-compilation' +import type { PropertiesWithElementPath } from '../../editor/actions/action-utils' +import { isFeatureEnabled } from '../../../utils/feature-switches' export interface StylePlugin { name: string @@ -14,6 +16,7 @@ export interface StylePlugin { normalizeFromInlineStyle: ( editorState: EditorState, elementsToNormalize: ElementPath[], + propertiesToRemove: PropertiesWithElementPath[], ) => EditorState } @@ -23,7 +26,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.spec.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts index c4490e97d2a0..1c73ea9ef373 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts @@ -66,6 +66,7 @@ describe('tailwind style plugin', () => { const normalizedEditor = TailwindPlugin(null).normalizeFromInlineStyle( editor.getEditorState().editor, [target], + [], ) const normalizedElement = getJSXElementFromProjectContents( @@ -96,7 +97,7 @@ describe('tailwind style plugin', () => { const target = EP.fromString('sb/scene/div') const normalizedEditor = TailwindPlugin( getTailwindConfigCached(editor.getEditorState().editor), - ).normalizeFromInlineStyle(editor.getEditorState().editor, [target]) + ).normalizeFromInlineStyle(editor.getEditorState().editor, [target], []) const normalizedElement = getJSXElementFromProjectContents( target, diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 23e2e7413a9c..77bb8b9a32ef 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -1,5 +1,5 @@ import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' -import type { JSXAttributesEntry } from 'utopia-shared/src/types' +import type { JSExpression, JSXAttributesEntry, PropertyPath } from 'utopia-shared/src/types' import { isLeft } from '../../../core/shared/either' import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' import { @@ -18,6 +18,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 type { PropertiesWithElementPath } from '../../editor/actions/action-utils' function parseTailwindProperty(value: unknown, parse: Parser): WithPropertyTag | null { const parsed = parse(value, null) @@ -47,7 +48,7 @@ function stringifiedStylePropValue(value: unknown): string | null { return null } -function getTailwindClassMapping(classes: string[], config: Config | null) { +function getTailwindClassMapping(classes: string[], config: Config | null): Record { const mapping: Record = {} classes.forEach((className) => { const parsed = TailwindClassParser.parse(className, config ?? undefined) @@ -59,6 +60,55 @@ 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(properties: PropertyPath[]): UCL.ClassListUpdate[] { + return mapDropNulls((property) => { + const [maybeStyle, maybeCSSProp] = property.propertyElements + if ( + maybeStyle !== 'style' || + maybeCSSProp == null || + !isSupportedTailwindProperty(maybeCSSProp) + ) { + return null + } + return UCL.remove(TailwindPropertyMapping[maybeCSSProp]) + }, properties) +} + +function getPropertyCleanupCommands(propertiesToRemove: PropertiesWithElementPath[]) { + return mapDropNulls(({ elementPath, properties }) => { + const removeUpdates = getRemoveUpdates(properties) + if (removeUpdates.length === 0) { + return null + } + return updateClassListCommand('always', elementPath, removeUpdates) + }, propertiesToRemove) +} + export const TailwindPlugin = (config: Config | null): StylePlugin => ({ name: 'Tailwind', styleInfoFactory: @@ -82,7 +132,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ ), } }, - normalizeFromInlineStyle: (editorState, elementsToNormalize) => { + normalizeFromInlineStyle: (editorState, elementsToNormalize, propertiesToRemove) => { const commands = elementsToNormalize.flatMap((elementPath) => { const element = getJSXElementFromProjectContents(elementPath, editorState.projectContents) if (element == null) { @@ -97,19 +147,15 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ return [] } - const styleValue = styleAttribute.value - if (styleValue.type !== 'ATTRIBUTE_NESTED_OBJECT') { + const styleValue = getStylePropContents(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 }) => { @@ -134,10 +180,15 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ ), ] }) - if (commands.length === 0) { + + const commandsWithPropertyCleanup = [ + ...commands, + ...getPropertyCleanupCommands(propertiesToRemove), + ] + if (commandsWithPropertyCleanup.length === 0) { return editorState } - return foldAndApplyCommandsSimple(editorState, commands) + return foldAndApplyCommandsSimple(editorState, commandsWithPropertyCleanup) }, }) diff --git a/editor/src/components/editor/actions/action-utils.ts b/editor/src/components/editor/actions/action-utils.ts index a695b30ad16a..5f00da49b4c8 100644 --- a/editor/src/components/editor/actions/action-utils.ts +++ b/editor/src/components/editor/actions/action-utils.ts @@ -1,4 +1,6 @@ -import { safeIndex } from '../../../core/shared/array-utils' +import type { ElementPath, PropertyPath } from 'utopia-shared/src/types' +import { mapDropNulls, safeIndex } from '../../../core/shared/array-utils' +import type { CanvasCommand } from '../../canvas/commands/commands' import type { EditorAction } from '../action-types' import { isFromVSCodeAction } from './actions-from-vscode' @@ -362,3 +364,74 @@ export function simpleStringifyActions( .map((a) => simpleStringifyAction(a, indentation)) .join(`,\n${spacing}`)}\n${spacingBeforeClose}]` } + +export function getElementsToNormalizeFromCommands(commands: CanvasCommand[]): ElementPath[] { + return mapDropNulls((command) => { + switch (command.type) { + case 'ADJUST_CSS_LENGTH_PROPERTY': + case 'SET_CSS_LENGTH_PROPERTY': + case 'ADJUST_NUMBER_PROPERTY': + case 'CONVERT_CSS_PERCENT_TO_PX': + case 'CONVERT_TO_ABSOLUTE': + return command.target + case 'ADD_CONTAIN_LAYOUT_IF_NEEDED': + case 'SET_PROPERTY': + case 'UPDATE_BULK_PROPERTIES': + return command.element + default: + return null + } + }, commands) +} + +export function getElementsToNormalizeFromActions(actions: EditorAction[]): ElementPath[] { + return actions.flatMap((action) => { + switch (action.action) { + case 'APPLY_COMMANDS': + return getElementsToNormalizeFromCommands(action.commands) + // TODO: extends this switch when we add support to non-canvas + // command-based edits + default: + return [] + } + }) +} + +export interface PropertiesWithElementPath { + elementPath: ElementPath + properties: PropertyPath[] +} + +export function getPropertiesToRemoveFromCommands( + commands: CanvasCommand[], +): PropertiesWithElementPath[] { + return mapDropNulls((command) => { + switch (command.type) { + case 'DELETE_PROPERTIES': + return { elementPath: command.element, properties: command.properties } + case 'UPDATE_BULK_PROPERTIES': + return { + elementPath: command.element, + properties: mapDropNulls( + (p) => (p.type === 'DELETE' ? p.path : null), + command.properties, + ), + } + default: + return null + } + }, commands) +} + +export function getPropertiesToRemoveFromActions( + actions: EditorAction[], +): PropertiesWithElementPath[] { + return actions.flatMap((action) => { + switch (action.action) { + case 'APPLY_COMMANDS': + return getPropertiesToRemoveFromCommands(action.commands) + default: + return [] + } + }) +} diff --git a/editor/src/components/editor/store/dispatch-strategies.tsx b/editor/src/components/editor/store/dispatch-strategies.tsx index 0180914754a7..1f153bc0c37c 100644 --- a/editor/src/components/editor/store/dispatch-strategies.tsx +++ b/editor/src/components/editor/store/dispatch-strategies.tsx @@ -30,7 +30,9 @@ import type { StartPostActionSession, } from '../action-types' import { SelectComponents } from '../action-types' +import type { PropertiesWithElementPath } from '../actions/action-utils' import { + getPropertiesToRemoveFromCommands, isClearInteractionSession, isCreateOrUpdateInteractionSession, isTransientAction, @@ -57,12 +59,40 @@ 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' +import type { ElementPath } from 'utopia-shared/src/types' + +export interface PropertiesToPatch { + type: 'properties-to-patch' + propertiesToPatch: PropertiesWithElementPath[] +} + +export const propertiesToPatch = (props: PropertiesWithElementPath[]): PropertiesToPatch => ({ + type: 'properties-to-patch', + propertiesToPatch: props, +}) + +export interface NormalizationData { + type: 'normalization-data' + elementsToNormalize: ElementPath[] + propertiesToRemove: PropertiesWithElementPath[] +} + +export const normalizationData = ( + elementsToNormalize: ElementPath[], + propsToRemove: PropertiesWithElementPath[], +): NormalizationData => ({ + type: 'normalization-data', + elementsToNormalize: elementsToNormalize, + propertiesToRemove: propsToRemove, +}) + +export type PostProcessingData = PropertiesToPatch | NormalizationData interface HandleStrategiesResult { unpatchedEditorState: EditorState patchedEditorState: EditorState newStrategyState: StrategyState + postProcessingData: PostProcessingData | null } export function interactionFinished( @@ -113,14 +143,9 @@ export function interactionFinished( 'end-interaction', ) - const normalizedEditor = getActivePlugin(editorState).normalizeFromInlineStyle( - editorState, - strategyResult.elementsToRerender, - ) - const finalEditor: EditorState = applyElementsToRerenderFromStrategyResult( { - ...normalizedEditor, + ...editorState, // TODO instead of clearing the metadata, we should save the latest valid metadata here to save a dom-walker run jsxMetadata: {}, domMetadata: {}, @@ -133,6 +158,10 @@ export function interactionFinished( unpatchedEditorState: finalEditor, patchedEditorState: finalEditor, newStrategyState: withClearedSession, + postProcessingData: normalizationData( + strategyResult.elementsToRerender, + getPropertiesToRemoveFromCommands(strategyResult.commands), + ), } } else { // Try to keep any updated metadata that may have been populated into here @@ -148,6 +177,7 @@ export function interactionFinished( unpatchedEditorState: newEditorState, patchedEditorState: newEditorState, newStrategyState: withClearedSession, + postProcessingData: null, } } } @@ -176,6 +206,7 @@ export function interactionHardReset( unpatchedEditorState: newEditorState, patchedEditorState: newEditorState, newStrategyState: withClearedSession, + postProcessingData: null, } } else { const resetInteractionSession = interactionSessionHardReset(interactionSession) @@ -233,12 +264,14 @@ export function interactionHardReset( strategyResult, ), newStrategyState: newStrategyState, + postProcessingData: null, } } else { return { unpatchedEditorState: newEditorState, patchedEditorState: newEditorState, newStrategyState: withClearedSession, + postProcessingData: null, } } } @@ -266,6 +299,7 @@ export function interactionUpdate( unpatchedEditorState: newEditorState, patchedEditorState: newEditorState, newStrategyState: result.strategyState, + postProcessingData: null, } } else { // Determine the new canvas strategy to run this time around. @@ -348,6 +382,7 @@ export function interactionStart( unpatchedEditorState: newEditorState, patchedEditorState: newEditorState, newStrategyState: withClearedSession, + postProcessingData: null, } } else { // Determine the new canvas strategy to run this time around. @@ -401,12 +436,14 @@ export function interactionStart( strategyResult, ), newStrategyState: newStrategyState, + postProcessingData: null, } } else { return { unpatchedEditorState: newEditorState, patchedEditorState: newEditorState, newStrategyState: withClearedSession, + postProcessingData: null, } } } @@ -434,6 +471,7 @@ export function interactionCancel( unpatchedEditorState: updatedEditorState, patchedEditorState: updatedEditorState, newStrategyState: createEmptyStrategyState({}, {}, {}), + postProcessingData: null, } } @@ -508,12 +546,14 @@ function handleUserChangedStrategy( strategyResult, ), newStrategyState: newStrategyState, + postProcessingData: null, } } else { return { unpatchedEditorState: newEditorState, patchedEditorState: newEditorState, newStrategyState: strategyState, + postProcessingData: null, } } } @@ -594,6 +634,7 @@ function handleAccumulatingKeypresses( strategyResult, ), newStrategyState: newStrategyState, + postProcessingData: null, } } } @@ -601,6 +642,7 @@ function handleAccumulatingKeypresses( unpatchedEditorState: newEditorState, patchedEditorState: newEditorState, newStrategyState: strategyState, + postProcessingData: null, } } @@ -661,12 +703,16 @@ function handleUpdate( strategyResult, ), newStrategyState: newStrategyState, + postProcessingData: propertiesToPatch( + getPropertiesToRemoveFromCommands(strategyResult.commands), + ), } } else { return { unpatchedEditorState: newEditorState, patchedEditorState: newEditorState, newStrategyState: strategyState, + postProcessingData: null, } } } @@ -686,6 +732,7 @@ export function handleStrategies( let unpatchedEditorState: EditorState let patchedEditorState: EditorState let newStrategyState: StrategyState + let postProcessingData: PostProcessingData | null if (allowedToEditProject(storedState.userState.loginState, storedState.projectServerState)) { const strategiesResult = handleStrategiesInner( strategies, @@ -696,10 +743,12 @@ export function handleStrategies( unpatchedEditorState = strategiesResult.unpatchedEditorState patchedEditorState = strategiesResult.patchedEditorState newStrategyState = strategiesResult.newStrategyState + postProcessingData = strategiesResult.postProcessingData } else { unpatchedEditorState = result.unpatchedEditor patchedEditorState = result.unpatchedEditor newStrategyState = result.strategyState + postProcessingData = null } const patchedEditorWithMetadata: EditorState = { @@ -739,8 +788,9 @@ export function handleStrategies( return { unpatchedEditorState: unpatchedEditorState, patchedEditorState: patchedEditorWithMetadata, - patchedDerivedState, + patchedDerivedState: patchedDerivedState, newStrategyState: newStrategyState, + postProcessingData: postProcessingData, } } @@ -815,6 +865,7 @@ function handleStrategiesInner( unpatchedEditorState: result.unpatchedEditor, // we return the fresh unpatchedEditor, containing the up-to-date domMetadata and spyMetadata patchedEditorState: oldPatchedEditorWithNewMetadata, // the previous patched editor with updated metadata newStrategyState: storedState.strategyState, + postProcessingData: null, } } else if (storedState.unpatchedEditor.canvas.interactionSession == null) { if (result.unpatchedEditor.canvas.interactionSession == null) { @@ -822,6 +873,7 @@ function handleStrategiesInner( unpatchedEditorState: result.unpatchedEditor, patchedEditorState: result.unpatchedEditor, newStrategyState: result.strategyState, + postProcessingData: null, } } else { return interactionStart(strategies, storedState, result) diff --git a/editor/src/components/editor/store/dispatch.tsx b/editor/src/components/editor/store/dispatch.tsx index e8a522d2d7f1..f014fcbd20d6 100644 --- a/editor/src/components/editor/store/dispatch.tsx +++ b/editor/src/components/editor/store/dispatch.tsx @@ -7,6 +7,7 @@ import type { CanvasAction } from '../../canvas/canvas-types' import type { LocalNavigatorAction } from '../../navigator/actions' import type { EditorAction, EditorDispatch, UpdateMetadataInEditorState } from '../action-types' import { isLoggedIn } from '../action-types' +import type { PropertiesWithElementPath } from '../actions/action-utils' import { isTransientAction, isUndoOrRedo, @@ -14,6 +15,8 @@ import { checkAnyWorkerUpdates, onlyActionIsWorkerParsedUpdate, simpleStringifyActions, + getElementsToNormalizeFromActions, + getPropertiesToRemoveFromActions, } from '../actions/action-utils' import * as EditorActions from '../actions/action-creators' import * as History from '../history' @@ -27,6 +30,7 @@ import type { } from './editor-state' import { deriveState, + getJSXElementFromProjectContents, persistentModelFromEditorModel, storedEditorStateFromEditorState, } from './editor-state' @@ -40,7 +44,7 @@ import { runResetOnlineState, runUpdateProjectServerState, } from './editor-update' -import { isBrowserEnvironment } from '../../../core/shared/utils' +import { assertNever, isBrowserEnvironment } from '../../../core/shared/utils' import type { UiJsxCanvasContextData } from '../../canvas/ui-jsx-canvas' import type { ProjectContentTreeRoot } from '../../assets' import { treeToContents } from '../../assets' @@ -58,7 +62,8 @@ import { getProjectChanges, sendVSCodeChanges, } from './vscode-changes' -import { handleStrategies, updatePostActionState } from './dispatch-strategies' +import type { NormalizationData, PostProcessingData } from './dispatch-strategies' +import { handleStrategies, normalizationData, updatePostActionState } from './dispatch-strategies' import type { MetaCanvasStrategy } from '../../canvas/canvas-strategies/canvas-strategies' import { RegisteredCanvasStrategies } from '../../canvas/canvas-strategies/canvas-strategies' @@ -75,18 +80,8 @@ import { maybeClearPseudoInsertMode } from '../canvas-toolbar-states' import { isSteganographyEnabled } from '../../../core/shared/stegano-text' import { updateCollaborativeProjectContents } from './collaborative-editing' import { ensureSceneIdsExist } from '../../../core/model/scene-id-utils' -import type { ModuleEvaluator } from '../../../core/property-controls/property-controls-local' -import { - createModuleEvaluator, - isComponentDescriptorFile, -} from '../../../core/property-controls/property-controls-local' -import { - hasReactRouterErrorBeenLogged, - setReactRouterErrorHasBeenLogged, -} from '../../../core/shared/runtime-report-logs' -import type { PropertyControlsInfo } from '../../custom-code/code-file' +import { isComponentDescriptorFile } from '../../../core/property-controls/property-controls-local' import { getFilePathMappings } from '../../../core/model/project-file-utils' -import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template' import { getParserChunkCount, getParserWorkerCount, @@ -97,6 +92,10 @@ import { startPerformanceMeasure, } from '../../../core/performance/performance-utils' import { getParseCacheOptions } from '../../../core/shared/parse-cache-utils' +import { getActivePlugin } from '../../canvas/plugins/style-plugins' +import { mapDropNulls } from '../../../core/shared/array-utils' +import { propertyToSet, updateBulkProperties } from '../../canvas/commands/set-property-command' +import { foldAndApplyCommandsSimple } from '../../canvas/commands/commands' type DispatchResultFields = { nothingChanged: boolean @@ -1003,18 +1002,56 @@ function editorDispatchInner( ) } - const { unpatchedEditorState, patchedEditorState, newStrategyState, patchedDerivedState } = - handleStrategies( - strategiesToUse, - dispatchedActions, - storedState, - result, - storedState.patchedDerived, - ) + const { + unpatchedEditorState: unpatchedEditorStateFromStrategies, + patchedEditorState: patchedEditorStateFromStrategies, + newStrategyState, + patchedDerivedState, + postProcessingData: postProcessingDataFromStrategies, + } = handleStrategies( + strategiesToUse, + dispatchedActions, + storedState, + result, + storedState.patchedDerived, + ) + + const elementsToNormalizeFromActions = getElementsToNormalizeFromActions(dispatchedActions) + const propertiesToRemoveFromActions = getPropertiesToRemoveFromActions(dispatchedActions) + + const elementsToNormalize = [ + ...elementsToNormalizeFromActions, + ...(postProcessingDataFromStrategies?.type === 'normalization-data' + ? postProcessingDataFromStrategies.elementsToNormalize + : []), + ] + + const propertiesToRemove = [ + ...propertiesToRemoveFromActions, + ...(postProcessingDataFromStrategies?.type === 'normalization-data' + ? postProcessingDataFromStrategies.propertiesToRemove + : []), + ] + + const unpatchedEditor = runRemovedPropertyPatchingAndNormalization( + unpatchedEditorStateFromStrategies, + normalizationData(elementsToNormalize, propertiesToRemove), + ) + + const patchedEditor = + postProcessingDataFromStrategies === null + ? patchedEditorStateFromStrategies + : runRemovedPropertyPatchingAndNormalization( + patchedEditorStateFromStrategies, + addNormalizationDataFromActions( + postProcessingDataFromStrategies, + normalizationData(elementsToNormalizeFromActions, propertiesToRemoveFromActions), + ), + ) return { - unpatchedEditor: unpatchedEditorState, - patchedEditor: patchedEditorState, + unpatchedEditor: unpatchedEditor, + patchedEditor: patchedEditor, unpatchedDerived: frozenDerivedState, patchedDerived: patchedDerivedState, strategyState: newStrategyState, @@ -1072,3 +1109,78 @@ function resetUnpatchedEditorTransientFields(editor: EditorState): EditorState { }, } } + +function addNormalizationDataFromActions( + data: PostProcessingData, + fromActions: NormalizationData, +): PostProcessingData { + if (data.type === 'properties-to-patch') { + return data + } + + return normalizationData( + [...data.elementsToNormalize, ...fromActions.elementsToNormalize], + [...data.propertiesToRemove, ...fromActions.propertiesToRemove], + ) +} + +const PropertyDefaultValues: Record = { + gap: '0px', +} + +function patchRemovedProperties( + editor: EditorState, + propertiesToPatch: PropertiesWithElementPath[], +): EditorState { + const propertiesToSetCommands = mapDropNulls( + ({ elementPath, properties }) => + getJSXElementFromProjectContents(elementPath, editor.projectContents) == null + ? null + : updateBulkProperties( + 'always', + elementPath, + mapDropNulls((property) => { + const [maybeStyle, maybeCSSProp] = property.propertyElements + if (maybeStyle !== 'style' || maybeCSSProp == null) { + return null + } + + const defaultValue = PropertyDefaultValues[maybeCSSProp] + if (defaultValue == null) { + return null + } + return propertyToSet(property, defaultValue) + }, properties), + ), + propertiesToPatch, + ) + return foldAndApplyCommandsSimple(editor, propertiesToSetCommands) +} + +function runRemovedPropertyPatchingAndNormalization( + editorState: EditorState, + postProcessingData: PostProcessingData, +): EditorState { + if (postProcessingData.type === 'properties-to-patch') { + return postProcessingData.propertiesToPatch.length === 0 + ? editorState + : patchRemovedProperties(editorState, postProcessingData.propertiesToPatch) + } + + if (postProcessingData.type === 'normalization-data') { + if ( + postProcessingData.elementsToNormalize.length === 0 && + postProcessingData.propertiesToRemove.length === 0 + ) { + return editorState + } + + return getActivePlugin(editorState).normalizeFromInlineStyle( + editorState, + postProcessingData.elementsToNormalize, + postProcessingData.propertiesToRemove, + ) + } + + assertNever(postProcessingData) +}