From 281b59508767ba241f373738165a58ec0ea0dfb8 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Mon, 27 May 2024 18:02:45 +0200 Subject: [PATCH] data-can-condense initial support (#5757) --- editor/src/components/context-menu-items.ts | 11 ++++ editor/src/components/editor/action-types.ts | 6 +++ .../editor/actions/action-creators.ts | 8 +++ .../components/editor/actions/action-utils.ts | 1 + .../src/components/editor/actions/actions.tsx | 29 ++++++++++ .../components/editor/store/editor-update.tsx | 2 + .../src/components/element-context-menu.tsx | 2 + .../components/navigator/navigator-utils.ts | 23 ++++++-- editor/src/utils/can-condense.ts | 53 +++++++++++++++++++ 9 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 editor/src/utils/can-condense.ts diff --git a/editor/src/components/context-menu-items.ts b/editor/src/components/context-menu-items.ts index 5f3fc2301866..4213453c76ab 100644 --- a/editor/src/components/context-menu-items.ts +++ b/editor/src/components/context-menu-items.ts @@ -28,6 +28,7 @@ import * as EditorActions from './editor/actions/action-creators' import { copySelectionToClipboard, duplicateSelected, + toggleDataCanCondense, toggleHidden, } from './editor/actions/action-creators' import { @@ -329,6 +330,16 @@ export const toggleVisibility: ContextMenuItem = { }, } +export const toggleCanCondense: ContextMenuItem = { + name: 'Toggle Can Condense', + enabled: (data) => { + return data.selectedViews.length > 0 + }, + action: (data, dispatch?: EditorDispatch) => { + requireDispatch(dispatch)([toggleDataCanCondense(data.selectedViews)], 'everyone') + }, +} + export const lineSeparator: ContextMenuItem = { name: RU.create('div', { key: 'separator', className: 'react-contexify__separator' }, ''), enabled: false, diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index 8e52422af829..950e6e1eaf53 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -227,6 +227,11 @@ export type ToggleHidden = { targets: Array } +export type ToggleDataCanCondense = { + action: 'TOGGLE_DATA_CAN_CONDENSE' + targets: Array +} + export type UnsetProperty = { action: 'UNSET_PROPERTY' element: ElementPath @@ -1217,6 +1222,7 @@ export type EditorAction = | Undo | Redo | ToggleHidden + | ToggleDataCanCondense | RenameComponent | SetPanelVisibility | ToggleFocusedOmniboxTab diff --git a/editor/src/components/editor/actions/action-creators.ts b/editor/src/components/editor/actions/action-creators.ts index 8fab22291e72..c05c366c6dc3 100644 --- a/editor/src/components/editor/actions/action-creators.ts +++ b/editor/src/components/editor/actions/action-creators.ts @@ -238,6 +238,7 @@ import type { ReplaceElementInScope, ElementReplacementPath, ReplaceJSXElement, + ToggleDataCanCondense, } from '../action-types' import type { InsertionSubjectWrapper, Mode } from '../editor-modes' import { EditorModes, insertionSubject } from '../editor-modes' @@ -365,6 +366,13 @@ export function toggleHidden(targets: Array = []): ToggleHidden { } } +export function toggleDataCanCondense(targets: Array): ToggleDataCanCondense { + return { + action: 'TOGGLE_DATA_CAN_CONDENSE', + targets: targets, + } +} + export function transientActions( actions: Array, elementsToRerender: Array | null = null, diff --git a/editor/src/components/editor/actions/action-utils.ts b/editor/src/components/editor/actions/action-utils.ts index 6aba556d2798..7f69bc4eacac 100644 --- a/editor/src/components/editor/actions/action-utils.ts +++ b/editor/src/components/editor/actions/action-utils.ts @@ -172,6 +172,7 @@ export function isTransientAction(action: EditorAction): boolean { case 'ALIGN_SELECTED_VIEWS': case 'DISTRIBUTE_SELECTED_VIEWS': case 'TOGGLE_HIDDEN': + case 'TOGGLE_DATA_CAN_CONDENSE': case 'UPDATE_FILE_PATH': case 'UPDATE_REMIX_ROUTE': case 'ADD_FOLDER': diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index d84c9a73b9d4..86e290e1e209 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -347,6 +347,7 @@ import type { ReplaceMappedElement, ReplaceElementInScope, ReplaceJSXElement, + ToggleDataCanCondense, } from '../action-types' import { isLoggedIn } from '../action-types' import type { Mode } from '../editor-modes' @@ -610,6 +611,11 @@ import { fixTopLevelElementsUIDs } from '../../../core/workers/parser-printer/ui import { nextSelectedTab } from '../../navigator/left-pane/left-pane-utils' import { getRemixRootDir } from '../store/remix-derived-data' import { isReplaceKeepChildrenAndStyleTarget } from '../../navigator/navigator-item/component-picker-context-menu' +import { + canCondenseJSXElementChild, + dataCanCondenseProp, + isDataCanCondenseProp, +} from '../../../utils/can-condense' export const MIN_CODE_PANE_REOPEN_WIDTH = 100 @@ -2242,6 +2248,29 @@ export const UPDATE_FNS = { } }, editor) }, + TOGGLE_DATA_CAN_CONDENSE: (action: ToggleDataCanCondense, editor: EditorModel): EditorModel => { + let working = { ...editor } + for (const path of action.targets) { + working = modifyOpenJsxElementAtPath( + path, + (element) => { + const canCondense = canCondenseJSXElementChild(element) + // remove any data-can-condense props + const props = element.props.filter((prop) => !isDataCanCondenseProp(prop)) + // if it needs to switch to true, append the new prop + if (!canCondense) { + props.push(dataCanCondenseProp(true)) + } + return { + ...element, + props: props, + } + }, + working, + ) + } + return working + }, RENAME_COMPONENT: (action: RenameComponent, editor: EditorModel): EditorModel => { const { name } = action const target = action.target diff --git a/editor/src/components/editor/store/editor-update.tsx b/editor/src/components/editor/store/editor-update.tsx index 9295548fe965..b6fa587a1038 100644 --- a/editor/src/components/editor/store/editor-update.tsx +++ b/editor/src/components/editor/store/editor-update.tsx @@ -168,6 +168,8 @@ export function runSimpleLocalEditorAction( return UPDATE_FNS.SWITCH_EDITOR_MODE(action, state, userState) case 'TOGGLE_HIDDEN': return UPDATE_FNS.TOGGLE_HIDDEN(action, state) + case 'TOGGLE_DATA_CAN_CONDENSE': + return UPDATE_FNS.TOGGLE_DATA_CAN_CONDENSE(action, state) case 'RENAME_COMPONENT': return UPDATE_FNS.RENAME_COMPONENT(action, state) case 'INSERT_JSX_ELEMENT': diff --git a/editor/src/components/element-context-menu.tsx b/editor/src/components/element-context-menu.tsx index 7bd1349d8992..a8c6bc3d71d0 100644 --- a/editor/src/components/element-context-menu.tsx +++ b/editor/src/components/element-context-menu.tsx @@ -30,6 +30,7 @@ import { pasteToReplace, pasteHere, replace, + toggleCanCondense, } from './context-menu-items' import { ContextMenu } from './context-menu-wrapper' import { useRefEditorState, useEditorState, Substores } from './editor/store/store-hook' @@ -90,6 +91,7 @@ const ElementContextMenuItems: Array> = [ sendToBack, lineSeparator, toggleVisibility, + toggleCanCondense, lineSeparator, toggleBackgroundLayersItem, toggleBorderItem, diff --git a/editor/src/components/navigator/navigator-utils.ts b/editor/src/components/navigator/navigator-utils.ts index ac13ad9fdbde..10a6acb34952 100644 --- a/editor/src/components/navigator/navigator-utils.ts +++ b/editor/src/components/navigator/navigator-utils.ts @@ -71,6 +71,7 @@ import { getUtopiaID } from '../../core/shared/uid-utils' import { create } from 'tar' import { emptySet } from '../../core/shared/set-utils' import { objectMap } from '../../core/shared/object-utils' +import { dataCanCondenseFromMetadata } from '../../utils/can-condense' export function baseNavigatorDepth(path: ElementPath): number { // The storyboard means that this starts at -1, @@ -581,7 +582,10 @@ function isCondensableLeafEntry(entry: NavigatorTree): boolean { ) } -function condenseNavigatorTree(navigatorTree: Array): Array { +function condenseNavigatorTree( + metadata: ElementInstanceMetadataMap, + navigatorTree: Array, +): Array { if (!isFeatureEnabled('Condensed Navigator Entries')) { return navigatorTree } @@ -601,7 +605,11 @@ function condenseNavigatorTree(navigatorTree: Array): Array, filterVisible: 'all-navigator-targets' | 'visible-navigator-targets', ): Array { - const condensedTree = condenseNavigatorTree(navigatorTree) + const condensedTree = condenseNavigatorTree(metadata, navigatorTree) function walkTree(entry: NavigatorTree, indentation: number): Array { function walkIfSubtreeVisible(e: NavigatorTree, i: number): Array { @@ -810,10 +819,14 @@ export function getNavigatorTargets( projectContents, ) - const navigatorRows = getNavigatorRowsForTree(navigatorTrees, 'all-navigator-targets') + const navigatorRows = getNavigatorRowsForTree(metadata, navigatorTrees, 'all-navigator-targets') const navigatorTargets = navigatorRows.flatMap(getEntriesForRow) - const visibleNavigatorRows = getNavigatorRowsForTree(navigatorTrees, 'visible-navigator-targets') + const visibleNavigatorRows = getNavigatorRowsForTree( + metadata, + navigatorTrees, + 'visible-navigator-targets', + ) const filteredVisibleNavigatorRows = visibleNavigatorRows const visibleNavigatorTargets = filteredVisibleNavigatorRows.flatMap(getEntriesForRow) diff --git a/editor/src/utils/can-condense.ts b/editor/src/utils/can-condense.ts new file mode 100644 index 000000000000..83578cf6290c --- /dev/null +++ b/editor/src/utils/can-condense.ts @@ -0,0 +1,53 @@ +import { emptyComments, type ElementPath } from 'utopia-shared/src/types' +import { MetadataUtils } from '../core/model/element-metadata-utils' +import { isRight } from '../core/shared/either' +import type { + JSXAttributesEntry, + JSXAttributesPart, + JSXElementChild, +} from '../core/shared/element-template' +import { + isJSXAttributeValue, + isJSXAttributesEntry, + isJSXElement, + jsExpressionValue, + jsxAttributesEntry, + type ElementInstanceMetadataMap, +} from '../core/shared/element-template' + +export const DataCanCondense = 'data-can-condense' + +export function dataCanCondenseFromMetadata( + metadata: ElementInstanceMetadataMap, + path: ElementPath, +): boolean { + const target = MetadataUtils.findElementByElementPath(metadata, path) + return ( + target != null && + isRight(target.element) && + isJSXElement(target.element.value) && + canCondenseJSXElementChild(target.element.value) + ) +} + +export function canCondenseJSXElementChild(element: JSXElementChild) { + return ( + isJSXElement(element) && + element.props.some( + (prop) => + isDataCanCondenseProp(prop) && isJSXAttributeValue(prop.value) && prop.value.value === true, + ) + ) +} + +interface DataCanCondenseProp extends JSXAttributesEntry { + key: typeof DataCanCondense +} + +export function isDataCanCondenseProp(prop: JSXAttributesPart): prop is DataCanCondenseProp { + return isJSXAttributesEntry(prop) && (prop as DataCanCondenseProp).key === 'data-can-condense' +} + +export function dataCanCondenseProp(value: boolean): JSXAttributesEntry { + return jsxAttributesEntry(DataCanCondense, jsExpressionValue(value, emptyComments), emptyComments) +}