diff --git a/editor/src/components/navigator/navigator-item/layout-icon.tsx b/editor/src/components/navigator/navigator-item/layout-icon.tsx index 568d40b34531..59af6e1c3487 100644 --- a/editor/src/components/navigator/navigator-item/layout-icon.tsx +++ b/editor/src/components/navigator/navigator-item/layout-icon.tsx @@ -21,6 +21,7 @@ import type { MetadataSubstate } from '../../editor/store/store-hook-substore-ty import * as EP from '../../../core/shared/element-path' import { Substores, useEditorState } from '../../editor/store/store-hook' import type { Icon } from 'utopia-api' +import { when } from '../../../utils/react-conditionals' interface LayoutIconProps { navigatorEntry: NavigatorEntry @@ -265,19 +266,22 @@ export const LayoutIcon: React.FunctionComponent -
- {marker} -
+ {when( + marker != null, +
+ {marker} +
, + )} {icon} ) diff --git a/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx b/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx index d8ffb9ef1449..4c1fbb022a96 100644 --- a/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx +++ b/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx @@ -1,9 +1,9 @@ /** @jsxRuntime classic */ /** @jsx jsx */ import { jsx } from '@emotion/react' -import React from 'react' -import { assertNever } from '../../../core/shared/utils' import { createCachedSelector } from 're-reselect' +import React from 'react' +import { maybeConditionalExpression } from '../../../core/model/conditionals' import { MetadataUtils } from '../../../core/model/element-metadata-utils' import * as EP from '../../../core/shared/element-path' import type { @@ -15,6 +15,14 @@ import { isNullJSXAttributeValue, } from '../../../core/shared/element-template' import type { ElementPath } from '../../../core/shared/project-file-types' +import { assertNever } from '../../../core/shared/utils' +import { Icons, Tooltip, useColorTheme } from '../../../uuiui' +import { getRouteComponentNameForOutlet } from '../../canvas/remix/remix-utils' +import { + selectComponents, + setHighlightedViews, + toggleCollapse, +} from '../../editor/actions/action-creators' import { useDispatch } from '../../editor/store/dispatch-context' import type { DropTargetHint, @@ -34,6 +42,11 @@ import type { ProjectContentAndMetadataSubstate, PropertyControlsInfoSubstate, } from '../../editor/store/store-hook-substore-types' +import type { CondensedNavigatorRow, CondensedNavigatorRowVariant } from '../navigator-row' +import { isRegulaNavigatorRow, type NavigatorRow } from '../navigator-row' +import { navigatorDepth } from '../navigator-utils' +import { LayoutIcon } from './layout-icon' +import { BasePaddingUnit, elementWarningsSelector } from './navigator-item' import type { ConditionalClauseNavigatorItemContainerProps, ErrorNavigatorItemContainerProps, @@ -52,11 +65,8 @@ import { SlotNavigatorItemContainer, SyntheticNavigatorItemContainer, } from './navigator-item-dnd-container' -import { navigatorDepth } from '../navigator-utils' -import { maybeConditionalExpression } from '../../../core/model/conditionals' -import { getRouteComponentNameForOutlet } from '../../canvas/remix/remix-utils' -import { CondensedNavigatorRow, isRegulaNavigatorRow, type NavigatorRow } from '../navigator-row' -import { BasePaddingUnit } from './navigator-item' +import { ExpandableIndicator } from './expandable-indicator' +import { when } from '../../../utils/react-conditionals' interface NavigatorItemWrapperProps { index: number @@ -237,32 +247,278 @@ export const NavigatorItemWrapper: React.FunctionComponent ) } + return ( + + ) + }, +) + +const CondensedEntryItemWrapper = React.memo( + (props: { windowStyle: React.CSSProperties; navigatorRow: CondensedNavigatorRow }) => { + const colorTheme = useColorTheme() + + const selectedViews = useEditorState( + Substores.selectedViews, + (store) => store.editor.selectedViews, + 'CondensedEntryItemWrapper selectedViews', + ) + + const hasSelection = React.useMemo(() => { + return selectedViews.some((path) => + props.navigatorRow.entries.some((entry) => EP.pathsEqual(path, entry.elementPath)), + ) + }, [selectedViews, props.navigatorRow]) + return (
- {props.navigatorRow.variant === 'trunk' ? ( - - {props.navigatorRow.entries.map((entry) => ( - {EP.toUid(entry.elementPath)} / - ))} - - ) : ( - - [ - {props.navigatorRow.entries.map((entry) => ( - {EP.toUid(entry.elementPath)}, - ))} - ] - - )} + {props.navigatorRow.entries.map((entry, idx) => { + const showSeparator = idx < props.navigatorRow.entries.length - 1 + const separator = showSeparator ? ( + + ) : null + + return ( + + ) + })}
) }, ) +CondensedEntryItemWrapper.displayName = 'CondensedEntryItemWrapper' + +const CondensedEntryItemSeparator = React.memo( + (props: { variant: CondensedNavigatorRowVariant }) => { + const colorTheme = useColorTheme() + return ( +
+ {props.variant === 'leaf' ? , : } +
+ ) + }, +) +CondensedEntryItemSeparator.displayName = 'CondensedEntryItemSeparator' + +const CondensedEntryItem = React.memo( + (props: { + entry: NavigatorEntry + separator: React.ReactNode + showExpandableIndicator: boolean + }) => { + const colorTheme = useColorTheme() + const dispatch = useDispatch() + + const selectedViews = useEditorState( + Substores.selectedViews, + (store) => store.editor.selectedViews, + 'CondensedEntry selectedViews', + ) + + const highlightedViews = useEditorState( + Substores.highlightedHoveredViews, + (store) => store.editor.highlightedViews, + 'CondensedEntry highlightedViews', + ) + + const iconOverride = useEditorState( + Substores.propertyControlsInfo, + (store) => + MetadataUtils.getIconOfComponent( + props.entry.elementPath, + store.editor.propertyControlsInfo, + store.editor.projectContents, + ), + 'CondensedEntry iconOverride', + ) + + const labelForTheElement = useEditorState( + Substores.projectContentsAndMetadata, + (store) => labelSelector(store, props.entry), + 'CondensedEntry labelSelector', + ) + + const elementWarnings = useEditorState( + Substores.derived, + (store) => elementWarningsSelector(store, props.entry), + 'CondensedEntry elementWarningsSelector', + ) + + const isCollapsed = useEditorState( + Substores.navigator, + (store) => + store.editor.navigator.collapsedViews.some((path) => + EP.pathsEqual(path, props.entry.elementPath), + ), + 'CondensedEntryItemWrapper isCollapsed', + ) + + const showLabel = useEditorState( + Substores.metadata, + (store) => { + return ( + MetadataUtils.isProbablyScene(store.editor.jsxMetadata, props.entry.elementPath) || + MetadataUtils.isProbablyRemixScene(store.editor.jsxMetadata, props.entry.elementPath) + ) + }, + 'CondensedEntryItemWrapper isScene', + ) + + const isChildOfSelected = React.useMemo(() => { + return selectedViews.some( + (path) => + EP.isDescendantOf(props.entry.elementPath, path) && + !EP.pathsEqual(path, props.entry.elementPath), + ) + }, [props.entry, selectedViews]) + + const isSelected = React.useMemo(() => { + return selectedViews.some((path) => EP.pathsEqual(path, props.entry.elementPath)) + }, [selectedViews, props.entry]) + + const onClick = React.useCallback( + (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + dispatch([selectComponents([props.entry.elementPath], false)]) + }, + [dispatch, props.entry], + ) + + const onMouseOver = React.useCallback(() => { + dispatch([setHighlightedViews([props.entry.elementPath])]) + }, [props.entry, dispatch]) + + const onMouseOut = React.useCallback(() => { + dispatch([ + setHighlightedViews( + highlightedViews.filter((path) => !EP.pathsEqual(path, props.entry.elementPath)), + ), + ]) + }, [props.entry, dispatch, highlightedViews]) + + const collapse = React.useCallback( + (elementPath: ElementPath) => (e: React.MouseEvent) => { + e.stopPropagation() + dispatch([toggleCollapse(elementPath)], 'leftpane') + }, + [dispatch], + ) + + return ( + + +
+
+ {when( + props.showExpandableIndicator, +
+ +
, + )} + + {when( + showLabel, + + {labelForTheElement} + , + )} +
+
+
+
+ {props.separator} +
+
+ ) + }, +) +CondensedEntryItem.displayName = 'CondensedEntryItem' type SingleEntryNavigatorItemWrapperProps = NavigatorItemWrapperProps & { indentation: number diff --git a/editor/src/components/navigator/navigator-item/navigator-item.tsx b/editor/src/components/navigator/navigator-item/navigator-item.tsx index fa77faabe6a4..88a18206ae21 100644 --- a/editor/src/components/navigator/navigator-item/navigator-item.tsx +++ b/editor/src/components/navigator/navigator-item/navigator-item.tsx @@ -466,7 +466,7 @@ const isHiddenConditionalBranchSelector = createCachedSelector( }, )((_, elementPath, parentPath) => `${EP.toString(elementPath)}_${EP.toString(parentPath)}`) -const elementWarningsSelector = createCachedSelector( +export const elementWarningsSelector = createCachedSelector( (store: DerivedSubstate) => store.derived.elementWarnings, (_: DerivedSubstate, navigatorEntry: NavigatorEntry) => navigatorEntry, (elementWarnings, navigatorEntry) => { diff --git a/editor/src/components/navigator/navigator-row.tsx b/editor/src/components/navigator/navigator-row.tsx index 450ecd41c8d4..029ca5779f85 100644 --- a/editor/src/components/navigator/navigator-row.tsx +++ b/editor/src/components/navigator/navigator-row.tsx @@ -26,10 +26,12 @@ export function isRegulaNavigatorRow(row: NavigatorRow): row is RegularNavigator export interface CondensedNavigatorRow { type: 'condensed-row' indentation: number - variant: 'trunk' | 'leaf' + variant: CondensedNavigatorRowVariant entries: Array } +export type CondensedNavigatorRowVariant = 'trunk' | 'leaf' + export function condensedNavigatorRow( entries: Array, variant: 'trunk' | 'leaf', diff --git a/editor/src/components/navigator/navigator-utils.ts b/editor/src/components/navigator/navigator-utils.ts index d15569679cbc..8e8e8e2128b3 100644 --- a/editor/src/components/navigator/navigator-utils.ts +++ b/editor/src/components/navigator/navigator-utils.ts @@ -772,11 +772,16 @@ export function getNavigatorTargets( const navigatorRows = getNavigatorRowsForTree(navigatorTrees, 'all-navigator-targets') const navigatorTargets = navigatorRows.flatMap(getEntriesForRow) + const visibleNavigatorRows = getNavigatorRowsForTree(navigatorTrees, 'visible-navigator-targets') - const visibleNavigatorTargets = visibleNavigatorRows.flatMap(getEntriesForRow) + const filteredVisibleNavigatorRows = filterCollapsedNavigatorRows( + visibleNavigatorRows, + collapsedViews, + ) + const visibleNavigatorTargets = filteredVisibleNavigatorRows.flatMap(getEntriesForRow) return { - navigatorRows: visibleNavigatorRows, + navigatorRows: filteredVisibleNavigatorRows, navigatorTargets: navigatorTargets, visibleNavigatorTargets: visibleNavigatorTargets, } @@ -805,3 +810,38 @@ export function getConditionalClausePathForNavigatorEntry( function renderPropId(propName: string): string { return `prop-label-${propName}` } + +function filterCollapsedNavigatorRows( + visibleNavigatorRows: NavigatorRow[], + collapsedViews: ElementPath[], +) { + // 1. grab the condensed rows + const condensedRows = visibleNavigatorRows.filter( + (row) => row.type === 'condensed-row', + ) as CondensedNavigatorRow[] + // 2. get the EPs of collapsed condensed rows + const collapsedCondensedRows = condensedRows + .map((row) => { + return row.entries[0].elementPath + }) + .filter((row) => { + return collapsedViews.some((path) => EP.pathsEqual(path, row)) + }) + // 3. filter out the rows which are descendants of collapsed condensed rows + const filteredVisibleNavigatorRows = visibleNavigatorRows.filter((row) => { + const isChildOfCollapsedCondensedRow = collapsedCondensedRows.some((collapsed) => { + switch (row.type) { + case 'condensed-row': + return EP.isDescendantOf(row.entries[0].elementPath, collapsed) + case 'regular-row': + return EP.isDescendantOf(row.entry.elementPath, collapsed) + default: + assertNever(row) + return false // lint + } + }) + return !isChildOfCollapsedCondensedRow + }) + + return filteredVisibleNavigatorRows +}