Skip to content

Commit

Permalink
Feautre/cartouche context menu (#5993)
Browse files Browse the repository at this point in the history
This PR adds a basic context menu to the Data Reference Cartouche.
<img width="315" alt="image"
src="https://github.com/concrete-utopia/utopia/assets/2226774/a4f57aae-cc2c-4fce-8389-376e28d3a68c">

Most of my evening was spent wrestling ContextMenuWrapper. I decided to
"give up" and create a new component which uses a proper portal instead
of horrible x-hacks and whatevers.


**Commit Details:**
- Renamed `ContextMenuWrapper` to `ContextMenuWrapper_DEPRECATED` also
marked it as deprecated
- Added new `ContextMenuWrapper` which takes way less props, uses less
wrapping divs, and uses a Portal.
- `DataCartoucheInner` uses `ContextMenuWrapper`. 
- The only actually working option is `Replace...` which just opens the
data picker
- Everything else is a placeholder for future capability

Fixes #5972

---------

Co-authored-by: Berci Kormendy <[email protected]>
  • Loading branch information
balazsbajorics and bkrmendy authored Jun 19, 2024
1 parent f4b8ea8 commit 16381e0
Show file tree
Hide file tree
Showing 10 changed files with 366 additions and 68 deletions.
46 changes: 44 additions & 2 deletions editor/src/components/context-menu-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {
Submenu as SubmenuComponent,
useContextMenu,
} from 'react-contexify'
import { colorTheme, Icons, UtopiaStyles } from '../uuiui'
import { colorTheme, Icons, OnClickOutsideHOC, UtopiaStyles } from '../uuiui'
import { getControlStyles } from '../uuiui-deps'
import type { ContextMenuItem } from './context-menu-items'
import type { EditorDispatch } from './editor/action-types'
import type { WindowPoint } from '../core/shared/math-utils'
import { windowPoint } from '../core/shared/math-utils'
import { addOpenMenuId, removeOpenMenuId } from '../core/shared/menu-state'
import { createPortal } from 'react-dom'
import { CanvasContextMenuPortalTargetID } from '../core/shared/utils'

interface Submenu<T> {
items: Item<T>[]
Expand Down Expand Up @@ -176,7 +178,8 @@ export const ContextMenu = <T,>({ dispatch, getData, id, items }: ContextMenuPro
)
}

export const ContextMenuWrapper = <T,>({
/** @deprecated use ContextMenuWrapper instead, which is Portaled */
export const ContextMenuWrapper_DEPRECATED = <T,>({
children,
className = '',
data,
Expand Down Expand Up @@ -290,6 +293,7 @@ export const MenuProvider = ({

const onContextMenu = React.useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation()
if (itemsLength <= 0) {
return
}
Expand All @@ -311,3 +315,41 @@ export const MenuProvider = ({
</div>
)
}

export const ContextMenuWrapper = <T,>({
children,
data,
dispatch,
id,
items,
style,
}: {
children?: React.ReactNode
data: T
dispatch?: EditorDispatch
id: string
items: ContextMenuItem<T>[]
style?: React.CSSProperties
}) => {
const { hideAll } = useContextMenu({ id })

const getData = React.useCallback(() => data, [data])

const portalTarget = document.getElementById(CanvasContextMenuPortalTargetID)

return (
<React.Fragment>
<MenuProvider id={id} itemsLength={items.length} key={`${id}-provider`} style={style}>
{children}
</MenuProvider>
{portalTarget != null
? createPortal(
<OnClickOutsideHOC onClickOutside={hideAll}>
<ContextMenu dispatch={dispatch} getData={getData} id={id} items={items} key={id} />
</OnClickOutsideHOC>,
portalTarget,
)
: null}
</React.Fragment>
)
}
6 changes: 3 additions & 3 deletions editor/src/components/filebrowser/fileitem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Clipboard } from '../../utils/clipboard'
import Utils from '../../utils/utils'
import type { ContextMenuItem } from '../context-menu-items'
import { requireDispatch } from '../context-menu-items'
import { ContextMenuWrapper } from '../context-menu-wrapper'
import { ContextMenuWrapper_DEPRECATED } from '../context-menu-wrapper'
import type { EditorAction, EditorDispatch } from '../editor/action-types'
import * as EditorActions from '../editor/actions/action-creators'
import { ExpandableIndicator } from '../navigator/navigator-item/expandable-indicator'
Expand Down Expand Up @@ -887,14 +887,14 @@ class FileBrowserItemInner extends React.PureComponent<
style={{ width: '100%' }}
key={`${contextMenuID}-wrapper`}
>
<ContextMenuWrapper
<ContextMenuWrapper_DEPRECATED
id={contextMenuID}
dispatch={this.props.dispatch}
items={items}
data={{}}
>
{fileBrowserItem}
</ContextMenuWrapper>
</ContextMenuWrapper_DEPRECATED>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
RegularControlDescription,
} from '../../../custom-code/internal-property-controls'
import type { ElementPath } from '../../../../core/shared/project-file-types'
import * as EP from '../../../../core/shared/element-path'
import * as PP from '../../../../core/shared/property-path'
import { iconForControlType } from '../../../../uuiui'
import type { ControlForPropProps } from './property-control-controls'
Expand Down Expand Up @@ -66,7 +67,7 @@ export function useChildrenPropOverride(
matchType='partial'
onOpenDataPicker={props.onOpenDataPicker}
onDeleteCartouche={props.onDeleteCartouche}
testId={`cartouche-${PP.toString(props.propPath)}`}
testId={`cartouche-${EP.toString(props.elementPath)}-${PP.toString(props.propPath)}`}
propertyPath={props.propPath}
safeToDelete={props.safeToDelete}
elementPath={props.elementPath}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import type { CartoucheDataType, CartoucheHighlight, CartoucheUIProps } from './
import { CartoucheUI } from './cartouche-ui'
import * as PP from '../../../../core/shared/property-path'
import { AllHtmlEntities } from 'html-entities'
import { ContextMenuWrapper } from '../../../context-menu-wrapper'
import type { ContextMenuItem } from '../../../context-menu-items'
import { optionalMap } from '../../../../core/shared/optional-utils'

const htmlEntities = new AllHtmlEntities()
Expand Down Expand Up @@ -185,7 +187,7 @@ export const DataReferenceCartoucheControl = React.memo(
export type DataReferenceCartoucheContentType = 'value-literal' | 'object-literal' | 'reference'
interface DataCartoucheInnerProps {
onClick: (e: React.MouseEvent) => void
onDoubleClick: (e: React.MouseEvent) => void
onDoubleClick: () => void
selected: boolean
contentsToDisplay: {
type: DataReferenceCartoucheContentType
Expand Down Expand Up @@ -216,6 +218,8 @@ export const DataCartoucheInner = React.forwardRef(
datatype,
} = props

const dispatch = useDispatch()

const onDeleteInner = React.useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
Expand All @@ -236,26 +240,79 @@ export const DataCartoucheInner = React.forwardRef(
: 'internal'

return (
<CartoucheUI
onDelete={onDelete}
onClick={onClick}
onDoubleClick={onDoubleClick}
datatype={datatype}
selected={selected}
highlight={highlight}
testId={testId}
tooltip={contentsToDisplay.label ?? contentsToDisplay.shortLabel ?? 'DATA'}
role='selection'
source={source}
ref={ref}
badge={props.badge}
<ContextMenuWrapper<ContextMenuItemsData>
id={`cartouche-context-menu-${props.testId}`}
dispatch={dispatch}
items={contextMenuItems}
data={{ openDataPicker: onDoubleClick, deleteCartouche: onDeleteCallback }}
>
{contentsToDisplay.shortLabel ?? contentsToDisplay.label ?? 'DATA'}
</CartoucheUI>
<CartoucheUI
onDelete={onDelete}
onClick={onClick}
onDoubleClick={onDoubleClick}
datatype={datatype}
selected={selected}
highlight={highlight}
testId={testId}
tooltip={contentsToDisplay.label ?? contentsToDisplay.shortLabel ?? 'DATA'}
role='selection'
source={source}
ref={ref}
badge={props.badge}
>
{contentsToDisplay.shortLabel ?? contentsToDisplay.label ?? 'DATA'}
</CartoucheUI>
</ContextMenuWrapper>
)
},
)

type ContextMenuItemsData = {
openDataPicker?: () => void
deleteCartouche?: () => void
}

const Separator = {
name: <div key='separator' className='contexify_separator' />,
enabled: false,
action: NO_OP,
isSeparator: true,
} as const

const contextMenuItems: Array<ContextMenuItem<ContextMenuItemsData>> = [
{
name: 'Replace...',
enabled: (data) => data.openDataPicker != null,
action: (data) => {
data.openDataPicker?.()
},
},
{
name: 'Remove',
enabled: false,
action: (data) => {
data.deleteCartouche?.()
},
},
Separator,
{
name: 'Edit value',
enabled: false,
action: (data) => {},
},
{
name: 'Open in external CMS',
enabled: false,
action: (data) => {},
},
Separator,
{
name: 'Open in code editor',
enabled: false,
action: (data) => {},
},
]

export function getTextContentOfElement(
element: JSXElementChild,
metadata: ElementInstanceMetadata | null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ export const DataSelectorModal = React.memo(
onChange={onSearchFieldValueChange}
ref={searchBoxRef}
value={searchTerm ?? ''}
data-testId='data-selector-modal-search-input'
data-testid='data-selector-modal-search-input'
placeholder='Search data'
style={{
outline: 'none',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
UIRow,
Icn,
} from '../../../../uuiui'
import { ContextMenuWrapper } from '../../../../uuiui-deps'
import { ContextMenuWrapper_DEPRECATED } from '../../../../uuiui-deps'
import { useDispatch } from '../../../editor/store/dispatch-context'
import { useEditorState } from '../../../editor/store/store-hook'
import { ExpandableIndicator } from '../../../navigator/navigator-item/expandable-indicator'
Expand Down Expand Up @@ -299,7 +299,7 @@ const TargetListItem = React.memo((props: TargetListItemProps) => {
}, [onDeleteByIndex, itemIndex])

return (
<ContextMenuWrapper
<ContextMenuWrapper_DEPRECATED
id={`${id}-contextMenu`}
items={[
{
Expand Down Expand Up @@ -366,7 +366,7 @@ const TargetListItem = React.memo((props: TargetListItemProps) => {
</React.Fragment>
)}
</UIRow>
</ContextMenuWrapper>
</ContextMenuWrapper_DEPRECATED>
)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { traceDataFromElement, dataPathSuccess } from '../../../../core/data-tra
import type { CartoucheDataType } from '../component-section/cartouche-ui'
import { CartoucheInspectorWrapper } from '../component-section/cartouche-control'
import { MapCounter } from '../../../navigator/navigator-item/map-counter'
import * as EP from '../../../../core/shared/element-path'

interface MapListSourceCartoucheProps {
target: ElementPath
Expand Down Expand Up @@ -174,7 +175,7 @@ const MapListSourceCartoucheInner = React.memo(
onDelete={NO_OP}
selected={props.selected}
safeToDelete={false}
testId='list-source-cartouche'
testId={`list-source-cartouche-${EP.toString(target)}`}
contentIsComingFromServer={isDataComingFromHookResult}
datatype={cartoucheDataType}
badge={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import type { PreferredChildComponentDescriptor } from '../../custom-code/intern
import { fixUtopiaElement, generateConsistentUID } from '../../../core/shared/uid-utils'
import { getAllUniqueUids } from '../../../core/model/get-unique-ids'
import { elementFromInsertMenuItem } from '../../editor/insert-callbacks'
import { ContextMenuWrapper } from '../../context-menu-wrapper'
import { ContextMenuWrapper_DEPRECATED } from '../../context-menu-wrapper'
import { BodyMenuOpenClass, assertNever } from '../../../core/shared/utils'
import { type ContextMenuItem } from '../../context-menu-items'
import { FlexRow, Icn, type IcnProps } from '../../../uuiui'
Expand Down Expand Up @@ -848,7 +848,7 @@ const ComponentPickerContextMenuSimple = React.memo<ComponentPickerContextMenuPr
const submenuLabel = (
<FlexRow
style={{ gap: 10, width: 228 }}
data-testId={labelTestIdForComponentIcon(data.name, data.moduleName ?? '', data.icon)}
data-testid={labelTestIdForComponentIcon(data.name, data.moduleName ?? '', data.icon)}
>
<Icn {...iconProps} width={12} height={12} />
{data.name}
Expand All @@ -868,7 +868,12 @@ const ComponentPickerContextMenuSimple = React.memo<ComponentPickerContextMenuPr
.concat([separatorItem, moreItem(wrapperRef, showFullMenu)])

return (
<ContextMenuWrapper items={items} data={{}} id={PreferredMenuId} forwardRef={wrapperRef} />
<ContextMenuWrapper_DEPRECATED
items={items}
data={{}}
id={PreferredMenuId}
forwardRef={wrapperRef}
/>
)
},
)
Expand Down
Loading

0 comments on commit 16381e0

Please sign in to comment.