From e053c5bf41c6e95339639ff6ed4f6b450cd56d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertalan=20K=C3=B6rmendy?= Date: Thu, 17 Oct 2024 11:31:07 +0200 Subject: [PATCH] Move normalization step into the dispatch flow (#6549) https://github.com/user-attachments/assets/d184b155-e18a-42d6-ba84-1532f6ebd280 ## Problem The tailwind style plugin can't differentiate between a prop being removed from the inline style prop, and that prop never having been there at all. For example, when a flex gap is removed by dragging it in the negative direction, it goes from positive -> 0 -> being removed from the style prop. When the normalisation step kicks in, there's no gap prop in the inline style, so the tailwind plugin doesn't do anything. To make matters worse, when the gap property is removed while the interaction is in progress, the value coming from the generated tailwind class takes precedence, so the flex gap jumps back to its original size. ## Fix The problem of removing props in the normalization step is fixed by 1. moving the normalization step in the display flow 2. making the normalization step aware that some props need to be removed 3. find out which elements to normalize/which props to remove from the dispatched actions and the strategies, and running the normalization on those elements and prop. The problem of prevent the CSS classes to kick in when a prop is removed (for example, when the inline `gap` prop is removed in the flex gap strategy), this PR adds a postprocessing step alongside the normalization step, that patches an allowlisted set of props to a "zero" value, which simulates the prop not being there at all ### Details - added a test for removing the gap prop set from Tailwind - added a `propertiesToRemove` param to `normalizeFromInlineStyle`, and updated the tailwind style plugin to use that prop to remove classes from the `className` prop - added the code that finds out which elements/props to normalize from the dispatched actions - added the code that finds out which elements/props to normalize from the strategies, and that tells which elements to patch during an interaction **Manual Tests:** I hereby swear that: - [x] I opened a hydrogen project and it loaded - [x] I could navigate to various routes in Play mode Fixes #[ticket_number] (<< pls delete this line if it's not relevant) --- .../set-flex-gap-strategy.spec.browser2.tsx | 8 + .../canvas/plugins/style-plugins.ts | 5 +- .../plugins/tailwind-style-plugin.spec.ts | 3 +- .../canvas/plugins/tailwind-style-plugin.ts | 77 +++++++-- .../components/editor/actions/action-utils.ts | 75 ++++++++- .../editor/store/dispatch-strategies.tsx | 68 +++++++- .../src/components/editor/store/dispatch.tsx | 158 +++++++++++++++--- 7 files changed, 347 insertions(+), 47 deletions(-) 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) +}