From 1575413a5b224efa3ced196209c4c0cdd70ed36d Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 22 Nov 2022 19:05:41 +0000 Subject: [PATCH 01/17] Use drafts, remove throttling --- .../PageEditor/RenderPanel/RenderOverlay.tsx | 382 +++++++++++------- .../toolpad-app/src/toolpad/DomLoader.tsx | 41 +- 2 files changed, 256 insertions(+), 167 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index 3aeb027ff90..05a94915315 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -209,16 +209,20 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); const deleteOrphanedLayoutComponents = React.useCallback( - (movedOrDeletedNode: appDom.ElementNode, moveTargetNodeId: NodeId | null = null) => { + ( + draft: appDom.AppDom, + movedOrDeletedNode: appDom.ElementNode, + moveTargetNodeId: NodeId | null = null, + ) => { const movedOrDeletedNodeParentProp = movedOrDeletedNode.parentProp; - const parent = appDom.getParent(dom, movedOrDeletedNode); - const parentParent = parent && appDom.getParent(dom, parent); - const parentParentParent = parentParent && appDom.getParent(dom, parentParent); + const parent = appDom.getParent(draft, movedOrDeletedNode); + const parentParent = parent && appDom.getParent(draft, parent); + const parentParentParent = parentParent && appDom.getParent(draft, parentParent); const parentChildren = parent && movedOrDeletedNodeParentProp - ? (appDom.getChildNodes(dom, parent) as appDom.NodeChildren)[ + ? (appDom.getChildNodes(draft, parent) as appDom.NodeChildren)[ movedOrDeletedNodeParentProp ] : []; @@ -234,7 +238,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { parent.parentProp && appDom.isElement(parentParent) && isPageLayoutComponent(parentParent) && - appDom.getChildNodes(dom, parentParent)[parent.parentProp].length === 1; + appDom.getChildNodes(draft, parentParent)[parent.parentProp].length === 1; const isSecondLastLayoutContainerChild = parent && @@ -260,7 +264,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { moveTargetNodeId !== lastContainerChild.id && isPageLayoutComponent(parentParent) ) { - domApi.moveNode( + draft = appDom.moveNode( + draft, lastContainerChild, parentParent, lastContainerChild.parentProp, @@ -268,7 +273,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); if (isPageColumn(parent)) { - domApi.setNodeNamespacedProp( + draft = appDom.setNodeNamespacedProp( + draft, lastContainerChild, 'layout', 'columnSize', @@ -276,7 +282,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); } - domApi.removeNode(parent.id); + draft = appDom.removeNode(draft, parent.id); } if ( @@ -287,7 +293,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { moveTargetNodeId !== parentParent.id && moveTargetNodeId !== lastContainerChild.id ) { - domApi.moveNode( + draft = appDom.moveNode( + draft, lastContainerChild, parentParentParent, lastContainerChild.parentProp, @@ -295,7 +302,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); if (isPageColumn(parentParent)) { - domApi.setNodeNamespacedProp( + draft = appDom.setNodeNamespacedProp( + draft, lastContainerChild, 'layout', 'columnSize', @@ -303,21 +311,23 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); } - domApi.removeNode(parentParent.id); + draft = appDom.removeNode(draft, parentParent.id); } } } } if (isOnlyLayoutContainerChild) { - domApi.removeNode(parent.id); + draft = appDom.removeNode(draft, parent.id); if (isParentOnlyLayoutContainerChild && moveTargetNodeId !== parentParent.id) { - domApi.removeNode(parentParent.id); + draft = appDom.removeNode(draft, parentParent.id); } } + + return draft; }, - [dom, domApi], + [], ); const handleNodeDelete = React.useCallback( @@ -326,17 +336,21 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { event.stopPropagation(); } - const toRemove = appDom.getNode(dom, nodeId); + domApi.update((draft): appDom.AppDom => { + const toRemove = appDom.getNode(draft, nodeId); - domApi.removeNode(toRemove.id); + draft = appDom.removeNode(draft, toRemove.id); - if (appDom.isElement(toRemove)) { - deleteOrphanedLayoutComponents(toRemove); - } + if (appDom.isElement(toRemove)) { + draft = deleteOrphanedLayoutComponents(draft, toRemove); + } + + return draft; + }); api.deselect(); }, - [dom, domApi, api, deleteOrphanedLayoutComponents], + [domApi, api, deleteOrphanedLayoutComponents], ); const selectedRect = selectedNode ? nodesInfo[selectedNode.id]?.rect : null; @@ -878,27 +892,31 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { const isOriginalParentColumn = originalParent && appDom.isElement(originalParent) ? isPageColumn(originalParent) : false; - let addOrMoveNode = domApi.addNode; + let addOrMoveNode = appDom.addNode; if (selection) { - addOrMoveNode = domApi.moveNode; + addOrMoveNode = appDom.moveNode; } // Drop on page or layout slot if (isDraggingOverPage || isDraggingOverLayoutSlot) { - const newParentIndex = - dragOverZone === DROP_ZONE_TOP - ? appDom.getNewFirstParentIndexInNode(dom, dragOverNode, 'children') - : appDom.getNewLastParentIndexInNode(dom, dragOverNode, 'children'); - - if (!isPageRow(draggedNode)) { - const rowContainer = appDom.createElement(dom, PAGE_ROW_COMPONENT_ID, {}); - domApi.addNode(rowContainer, dragOverNode, 'children', newParentIndex); - parent = rowContainer; - - addOrMoveNode(draggedNode, rowContainer, 'children'); - } else { - addOrMoveNode(draggedNode, dragOverNode, 'children', newParentIndex); - } + domApi.update((draft): appDom.AppDom => { + const newParentIndex = + dragOverZone === DROP_ZONE_TOP + ? appDom.getNewFirstParentIndexInNode(draft, dragOverNode, 'children') + : appDom.getNewLastParentIndexInNode(draft, dragOverNode, 'children'); + + if (!isPageRow(draggedNode)) { + const rowContainer = appDom.createElement(dom, PAGE_ROW_COMPONENT_ID, {}); + draft = appDom.addNode(draft, rowContainer, dragOverNode, 'children', newParentIndex); + parent = rowContainer; + + draft = addOrMoveNode(draft, draggedNode, rowContainer, 'children'); + } else { + draft = addOrMoveNode(draft, draggedNode, dragOverNode, 'children', newParentIndex); + } + + return draft; + }); } if ( @@ -916,145 +934,225 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ? isVerticalFlow(dragOverSlot.flowDirection) : false; + const setNewElementLayout = ( + draft: appDom.AppDom, + elementParent: appDom.PageNode | appDom.ElementNode, + ) => { + const draggedNodeParent = selection ? appDom.getParent(draft, draggedNode) : null; + if ( + draggedNode.layout?.columnSize && + draggedNodeParent && + draggedNodeParent.id !== elementParent.id + ) { + appDom.setNodeNamespacedProp( + draft, + draggedNode, + 'layout', + 'columnSize', + appDom.createConst(1), + ); + } + + return draft; + }; + if (dragOverZone === DROP_ZONE_CENTER && dragOverSlotParentProp) { - addOrMoveNode(draggedNode, dragOverNode, dragOverSlotParentProp); + domApi.update((draft): appDom.AppDom => { + draft = addOrMoveNode(draft, draggedNode, dragOverNode, dragOverSlotParentProp); + + draft = setNewElementLayout(draft, parent); + + if (selection) { + draft = deleteOrphanedLayoutComponents(draft, draggedNode, dragOverNodeId); + } + + return draft; + }); } if ([DROP_ZONE_TOP, DROP_ZONE_BOTTOM].includes(dragOverZone)) { - if (!isDraggingOverVerticalContainer) { - const newParentIndex = - dragOverZone === DROP_ZONE_TOP - ? appDom.getNewParentIndexBeforeNode(dom, dragOverNode, dragOverNodeParentProp) - : appDom.getNewParentIndexAfterNode(dom, dragOverNode, dragOverNodeParentProp); - - if (isDraggingOverRow && !isPageRow(draggedNode)) { - if (isOriginalParentPage) { - const rowContainer = appDom.createElement(dom, PAGE_ROW_COMPONENT_ID, {}); - domApi.addNode(rowContainer, parent, dragOverNodeParentProp, newParentIndex); - parent = rowContainer; - - addOrMoveNode(draggedNode, parent, dragOverNodeParentProp); - } else { - addOrMoveNode(draggedNode, parent, dragOverNodeParentProp, newParentIndex); + domApi.update((draft): appDom.AppDom => { + if (!isDraggingOverVerticalContainer) { + const newParentIndex = + dragOverZone === DROP_ZONE_TOP + ? appDom.getNewParentIndexBeforeNode(draft, dragOverNode, dragOverNodeParentProp) + : appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp); + + if (isDraggingOverRow && !isPageRow(draggedNode)) { + if (isOriginalParentPage) { + const rowContainer = appDom.createElement(draft, PAGE_ROW_COMPONENT_ID, {}); + draft = appDom.addNode( + draft, + rowContainer, + parent, + dragOverNodeParentProp, + newParentIndex, + ); + parent = rowContainer; + + draft = addOrMoveNode(draft, draggedNode, parent, dragOverNodeParentProp); + } else { + draft = addOrMoveNode( + draft, + draggedNode, + parent, + dragOverNodeParentProp, + newParentIndex, + ); + } } - } - if (isOriginalParentRow) { - const columnContainer = appDom.createElement( - dom, - PAGE_COLUMN_COMPONENT_ID, - {}, - { - columnSize: dragOverNode.layout?.columnSize || appDom.createConst(1), - }, - ); + if (isOriginalParentRow) { + const columnContainer = appDom.createElement( + draft, + PAGE_COLUMN_COMPONENT_ID, + {}, + { + columnSize: dragOverNode.layout?.columnSize || appDom.createConst(1), + }, + ); - domApi.setNodeNamespacedProp( - dragOverNode, - 'layout', - 'columnSize', - appDom.createConst(1), - ); + draft = appDom.setNodeNamespacedProp( + draft, + dragOverNode, + 'layout', + 'columnSize', + appDom.createConst(1), + ); - domApi.addNode( - columnContainer, - parent, - dragOverNodeParentProp, - appDom.getNewParentIndexAfterNode(dom, dragOverNode, dragOverNodeParentProp), - ); - parent = columnContainer; + draft = appDom.addNode( + draft, + columnContainer, + parent, + dragOverNodeParentProp, + appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp), + ); + parent = columnContainer; - // Move existing element inside column right away if drag over zone is bottom - if (dragOverZone === DROP_ZONE_BOTTOM) { - domApi.moveNode(dragOverNode, parent, dragOverNodeParentProp); + // Move existing element inside column right away if drag over zone is bottom + if (dragOverZone === DROP_ZONE_BOTTOM) { + draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); + } + } + + if (!isDraggingOverRow || isPageRow(draggedNode)) { + draft = addOrMoveNode( + draft, + draggedNode, + parent, + dragOverNodeParentProp, + newParentIndex, + ); } - } - if (!isDraggingOverRow || isPageRow(draggedNode)) { - addOrMoveNode(draggedNode, parent, dragOverNodeParentProp, newParentIndex); + // Only move existing element inside column in the end if drag over zone is top + if ( + isOriginalParentRow && + !isDraggingOverVerticalContainer && + dragOverZone === DROP_ZONE_TOP + ) { + draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); + } } - // Only move existing element inside column in the end if drag over zone is top - if ( - isOriginalParentRow && - !isDraggingOverVerticalContainer && - dragOverZone === DROP_ZONE_TOP - ) { - domApi.moveNode(dragOverNode, parent, dragOverNodeParentProp); + if (dragOverSlotParentProp && isDraggingOverVerticalContainer) { + const isDraggingOverDirectionStart = + dragOverZone === + (dragOverSlot?.flowDirection === 'column' ? DROP_ZONE_TOP : DROP_ZONE_BOTTOM); + + const newParentIndex = isDraggingOverDirectionStart + ? appDom.getNewFirstParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp) + : appDom.getNewLastParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp); + + draft = addOrMoveNode( + draft, + draggedNode, + dragOverNode, + dragOverSlotParentProp, + newParentIndex, + ); } - } - if (dragOverSlotParentProp && isDraggingOverVerticalContainer) { - const isDraggingOverDirectionStart = - dragOverZone === - (dragOverSlot?.flowDirection === 'column' ? DROP_ZONE_TOP : DROP_ZONE_BOTTOM); + draft = setNewElementLayout(draft, parent); - const newParentIndex = isDraggingOverDirectionStart - ? appDom.getNewFirstParentIndexInNode(dom, dragOverNode, dragOverSlotParentProp) - : appDom.getNewLastParentIndexInNode(dom, dragOverNode, dragOverSlotParentProp); + if (selection) { + draft = deleteOrphanedLayoutComponents(draft, draggedNode, dragOverNodeId); + } - addOrMoveNode(draggedNode, dragOverNode, dragOverSlotParentProp, newParentIndex); - } + return draft; + }); } if ([DROP_ZONE_RIGHT, DROP_ZONE_LEFT].includes(dragOverZone)) { - if (!isDraggingOverHorizontalContainer) { - if (isOriginalParentColumn) { - const rowContainer = appDom.createElement(dom, PAGE_ROW_COMPONENT_ID, { - justifyContent: appDom.createConst(originalParentInfo?.props.alignItems || 'start'), - }); - domApi.addNode( - rowContainer, + domApi.update((draft): appDom.AppDom => { + if (!isDraggingOverHorizontalContainer) { + if (isOriginalParentColumn) { + const rowContainer = appDom.createElement(draft, PAGE_ROW_COMPONENT_ID, { + justifyContent: appDom.createConst( + originalParentInfo?.props.alignItems || 'start', + ), + }); + draft = appDom.addNode( + draft, + rowContainer, + parent, + dragOverNodeParentProp, + appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp), + ); + parent = rowContainer; + + // Move existing element inside right away if drag over zone is right + if (dragOverZone === DROP_ZONE_RIGHT) { + draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); + } + } + + const newParentIndex = + dragOverZone === DROP_ZONE_RIGHT + ? appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp) + : appDom.getNewParentIndexBeforeNode(draft, dragOverNode, dragOverNodeParentProp); + + draft = addOrMoveNode( + draft, + draggedNode, parent, dragOverNodeParentProp, - appDom.getNewParentIndexAfterNode(dom, dragOverNode, dragOverNodeParentProp), + newParentIndex, ); - parent = rowContainer; - // Move existing element inside right away if drag over zone is right - if (dragOverZone === DROP_ZONE_RIGHT) { - domApi.moveNode(dragOverNode, parent, dragOverNodeParentProp); + // Only move existing element inside column in the end if drag over zone is left + if (isOriginalParentColumn && dragOverZone === DROP_ZONE_LEFT) { + draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); } } - const newParentIndex = - dragOverZone === DROP_ZONE_RIGHT - ? appDom.getNewParentIndexAfterNode(dom, dragOverNode, dragOverNodeParentProp) - : appDom.getNewParentIndexBeforeNode(dom, dragOverNode, dragOverNodeParentProp); + if (dragOverSlotParentProp && isDraggingOverHorizontalContainer) { + const isDraggingOverDirectionStart = + dragOverZone === + (dragOverSlot?.flowDirection === 'row' ? DROP_ZONE_LEFT : DROP_ZONE_RIGHT); - addOrMoveNode(draggedNode, parent, dragOverNodeParentProp, newParentIndex); + const newParentIndex = isDraggingOverDirectionStart + ? appDom.getNewFirstParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp) + : appDom.getNewLastParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp); - // Only move existing element inside column in the end if drag over zone is left - if (isOriginalParentColumn && dragOverZone === DROP_ZONE_LEFT) { - domApi.moveNode(dragOverNode, parent, dragOverNodeParentProp); + draft = addOrMoveNode( + draft, + draggedNode, + dragOverNode, + dragOverSlotParentProp, + newParentIndex, + ); } - } - if (dragOverSlotParentProp && isDraggingOverHorizontalContainer) { - const isDraggingOverDirectionStart = - dragOverZone === - (dragOverSlot?.flowDirection === 'row' ? DROP_ZONE_LEFT : DROP_ZONE_RIGHT); + draft = setNewElementLayout(draft, parent); - const newParentIndex = isDraggingOverDirectionStart - ? appDom.getNewFirstParentIndexInNode(dom, dragOverNode, dragOverSlotParentProp) - : appDom.getNewLastParentIndexInNode(dom, dragOverNode, dragOverSlotParentProp); + if (selection) { + draft = deleteOrphanedLayoutComponents(draft, draggedNode, dragOverNodeId); + } - addOrMoveNode(draggedNode, dragOverNode, dragOverSlotParentProp, newParentIndex); - } + return draft; + }); } - - const draggedNodeParent = selection ? appDom.getParent(dom, draggedNode) : null; - if ( - draggedNode.layout?.columnSize && - draggedNodeParent && - draggedNodeParent.id !== parent.id - ) { - domApi.setNodeNamespacedProp(draggedNode, 'layout', 'columnSize', appDom.createConst(1)); - } - } - - if (selection) { - deleteOrphanedLayoutComponents(draggedNode, dragOverNodeId); } api.dragEnd(); diff --git a/packages/toolpad-app/src/toolpad/DomLoader.tsx b/packages/toolpad-app/src/toolpad/DomLoader.tsx index 2ca523ce37f..b25dd7a3074 100644 --- a/packages/toolpad-app/src/toolpad/DomLoader.tsx +++ b/packages/toolpad-app/src/toolpad/DomLoader.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { NodeId, BindableAttrValue, BindableAttrValues } from '@mui/toolpad-core'; import invariant from 'invariant'; -import { throttle, DebouncedFunc } from 'lodash-es'; import * as appDom from '../appDom'; import { update } from '../utils/immutability'; import client from '../api'; @@ -52,6 +51,10 @@ export type DomAction = namespace: string; value: BindableAttrValues | null; } + | { + type: 'DOM_UPDATE'; + updater: (dom: appDom.AppDom) => appDom.AppDom; + } | { type: 'DOM_ADD_NODE'; node: appDom.AppDomNode; @@ -105,6 +108,9 @@ export function domReducer(dom: appDom.AppDom, action: DomAction): appDom.AppDom case 'DOM_SET_NODE_NAMESPACE': { return appDom.setNodeNamespace(dom, action.node, action.namespace, action.value); } + case 'DOM_UPDATE': { + return action.updater(dom); + } case 'DOM_ADD_NODE': { return appDom.addNode( dom, @@ -239,14 +245,9 @@ export function domLoaderReducer(state: DomLoader, action: DomAction): DomLoader } } -function createDomApi( - dispatch: React.Dispatch, - scheduleHistoryUpdate?: DebouncedFunc<() => void>, -) { +function createDomApi(dispatch: React.Dispatch) { return { undo() { - scheduleHistoryUpdate?.flush(); - dispatch({ type: 'DOM_UNDO' }); }, redo() { @@ -255,6 +256,12 @@ function createDomApi( setNodeName(nodeId: NodeId, name: string) { dispatch({ type: 'DOM_SET_NODE_NAME', nodeId, name }); }, + update(updater: (dom: appDom.AppDom) => appDom.AppDom) { + dispatch({ + type: 'DOM_UPDATE', + updater, + }); + }, addNode( node: Child, parent: Parent, @@ -422,31 +429,15 @@ export default function DomProvider({ appId, children }: DomContextProps) { undoStack: [dom], redoStack: [], }); - - const scheduleHistoryUpdate = React.useMemo( - () => - throttle( - () => { - dispatch({ type: 'DOM_UPDATE_HISTORY' }); - }, - 500, - { leading: false, trailing: true }, - ), - [], - ); - const dispatchWithHistory = useEvent((action: DomAction) => { dispatch(action); if (!SKIP_UNDO_ACTIONS.has(action.type)) { - scheduleHistoryUpdate(); + dispatch({ type: 'DOM_UPDATE_HISTORY' }); } }); - const api = React.useMemo( - () => createDomApi(dispatchWithHistory, scheduleHistoryUpdate), - [dispatchWithHistory, scheduleHistoryUpdate], - ); + const api = React.useMemo(() => createDomApi(dispatchWithHistory), [dispatchWithHistory]); const handleSave = React.useCallback(() => { if (!state.dom || state.saving || state.savedDom === state.dom) { From ee45df308ff6358df4e22f67725734a0f5c344dc Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 22 Nov 2022 19:14:10 +0000 Subject: [PATCH 02/17] Forgot one dom --- .../toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index 05a94915315..46cd9b9b469 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -906,7 +906,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { : appDom.getNewLastParentIndexInNode(draft, dragOverNode, 'children'); if (!isPageRow(draggedNode)) { - const rowContainer = appDom.createElement(dom, PAGE_ROW_COMPONENT_ID, {}); + const rowContainer = appDom.createElement(draft, PAGE_ROW_COMPONENT_ID, {}); draft = appDom.addNode(draft, rowContainer, dragOverNode, 'children', newParentIndex); parent = rowContainer; From a3bb6502c344e6def7a9d3f5eeb2832ece1975e6 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 23 Nov 2022 21:50:17 +0000 Subject: [PATCH 03/17] Fix types --- packages/toolpad-app/src/appDom/index.ts | 4 ++-- .../AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/toolpad-app/src/appDom/index.ts b/packages/toolpad-app/src/appDom/index.ts index 2faab3af3bd..766b08f12ff 100644 --- a/packages/toolpad-app/src/appDom/index.ts +++ b/packages/toolpad-app/src/appDom/index.ts @@ -714,13 +714,13 @@ export function setQueryProp( export function setNodeNamespacedProp< Node extends AppDomNode, Namespace extends PropNamespaces, - Prop extends keyof Node[Namespace] & string, + Prop extends keyof NonNullable & string, >( dom: AppDom, node: Node, namespace: Namespace, prop: Prop, - value: Node[Namespace][Prop] | null, + value: NonNullable[Prop] | null, ): AppDom { if (value) { return update(dom, { diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index 46cd9b9b469..f53723d8bc5 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -936,7 +936,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { const setNewElementLayout = ( draft: appDom.AppDom, - elementParent: appDom.PageNode | appDom.ElementNode, + elementParent: appDom.ParentOf, ) => { const draggedNodeParent = selection ? appDom.getParent(draft, draggedNode) : null; if ( @@ -944,7 +944,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { draggedNodeParent && draggedNodeParent.id !== elementParent.id ) { - appDom.setNodeNamespacedProp( + draft = appDom.setNodeNamespacedProp( draft, draggedNode, 'layout', From 28d579944e794ae4dbc9f4f7351c6c32f7f87f03 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 23 Nov 2022 22:28:58 +0000 Subject: [PATCH 04/17] Try not using nested functions --- packages/toolpad-app/src/appDom/index.ts | 2 +- .../PageEditor/RenderPanel/RenderOverlay.tsx | 461 ++++++++++-------- .../toolpad-app/src/toolpad/DomLoader.tsx | 8 +- 3 files changed, 250 insertions(+), 221 deletions(-) diff --git a/packages/toolpad-app/src/appDom/index.ts b/packages/toolpad-app/src/appDom/index.ts index 766b08f12ff..f440dc2d9e9 100644 --- a/packages/toolpad-app/src/appDom/index.ts +++ b/packages/toolpad-app/src/appDom/index.ts @@ -736,7 +736,7 @@ export function setNodeNamespacedProp< return update(dom, { nodes: update(dom.nodes, { [node.id]: update(node, { - [namespace]: omit(node[namespace], prop) as Partial, + [namespace]: omit(node[namespace]!, prop) as Partial, } as Partial), }), }); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index f53723d8bc5..26c17c004e8 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -210,19 +210,19 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { const deleteOrphanedLayoutComponents = React.useCallback( ( - draft: appDom.AppDom, + updatedDom: appDom.AppDom, movedOrDeletedNode: appDom.ElementNode, moveTargetNodeId: NodeId | null = null, ) => { const movedOrDeletedNodeParentProp = movedOrDeletedNode.parentProp; - const parent = appDom.getParent(draft, movedOrDeletedNode); - const parentParent = parent && appDom.getParent(draft, parent); - const parentParentParent = parentParent && appDom.getParent(draft, parentParent); + const parent = appDom.getParent(updatedDom, movedOrDeletedNode); + const parentParent = parent && appDom.getParent(updatedDom, parent); + const parentParentParent = parentParent && appDom.getParent(updatedDom, parentParent); const parentChildren = parent && movedOrDeletedNodeParentProp - ? (appDom.getChildNodes(draft, parent) as appDom.NodeChildren)[ + ? (appDom.getChildNodes(updatedDom, parent) as appDom.NodeChildren)[ movedOrDeletedNodeParentProp ] : []; @@ -238,7 +238,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { parent.parentProp && appDom.isElement(parentParent) && isPageLayoutComponent(parentParent) && - appDom.getChildNodes(draft, parentParent)[parent.parentProp].length === 1; + appDom.getChildNodes(updatedDom, parentParent)[parent.parentProp].length === 1; const isSecondLastLayoutContainerChild = parent && @@ -264,8 +264,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { moveTargetNodeId !== lastContainerChild.id && isPageLayoutComponent(parentParent) ) { - draft = appDom.moveNode( - draft, + updatedDom = appDom.moveNode( + updatedDom, lastContainerChild, parentParent, lastContainerChild.parentProp, @@ -273,8 +273,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); if (isPageColumn(parent)) { - draft = appDom.setNodeNamespacedProp( - draft, + updatedDom = appDom.setNodeNamespacedProp( + updatedDom, lastContainerChild, 'layout', 'columnSize', @@ -282,7 +282,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); } - draft = appDom.removeNode(draft, parent.id); + updatedDom = appDom.removeNode(updatedDom, parent.id); } if ( @@ -293,8 +293,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { moveTargetNodeId !== parentParent.id && moveTargetNodeId !== lastContainerChild.id ) { - draft = appDom.moveNode( - draft, + updatedDom = appDom.moveNode( + updatedDom, lastContainerChild, parentParentParent, lastContainerChild.parentProp, @@ -302,8 +302,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); if (isPageColumn(parentParent)) { - draft = appDom.setNodeNamespacedProp( - draft, + updatedDom = appDom.setNodeNamespacedProp( + updatedDom, lastContainerChild, 'layout', 'columnSize', @@ -311,21 +311,21 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); } - draft = appDom.removeNode(draft, parentParent.id); + updatedDom = appDom.removeNode(updatedDom, parentParent.id); } } } } if (isOnlyLayoutContainerChild) { - draft = appDom.removeNode(draft, parent.id); + updatedDom = appDom.removeNode(updatedDom, parent.id); if (isParentOnlyLayoutContainerChild && moveTargetNodeId !== parentParent.id) { - draft = appDom.removeNode(draft, parentParent.id); + updatedDom = appDom.removeNode(updatedDom, parentParent.id); } } - return draft; + return updatedDom; }, [], ); @@ -336,21 +336,21 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { event.stopPropagation(); } - domApi.update((draft): appDom.AppDom => { - const toRemove = appDom.getNode(draft, nodeId); + let updatedDom = dom; - draft = appDom.removeNode(draft, toRemove.id); + const toRemove = appDom.getNode(updatedDom, nodeId); - if (appDom.isElement(toRemove)) { - draft = deleteOrphanedLayoutComponents(draft, toRemove); - } + updatedDom = appDom.removeNode(updatedDom, toRemove.id); - return draft; - }); + if (appDom.isElement(toRemove)) { + updatedDom = deleteOrphanedLayoutComponents(updatedDom, toRemove); + } + + domApi.update(updatedDom); api.deselect(); }, - [domApi, api, deleteOrphanedLayoutComponents], + [dom, domApi, api, deleteOrphanedLayoutComponents], ); const selectedRect = selectedNode ? nodesInfo[selectedNode.id]?.rect : null; @@ -897,26 +897,36 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { addOrMoveNode = appDom.moveNode; } + let updatedDom = dom; + // Drop on page or layout slot if (isDraggingOverPage || isDraggingOverLayoutSlot) { - domApi.update((draft): appDom.AppDom => { - const newParentIndex = - dragOverZone === DROP_ZONE_TOP - ? appDom.getNewFirstParentIndexInNode(draft, dragOverNode, 'children') - : appDom.getNewLastParentIndexInNode(draft, dragOverNode, 'children'); - - if (!isPageRow(draggedNode)) { - const rowContainer = appDom.createElement(draft, PAGE_ROW_COMPONENT_ID, {}); - draft = appDom.addNode(draft, rowContainer, dragOverNode, 'children', newParentIndex); - parent = rowContainer; - - draft = addOrMoveNode(draft, draggedNode, rowContainer, 'children'); - } else { - draft = addOrMoveNode(draft, draggedNode, dragOverNode, 'children', newParentIndex); - } - - return draft; - }); + const newParentIndex = + dragOverZone === DROP_ZONE_TOP + ? appDom.getNewFirstParentIndexInNode(updatedDom, dragOverNode, 'children') + : appDom.getNewLastParentIndexInNode(updatedDom, dragOverNode, 'children'); + + if (!isPageRow(draggedNode)) { + const rowContainer = appDom.createElement(updatedDom, PAGE_ROW_COMPONENT_ID, {}); + updatedDom = appDom.addNode( + updatedDom, + rowContainer, + dragOverNode, + 'children', + newParentIndex, + ); + parent = rowContainer; + + updatedDom = addOrMoveNode(updatedDom, draggedNode, rowContainer, 'children'); + } else { + updatedDom = addOrMoveNode( + updatedDom, + draggedNode, + dragOverNode, + 'children', + newParentIndex, + ); + } } if ( @@ -934,227 +944,246 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ? isVerticalFlow(dragOverSlot.flowDirection) : false; - const setNewElementLayout = ( - draft: appDom.AppDom, - elementParent: appDom.ParentOf, - ) => { - const draggedNodeParent = selection ? appDom.getParent(draft, draggedNode) : null; - if ( - draggedNode.layout?.columnSize && - draggedNodeParent && - draggedNodeParent.id !== elementParent.id - ) { - draft = appDom.setNodeNamespacedProp( - draft, - draggedNode, - 'layout', - 'columnSize', - appDom.createConst(1), - ); - } - - return draft; - }; - if (dragOverZone === DROP_ZONE_CENTER && dragOverSlotParentProp) { - domApi.update((draft): appDom.AppDom => { - draft = addOrMoveNode(draft, draggedNode, dragOverNode, dragOverSlotParentProp); - - draft = setNewElementLayout(draft, parent); - - if (selection) { - draft = deleteOrphanedLayoutComponents(draft, draggedNode, dragOverNodeId); - } - - return draft; - }); + updatedDom = addOrMoveNode(updatedDom, draggedNode, dragOverNode, dragOverSlotParentProp); } if ([DROP_ZONE_TOP, DROP_ZONE_BOTTOM].includes(dragOverZone)) { - domApi.update((draft): appDom.AppDom => { - if (!isDraggingOverVerticalContainer) { - const newParentIndex = - dragOverZone === DROP_ZONE_TOP - ? appDom.getNewParentIndexBeforeNode(draft, dragOverNode, dragOverNodeParentProp) - : appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp); - - if (isDraggingOverRow && !isPageRow(draggedNode)) { - if (isOriginalParentPage) { - const rowContainer = appDom.createElement(draft, PAGE_ROW_COMPONENT_ID, {}); - draft = appDom.addNode( - draft, - rowContainer, - parent, + if (!isDraggingOverVerticalContainer) { + const newParentIndex = + dragOverZone === DROP_ZONE_TOP + ? appDom.getNewParentIndexBeforeNode( + updatedDom, + dragOverNode, dragOverNodeParentProp, - newParentIndex, - ); - parent = rowContainer; - - draft = addOrMoveNode(draft, draggedNode, parent, dragOverNodeParentProp); - } else { - draft = addOrMoveNode( - draft, - draggedNode, - parent, + ) + : appDom.getNewParentIndexAfterNode( + updatedDom, + dragOverNode, dragOverNodeParentProp, - newParentIndex, ); - } - } - - if (isOriginalParentRow) { - const columnContainer = appDom.createElement( - draft, - PAGE_COLUMN_COMPONENT_ID, - {}, - { - columnSize: dragOverNode.layout?.columnSize || appDom.createConst(1), - }, - ); - - draft = appDom.setNodeNamespacedProp( - draft, - dragOverNode, - 'layout', - 'columnSize', - appDom.createConst(1), - ); - draft = appDom.addNode( - draft, - columnContainer, + if (isDraggingOverRow && !isPageRow(draggedNode)) { + if (isOriginalParentPage) { + const rowContainer = appDom.createElement(updatedDom, PAGE_ROW_COMPONENT_ID, {}); + updatedDom = appDom.addNode( + updatedDom, + rowContainer, parent, dragOverNodeParentProp, - appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp), + newParentIndex, ); - parent = columnContainer; - - // Move existing element inside column right away if drag over zone is bottom - if (dragOverZone === DROP_ZONE_BOTTOM) { - draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); - } - } + parent = rowContainer; - if (!isDraggingOverRow || isPageRow(draggedNode)) { - draft = addOrMoveNode( - draft, + updatedDom = addOrMoveNode(updatedDom, draggedNode, parent, dragOverNodeParentProp); + } else { + updatedDom = addOrMoveNode( + updatedDom, draggedNode, parent, dragOverNodeParentProp, newParentIndex, ); } - - // Only move existing element inside column in the end if drag over zone is top - if ( - isOriginalParentRow && - !isDraggingOverVerticalContainer && - dragOverZone === DROP_ZONE_TOP - ) { - draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); - } } - if (dragOverSlotParentProp && isDraggingOverVerticalContainer) { - const isDraggingOverDirectionStart = - dragOverZone === - (dragOverSlot?.flowDirection === 'column' ? DROP_ZONE_TOP : DROP_ZONE_BOTTOM); + if (isOriginalParentRow) { + const columnContainer = appDom.createElement( + updatedDom, + PAGE_COLUMN_COMPONENT_ID, + {}, + { + columnSize: dragOverNode.layout?.columnSize || appDom.createConst(1), + }, + ); - const newParentIndex = isDraggingOverDirectionStart - ? appDom.getNewFirstParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp) - : appDom.getNewLastParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp); + updatedDom = appDom.setNodeNamespacedProp( + updatedDom, + dragOverNode, + 'layout', + 'columnSize', + appDom.createConst(1), + ); - draft = addOrMoveNode( - draft, + updatedDom = appDom.addNode( + updatedDom, + columnContainer, + parent, + dragOverNodeParentProp, + appDom.getNewParentIndexAfterNode(updatedDom, dragOverNode, dragOverNodeParentProp), + ); + parent = columnContainer; + + // Move existing element inside column right away if drag over zone is bottom + if (dragOverZone === DROP_ZONE_BOTTOM) { + updatedDom = appDom.moveNode( + updatedDom, + dragOverNode, + parent, + dragOverNodeParentProp, + ); + } + } + + if (!isDraggingOverRow || isPageRow(draggedNode)) { + updatedDom = addOrMoveNode( + updatedDom, draggedNode, - dragOverNode, - dragOverSlotParentProp, + parent, + dragOverNodeParentProp, newParentIndex, ); } - draft = setNewElementLayout(draft, parent); - - if (selection) { - draft = deleteOrphanedLayoutComponents(draft, draggedNode, dragOverNodeId); + // Only move existing element inside column in the end if drag over zone is top + if ( + isOriginalParentRow && + !isDraggingOverVerticalContainer && + dragOverZone === DROP_ZONE_TOP + ) { + updatedDom = appDom.moveNode( + updatedDom, + dragOverNode, + parent, + dragOverNodeParentProp, + ); } + } - return draft; - }); - } + if (dragOverSlotParentProp && isDraggingOverVerticalContainer) { + const isDraggingOverDirectionStart = + dragOverZone === + (dragOverSlot?.flowDirection === 'column' ? DROP_ZONE_TOP : DROP_ZONE_BOTTOM); - if ([DROP_ZONE_RIGHT, DROP_ZONE_LEFT].includes(dragOverZone)) { - domApi.update((draft): appDom.AppDom => { - if (!isDraggingOverHorizontalContainer) { - if (isOriginalParentColumn) { - const rowContainer = appDom.createElement(draft, PAGE_ROW_COMPONENT_ID, { - justifyContent: appDom.createConst( - originalParentInfo?.props.alignItems || 'start', - ), - }); - draft = appDom.addNode( - draft, - rowContainer, - parent, - dragOverNodeParentProp, - appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp), + const newParentIndex = isDraggingOverDirectionStart + ? appDom.getNewFirstParentIndexInNode( + updatedDom, + dragOverNode, + dragOverSlotParentProp, + ) + : appDom.getNewLastParentIndexInNode( + updatedDom, + dragOverNode, + dragOverSlotParentProp, ); - parent = rowContainer; - - // Move existing element inside right away if drag over zone is right - if (dragOverZone === DROP_ZONE_RIGHT) { - draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); - } - } - const newParentIndex = - dragOverZone === DROP_ZONE_RIGHT - ? appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp) - : appDom.getNewParentIndexBeforeNode(draft, dragOverNode, dragOverNodeParentProp); + updatedDom = addOrMoveNode( + updatedDom, + draggedNode, + dragOverNode, + dragOverSlotParentProp, + newParentIndex, + ); + } + } - draft = addOrMoveNode( - draft, - draggedNode, + if ([DROP_ZONE_RIGHT, DROP_ZONE_LEFT].includes(dragOverZone)) { + if (!isDraggingOverHorizontalContainer) { + if (isOriginalParentColumn) { + const rowContainer = appDom.createElement(updatedDom, PAGE_ROW_COMPONENT_ID, { + justifyContent: appDom.createConst(originalParentInfo?.props.alignItems || 'start'), + }); + updatedDom = appDom.addNode( + updatedDom, + rowContainer, parent, dragOverNodeParentProp, - newParentIndex, + appDom.getNewParentIndexAfterNode(updatedDom, dragOverNode, dragOverNodeParentProp), ); + parent = rowContainer; - // Only move existing element inside column in the end if drag over zone is left - if (isOriginalParentColumn && dragOverZone === DROP_ZONE_LEFT) { - draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); + // Move existing element inside right away if drag over zone is right + if (dragOverZone === DROP_ZONE_RIGHT) { + updatedDom = appDom.moveNode( + updatedDom, + dragOverNode, + parent, + dragOverNodeParentProp, + ); } } - if (dragOverSlotParentProp && isDraggingOverHorizontalContainer) { - const isDraggingOverDirectionStart = - dragOverZone === - (dragOverSlot?.flowDirection === 'row' ? DROP_ZONE_LEFT : DROP_ZONE_RIGHT); + const newParentIndex = + dragOverZone === DROP_ZONE_RIGHT + ? appDom.getNewParentIndexAfterNode( + updatedDom, + dragOverNode, + dragOverNodeParentProp, + ) + : appDom.getNewParentIndexBeforeNode( + updatedDom, + dragOverNode, + dragOverNodeParentProp, + ); - const newParentIndex = isDraggingOverDirectionStart - ? appDom.getNewFirstParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp) - : appDom.getNewLastParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp); + updatedDom = addOrMoveNode( + updatedDom, + draggedNode, + parent, + dragOverNodeParentProp, + newParentIndex, + ); - draft = addOrMoveNode( - draft, - draggedNode, + // Only move existing element inside column in the end if drag over zone is left + if (isOriginalParentColumn && dragOverZone === DROP_ZONE_LEFT) { + updatedDom = appDom.moveNode( + updatedDom, dragOverNode, - dragOverSlotParentProp, - newParentIndex, + parent, + dragOverNodeParentProp, ); } + } - draft = setNewElementLayout(draft, parent); + if (dragOverSlotParentProp && isDraggingOverHorizontalContainer) { + const isDraggingOverDirectionStart = + dragOverZone === + (dragOverSlot?.flowDirection === 'row' ? DROP_ZONE_LEFT : DROP_ZONE_RIGHT); - if (selection) { - draft = deleteOrphanedLayoutComponents(draft, draggedNode, dragOverNodeId); - } + const newParentIndex = isDraggingOverDirectionStart + ? appDom.getNewFirstParentIndexInNode( + updatedDom, + dragOverNode, + dragOverSlotParentProp, + ) + : appDom.getNewLastParentIndexInNode( + updatedDom, + dragOverNode, + dragOverSlotParentProp, + ); - return draft; - }); + updatedDom = addOrMoveNode( + updatedDom, + draggedNode, + dragOverNode, + dragOverSlotParentProp, + newParentIndex, + ); + } + } + + const draggedNodeParent = selection ? appDom.getParent(dom, draggedNode) : null; + if ( + draggedNode.layout?.columnSize && + draggedNodeParent && + draggedNodeParent.id !== parent.id + ) { + updatedDom = appDom.setNodeNamespacedProp( + updatedDom, + draggedNode, + 'layout', + 'columnSize', + appDom.createConst(1), + ); } } + if (selection) { + updatedDom = deleteOrphanedLayoutComponents(updatedDom, draggedNode, dragOverNodeId); + } + + domApi.update(updatedDom); + api.dragEnd(); if (newNode) { diff --git a/packages/toolpad-app/src/toolpad/DomLoader.tsx b/packages/toolpad-app/src/toolpad/DomLoader.tsx index b25dd7a3074..0f1a7a85844 100644 --- a/packages/toolpad-app/src/toolpad/DomLoader.tsx +++ b/packages/toolpad-app/src/toolpad/DomLoader.tsx @@ -53,7 +53,7 @@ export type DomAction = } | { type: 'DOM_UPDATE'; - updater: (dom: appDom.AppDom) => appDom.AppDom; + updatedDom: appDom.AppDom; } | { type: 'DOM_ADD_NODE'; @@ -109,7 +109,7 @@ export function domReducer(dom: appDom.AppDom, action: DomAction): appDom.AppDom return appDom.setNodeNamespace(dom, action.node, action.namespace, action.value); } case 'DOM_UPDATE': { - return action.updater(dom); + return action.updatedDom; } case 'DOM_ADD_NODE': { return appDom.addNode( @@ -256,10 +256,10 @@ function createDomApi(dispatch: React.Dispatch) { setNodeName(nodeId: NodeId, name: string) { dispatch({ type: 'DOM_SET_NODE_NAME', nodeId, name }); }, - update(updater: (dom: appDom.AppDom) => appDom.AppDom) { + update(dom: appDom.AppDom) { dispatch({ type: 'DOM_UPDATE', - updater, + updatedDom: dom, }); }, addNode( From 6b332f7393ab4ee127308960d37205ee70a560ff Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 23 Nov 2022 22:56:25 +0000 Subject: [PATCH 05/17] Atomic column normalization --- .../PageEditor/RenderPanel/RenderOverlay.tsx | 521 ++++++++---------- 1 file changed, 238 insertions(+), 283 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index 26c17c004e8..ada89aa88e6 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -134,6 +134,124 @@ function getDropAreaParentProp(dropAreaId: string): string | null { return dropAreaId.split(':')[1] || null; } +function deleteOrphanedLayoutComponents( + draftDom: appDom.AppDom, + movedOrDeletedNode: appDom.ElementNode, + moveTargetNodeId: NodeId | null = null, +): appDom.AppDom { + const movedOrDeletedNodeParentProp = movedOrDeletedNode.parentProp; + + const parent = appDom.getParent(draftDom, movedOrDeletedNode); + const parentParent = parent && appDom.getParent(draftDom, parent); + const parentParentParent = parentParent && appDom.getParent(draftDom, parentParent); + + const parentChildren = + parent && movedOrDeletedNodeParentProp + ? (appDom.getChildNodes(draftDom, parent) as appDom.NodeChildren)[ + movedOrDeletedNodeParentProp + ] + : []; + + const isOnlyLayoutContainerChild = + parent && + appDom.isElement(parent) && + isPageLayoutComponent(parent) && + parentChildren.length === 1; + + const isParentOnlyLayoutContainerChild = + parentParent && + parent.parentProp && + appDom.isElement(parentParent) && + isPageLayoutComponent(parentParent) && + appDom.getChildNodes(draftDom, parentParent)[parent.parentProp].length === 1; + + const isSecondLastLayoutContainerChild = + parent && + appDom.isElement(parent) && + isPageLayoutComponent(parent) && + parentChildren.length === 2; + + const hasNoLayoutContainerSiblings = + parentChildren.filter( + (child) => child.id !== movedOrDeletedNode.id && (isPageRow(child) || isPageColumn(child)), + ).length === 0; + + if (isSecondLastLayoutContainerChild && hasNoLayoutContainerSiblings) { + if (parent.parentIndex && parentParent && appDom.isElement(parentParent)) { + const lastContainerChild = parentChildren.filter( + (child) => child.id !== movedOrDeletedNode.id, + )[0]; + + if (lastContainerChild.parentProp) { + if ( + moveTargetNodeId !== parent.id && + moveTargetNodeId !== lastContainerChild.id && + isPageLayoutComponent(parentParent) + ) { + draftDom = appDom.moveNode( + draftDom, + lastContainerChild, + parentParent, + lastContainerChild.parentProp, + parent.parentIndex, + ); + + if (isPageColumn(parent)) { + draftDom = appDom.setNodeNamespacedProp( + draftDom, + lastContainerChild, + 'layout', + 'columnSize', + parent.layout?.columnSize || appDom.createConst(1), + ); + } + + draftDom = appDom.removeNode(draftDom, parent.id); + } + + if ( + parentParent.parentIndex && + parentParentParent && + appDom.isElement(parentParentParent) && + isParentOnlyLayoutContainerChild && + moveTargetNodeId !== parentParent.id && + moveTargetNodeId !== lastContainerChild.id + ) { + draftDom = appDom.moveNode( + draftDom, + lastContainerChild, + parentParentParent, + lastContainerChild.parentProp, + parentParent.parentIndex, + ); + + if (isPageColumn(parentParent)) { + draftDom = appDom.setNodeNamespacedProp( + draftDom, + lastContainerChild, + 'layout', + 'columnSize', + parentParent.layout?.columnSize || appDom.createConst(1), + ); + } + + draftDom = appDom.removeNode(draftDom, parentParent.id); + } + } + } + } + + if (isOnlyLayoutContainerChild) { + draftDom = appDom.removeNode(draftDom, parent.id); + + if (isParentOnlyLayoutContainerChild && moveTargetNodeId !== parentParent.id) { + draftDom = appDom.removeNode(draftDom, parentParent.id); + } + } + + return draftDom; +} + interface RenderOverlayProps { canvasHostRef: React.RefObject; } @@ -188,6 +306,55 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { return rects; }, [nodesInfo, pageNodes]); + const previousRowColumnCountsRef = React.useRef>({}); + + const normalizePageRowColumnSizes = React.useCallback( + (draftDom: appDom.AppDom): appDom.AppDom => { + pageNodes.forEach((node: appDom.AppDomNode) => { + if (appDom.isElement(node) && isPageRow(node)) { + const nodeChildren = appDom.getChildNodes(dom, node).children; + const childrenCount = nodeChildren?.length || 0; + + if (childrenCount > 0 && childrenCount < previousRowColumnCountsRef.current[node.id]) { + const layoutColumnSizes = nodeChildren.map( + (child) => child.layout?.columnSize?.value || 1, + ); + const totalLayoutColumnSizes = layoutColumnSizes.reduce((acc, size) => acc + size, 0); + + const normalizedLayoutColumnSizes = layoutColumnSizes.map( + (size) => (size * nodeChildren.length) / totalLayoutColumnSizes, + ); + + nodeChildren.forEach((child, childIndex) => { + if (child.layout?.columnSize) { + draftDom = appDom.setNodeNamespacedProp( + draftDom, + child, + 'layout', + 'columnSize', + appDom.createConst(normalizedLayoutColumnSizes[childIndex]), + ); + } + }); + } + + previousRowColumnCountsRef.current[node.id] = childrenCount; + } + }); + + return draftDom; + }, + [dom, pageNodes], + ); + + const updateDom = React.useCallback( + (draftDom: appDom.AppDom) => { + draftDom = normalizePageRowColumnSizes(draftDom); + domApi.update(draftDom); + }, + [domApi, normalizePageRowColumnSizes], + ); + const handleNodeMouseUp = React.useCallback( (event: React.MouseEvent) => { const cursorPos = canvasHostRef.current?.getViewCoordinates(event.clientX, event.clientY); @@ -208,149 +375,27 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { [canvasHostRef, draggedNodeId, selectionRects, dom, api], ); - const deleteOrphanedLayoutComponents = React.useCallback( - ( - updatedDom: appDom.AppDom, - movedOrDeletedNode: appDom.ElementNode, - moveTargetNodeId: NodeId | null = null, - ) => { - const movedOrDeletedNodeParentProp = movedOrDeletedNode.parentProp; - - const parent = appDom.getParent(updatedDom, movedOrDeletedNode); - const parentParent = parent && appDom.getParent(updatedDom, parent); - const parentParentParent = parentParent && appDom.getParent(updatedDom, parentParent); - - const parentChildren = - parent && movedOrDeletedNodeParentProp - ? (appDom.getChildNodes(updatedDom, parent) as appDom.NodeChildren)[ - movedOrDeletedNodeParentProp - ] - : []; - - const isOnlyLayoutContainerChild = - parent && - appDom.isElement(parent) && - isPageLayoutComponent(parent) && - parentChildren.length === 1; - - const isParentOnlyLayoutContainerChild = - parentParent && - parent.parentProp && - appDom.isElement(parentParent) && - isPageLayoutComponent(parentParent) && - appDom.getChildNodes(updatedDom, parentParent)[parent.parentProp].length === 1; - - const isSecondLastLayoutContainerChild = - parent && - appDom.isElement(parent) && - isPageLayoutComponent(parent) && - parentChildren.length === 2; - - const hasNoLayoutContainerSiblings = - parentChildren.filter( - (child) => - child.id !== movedOrDeletedNode.id && (isPageRow(child) || isPageColumn(child)), - ).length === 0; - - if (isSecondLastLayoutContainerChild && hasNoLayoutContainerSiblings) { - if (parent.parentIndex && parentParent && appDom.isElement(parentParent)) { - const lastContainerChild = parentChildren.filter( - (child) => child.id !== movedOrDeletedNode.id, - )[0]; - - if (lastContainerChild.parentProp) { - if ( - moveTargetNodeId !== parent.id && - moveTargetNodeId !== lastContainerChild.id && - isPageLayoutComponent(parentParent) - ) { - updatedDom = appDom.moveNode( - updatedDom, - lastContainerChild, - parentParent, - lastContainerChild.parentProp, - parent.parentIndex, - ); - - if (isPageColumn(parent)) { - updatedDom = appDom.setNodeNamespacedProp( - updatedDom, - lastContainerChild, - 'layout', - 'columnSize', - parent.layout?.columnSize || appDom.createConst(1), - ); - } - - updatedDom = appDom.removeNode(updatedDom, parent.id); - } - - if ( - parentParent.parentIndex && - parentParentParent && - appDom.isElement(parentParentParent) && - isParentOnlyLayoutContainerChild && - moveTargetNodeId !== parentParent.id && - moveTargetNodeId !== lastContainerChild.id - ) { - updatedDom = appDom.moveNode( - updatedDom, - lastContainerChild, - parentParentParent, - lastContainerChild.parentProp, - parentParent.parentIndex, - ); - - if (isPageColumn(parentParent)) { - updatedDom = appDom.setNodeNamespacedProp( - updatedDom, - lastContainerChild, - 'layout', - 'columnSize', - parentParent.layout?.columnSize || appDom.createConst(1), - ); - } - - updatedDom = appDom.removeNode(updatedDom, parentParent.id); - } - } - } - } - - if (isOnlyLayoutContainerChild) { - updatedDom = appDom.removeNode(updatedDom, parent.id); - - if (isParentOnlyLayoutContainerChild && moveTargetNodeId !== parentParent.id) { - updatedDom = appDom.removeNode(updatedDom, parentParent.id); - } - } - - return updatedDom; - }, - [], - ); - const handleNodeDelete = React.useCallback( (nodeId: NodeId) => (event?: React.MouseEvent) => { if (event) { event.stopPropagation(); } - let updatedDom = dom; + let draftDom = dom; - const toRemove = appDom.getNode(updatedDom, nodeId); + const toRemove = appDom.getNode(draftDom, nodeId); - updatedDom = appDom.removeNode(updatedDom, toRemove.id); + draftDom = appDom.removeNode(draftDom, toRemove.id); if (appDom.isElement(toRemove)) { - updatedDom = deleteOrphanedLayoutComponents(updatedDom, toRemove); + draftDom = deleteOrphanedLayoutComponents(draftDom, toRemove); } - domApi.update(updatedDom); + updateDom(draftDom); api.deselect(); }, - [dom, domApi, api, deleteOrphanedLayoutComponents], + [dom, updateDom, api], ); const selectedRect = selectedNode ? nodesInfo[selectedNode.id]?.rect : null; @@ -897,19 +942,19 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { addOrMoveNode = appDom.moveNode; } - let updatedDom = dom; + let draftDom = dom; // Drop on page or layout slot if (isDraggingOverPage || isDraggingOverLayoutSlot) { const newParentIndex = dragOverZone === DROP_ZONE_TOP - ? appDom.getNewFirstParentIndexInNode(updatedDom, dragOverNode, 'children') - : appDom.getNewLastParentIndexInNode(updatedDom, dragOverNode, 'children'); + ? appDom.getNewFirstParentIndexInNode(draftDom, dragOverNode, 'children') + : appDom.getNewLastParentIndexInNode(draftDom, dragOverNode, 'children'); if (!isPageRow(draggedNode)) { - const rowContainer = appDom.createElement(updatedDom, PAGE_ROW_COMPONENT_ID, {}); - updatedDom = appDom.addNode( - updatedDom, + const rowContainer = appDom.createElement(draftDom, PAGE_ROW_COMPONENT_ID, {}); + draftDom = appDom.addNode( + draftDom, rowContainer, dragOverNode, 'children', @@ -917,15 +962,9 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); parent = rowContainer; - updatedDom = addOrMoveNode(updatedDom, draggedNode, rowContainer, 'children'); + draftDom = addOrMoveNode(draftDom, draggedNode, rowContainer, 'children'); } else { - updatedDom = addOrMoveNode( - updatedDom, - draggedNode, - dragOverNode, - 'children', - newParentIndex, - ); + draftDom = addOrMoveNode(draftDom, draggedNode, dragOverNode, 'children', newParentIndex); } } @@ -945,29 +984,21 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { : false; if (dragOverZone === DROP_ZONE_CENTER && dragOverSlotParentProp) { - updatedDom = addOrMoveNode(updatedDom, draggedNode, dragOverNode, dragOverSlotParentProp); + draftDom = addOrMoveNode(draftDom, draggedNode, dragOverNode, dragOverSlotParentProp); } if ([DROP_ZONE_TOP, DROP_ZONE_BOTTOM].includes(dragOverZone)) { if (!isDraggingOverVerticalContainer) { const newParentIndex = dragOverZone === DROP_ZONE_TOP - ? appDom.getNewParentIndexBeforeNode( - updatedDom, - dragOverNode, - dragOverNodeParentProp, - ) - : appDom.getNewParentIndexAfterNode( - updatedDom, - dragOverNode, - dragOverNodeParentProp, - ); + ? appDom.getNewParentIndexBeforeNode(draftDom, dragOverNode, dragOverNodeParentProp) + : appDom.getNewParentIndexAfterNode(draftDom, dragOverNode, dragOverNodeParentProp); if (isDraggingOverRow && !isPageRow(draggedNode)) { if (isOriginalParentPage) { - const rowContainer = appDom.createElement(updatedDom, PAGE_ROW_COMPONENT_ID, {}); - updatedDom = appDom.addNode( - updatedDom, + const rowContainer = appDom.createElement(draftDom, PAGE_ROW_COMPONENT_ID, {}); + draftDom = appDom.addNode( + draftDom, rowContainer, parent, dragOverNodeParentProp, @@ -975,10 +1006,10 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); parent = rowContainer; - updatedDom = addOrMoveNode(updatedDom, draggedNode, parent, dragOverNodeParentProp); + draftDom = addOrMoveNode(draftDom, draggedNode, parent, dragOverNodeParentProp); } else { - updatedDom = addOrMoveNode( - updatedDom, + draftDom = addOrMoveNode( + draftDom, draggedNode, parent, dragOverNodeParentProp, @@ -989,7 +1020,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { if (isOriginalParentRow) { const columnContainer = appDom.createElement( - updatedDom, + draftDom, PAGE_COLUMN_COMPONENT_ID, {}, { @@ -997,37 +1028,32 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { }, ); - updatedDom = appDom.setNodeNamespacedProp( - updatedDom, + draftDom = appDom.setNodeNamespacedProp( + draftDom, dragOverNode, 'layout', 'columnSize', appDom.createConst(1), ); - updatedDom = appDom.addNode( - updatedDom, + draftDom = appDom.addNode( + draftDom, columnContainer, parent, dragOverNodeParentProp, - appDom.getNewParentIndexAfterNode(updatedDom, dragOverNode, dragOverNodeParentProp), + appDom.getNewParentIndexAfterNode(draftDom, dragOverNode, dragOverNodeParentProp), ); parent = columnContainer; // Move existing element inside column right away if drag over zone is bottom if (dragOverZone === DROP_ZONE_BOTTOM) { - updatedDom = appDom.moveNode( - updatedDom, - dragOverNode, - parent, - dragOverNodeParentProp, - ); + draftDom = appDom.moveNode(draftDom, dragOverNode, parent, dragOverNodeParentProp); } } if (!isDraggingOverRow || isPageRow(draggedNode)) { - updatedDom = addOrMoveNode( - updatedDom, + draftDom = addOrMoveNode( + draftDom, draggedNode, parent, dragOverNodeParentProp, @@ -1041,12 +1067,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { !isDraggingOverVerticalContainer && dragOverZone === DROP_ZONE_TOP ) { - updatedDom = appDom.moveNode( - updatedDom, - dragOverNode, - parent, - dragOverNodeParentProp, - ); + draftDom = appDom.moveNode(draftDom, dragOverNode, parent, dragOverNodeParentProp); } } @@ -1056,19 +1077,11 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { (dragOverSlot?.flowDirection === 'column' ? DROP_ZONE_TOP : DROP_ZONE_BOTTOM); const newParentIndex = isDraggingOverDirectionStart - ? appDom.getNewFirstParentIndexInNode( - updatedDom, - dragOverNode, - dragOverSlotParentProp, - ) - : appDom.getNewLastParentIndexInNode( - updatedDom, - dragOverNode, - dragOverSlotParentProp, - ); + ? appDom.getNewFirstParentIndexInNode(draftDom, dragOverNode, dragOverSlotParentProp) + : appDom.getNewLastParentIndexInNode(draftDom, dragOverNode, dragOverSlotParentProp); - updatedDom = addOrMoveNode( - updatedDom, + draftDom = addOrMoveNode( + draftDom, draggedNode, dragOverNode, dragOverSlotParentProp, @@ -1080,44 +1093,35 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { if ([DROP_ZONE_RIGHT, DROP_ZONE_LEFT].includes(dragOverZone)) { if (!isDraggingOverHorizontalContainer) { if (isOriginalParentColumn) { - const rowContainer = appDom.createElement(updatedDom, PAGE_ROW_COMPONENT_ID, { + const rowContainer = appDom.createElement(draftDom, PAGE_ROW_COMPONENT_ID, { justifyContent: appDom.createConst(originalParentInfo?.props.alignItems || 'start'), }); - updatedDom = appDom.addNode( - updatedDom, + draftDom = appDom.addNode( + draftDom, rowContainer, parent, dragOverNodeParentProp, - appDom.getNewParentIndexAfterNode(updatedDom, dragOverNode, dragOverNodeParentProp), + appDom.getNewParentIndexAfterNode(draftDom, dragOverNode, dragOverNodeParentProp), ); parent = rowContainer; // Move existing element inside right away if drag over zone is right if (dragOverZone === DROP_ZONE_RIGHT) { - updatedDom = appDom.moveNode( - updatedDom, - dragOverNode, - parent, - dragOverNodeParentProp, - ); + draftDom = appDom.moveNode(draftDom, dragOverNode, parent, dragOverNodeParentProp); } } const newParentIndex = dragOverZone === DROP_ZONE_RIGHT - ? appDom.getNewParentIndexAfterNode( - updatedDom, - dragOverNode, - dragOverNodeParentProp, - ) + ? appDom.getNewParentIndexAfterNode(draftDom, dragOverNode, dragOverNodeParentProp) : appDom.getNewParentIndexBeforeNode( - updatedDom, + draftDom, dragOverNode, dragOverNodeParentProp, ); - updatedDom = addOrMoveNode( - updatedDom, + draftDom = addOrMoveNode( + draftDom, draggedNode, parent, dragOverNodeParentProp, @@ -1126,12 +1130,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { // Only move existing element inside column in the end if drag over zone is left if (isOriginalParentColumn && dragOverZone === DROP_ZONE_LEFT) { - updatedDom = appDom.moveNode( - updatedDom, - dragOverNode, - parent, - dragOverNodeParentProp, - ); + draftDom = appDom.moveNode(draftDom, dragOverNode, parent, dragOverNodeParentProp); } } @@ -1141,19 +1140,11 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { (dragOverSlot?.flowDirection === 'row' ? DROP_ZONE_LEFT : DROP_ZONE_RIGHT); const newParentIndex = isDraggingOverDirectionStart - ? appDom.getNewFirstParentIndexInNode( - updatedDom, - dragOverNode, - dragOverSlotParentProp, - ) - : appDom.getNewLastParentIndexInNode( - updatedDom, - dragOverNode, - dragOverSlotParentProp, - ); + ? appDom.getNewFirstParentIndexInNode(draftDom, dragOverNode, dragOverSlotParentProp) + : appDom.getNewLastParentIndexInNode(draftDom, dragOverNode, dragOverSlotParentProp); - updatedDom = addOrMoveNode( - updatedDom, + draftDom = addOrMoveNode( + draftDom, draggedNode, dragOverNode, dragOverSlotParentProp, @@ -1168,8 +1159,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { draggedNodeParent && draggedNodeParent.id !== parent.id ) { - updatedDom = appDom.setNodeNamespacedProp( - updatedDom, + draftDom = appDom.setNodeNamespacedProp( + draftDom, draggedNode, 'layout', 'columnSize', @@ -1179,10 +1170,10 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { } if (selection) { - updatedDom = deleteOrphanedLayoutComponents(updatedDom, draggedNode, dragOverNodeId); + draftDom = deleteOrphanedLayoutComponents(draftDom, draggedNode, dragOverNodeId); } - domApi.update(updatedDom); + updateDom(draftDom); api.dragEnd(); @@ -1199,9 +1190,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { api, availableDropZones, canvasHostRef, - deleteOrphanedLayoutComponents, dom, - domApi, dragOverNodeId, dragOverSlotParentProp, dragOverZone, @@ -1209,6 +1198,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { newNode, nodesInfo, selection, + updateDom, ], ); @@ -1243,50 +1233,6 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { getRightColumnEdges: () => [], }); - const normalizePageRowColumnSizes = React.useCallback( - (pageRowNode: appDom.ElementNode): number[] => { - const nodeChildren = appDom.getChildNodes(dom, pageRowNode).children; - - const layoutColumnSizes = nodeChildren.map((child) => child.layout?.columnSize?.value || 1); - const totalLayoutColumnSizes = layoutColumnSizes.reduce((acc, size) => acc + size, 0); - - const normalizedLayoutColumnSizes = layoutColumnSizes.map( - (size) => (size * nodeChildren.length) / totalLayoutColumnSizes, - ); - - nodeChildren.forEach((child, childIndex) => { - if (child.layout?.columnSize) { - domApi.setNodeNamespacedProp( - child, - 'layout', - 'columnSize', - appDom.createConst(normalizedLayoutColumnSizes[childIndex]), - ); - } - }); - - return normalizedLayoutColumnSizes; - }, - [dom, domApi], - ); - - const previousRowColumnCountsRef = React.useRef>({}); - - React.useEffect(() => { - pageNodes.forEach((node: appDom.AppDomNode) => { - if (appDom.isElement(node) && isPageRow(node)) { - const nodeChildren = appDom.getChildNodes(dom, node).children; - const childrenCount = nodeChildren?.length || 0; - - if (childrenCount > 0 && childrenCount < previousRowColumnCountsRef.current[node.id]) { - normalizePageRowColumnSizes(node); - } - - previousRowColumnCountsRef.current[node.id] = childrenCount; - } - }); - }, [dom, normalizePageRowColumnSizes, pageNodes]); - const handleEdgeDragOver = React.useCallback( (event: React.MouseEvent) => { if (!draggedNode) { @@ -1394,6 +1340,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { const resizePreviewRect = resizePreviewElement?.getBoundingClientRect(); if (draggedNodeRect && resizePreviewRect) { + let draftDom = dom; + if (draggedEdge === RECTANGLE_EDGE_LEFT || draggedEdge === RECTANGLE_EDGE_RIGHT) { const parentChildren = parent ? appDom.getChildNodes(dom, parent).children : []; const totalLayoutColumnSizes = parentChildren.reduce( @@ -1417,13 +1365,15 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { previousSiblingRect.width - (resizePreviewRect.width - draggedNodeRect.width), ); - domApi.setNodeNamespacedProp( + draftDom = appDom.setNodeNamespacedProp( + draftDom, draggedNode, 'layout', 'columnSize', appDom.createConst(updatedDraggedNodeColumnSize), ); - domApi.setNodeNamespacedProp( + draftDom = appDom.setNodeNamespacedProp( + draftDom, previousSibling, 'layout', 'columnSize', @@ -1445,13 +1395,15 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { nextSiblingRect.width - (resizePreviewRect.width - draggedNodeRect.width), ); - domApi.setNodeNamespacedProp( + draftDom = appDom.setNodeNamespacedProp( + draftDom, draggedNode, 'layout', 'columnSize', appDom.createConst(updatedDraggedNodeColumnSize), ); - domApi.setNodeNamespacedProp( + draftDom = appDom.setNodeNamespacedProp( + draftDom, nextSibling, 'layout', 'columnSize', @@ -1466,7 +1418,8 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { const resizableHeightProp = draggedNodeInfo?.componentConfig?.resizableHeightProp; if (resizableHeightProp) { - domApi.setNodeNamespacedProp( + draftDom = appDom.setNodeNamespacedProp( + draftDom, draggedNode, 'props', resizableHeightProp, @@ -1474,11 +1427,13 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { ); } } + + updateDom(draftDom); } api.dragEnd(); }, - [api, dom, domApi, draggedEdge, draggedNode, nodesInfo, resizePreviewElement], + [api, dom, draggedEdge, draggedNode, nodesInfo, resizePreviewElement, updateDom], ); return ( From 4c667d87d22d545152784123de0d0d918ad57d0b Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 23 Nov 2022 23:03:32 +0000 Subject: [PATCH 06/17] Use draft page nodes --- .../AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index ada89aa88e6..34112289b23 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -310,9 +310,11 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { const normalizePageRowColumnSizes = React.useCallback( (draftDom: appDom.AppDom): appDom.AppDom => { - pageNodes.forEach((node: appDom.AppDomNode) => { + const draftPageNodes = [pageNode, ...appDom.getDescendants(draftDom, pageNode)]; + + draftPageNodes.forEach((node: appDom.AppDomNode) => { if (appDom.isElement(node) && isPageRow(node)) { - const nodeChildren = appDom.getChildNodes(dom, node).children; + const nodeChildren = appDom.getChildNodes(draftDom, node).children; const childrenCount = nodeChildren?.length || 0; if (childrenCount > 0 && childrenCount < previousRowColumnCountsRef.current[node.id]) { @@ -344,7 +346,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { return draftDom; }, - [dom, pageNodes], + [pageNode], ); const updateDom = React.useCallback( From 65e7e7f3286fb5510eae2ecb12d80e765a5d9997 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 24 Nov 2022 15:42:33 +0000 Subject: [PATCH 07/17] Fix element removal --- .../PageEditor/RenderPanel/RenderOverlay.tsx | 93 ++++++++++++------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index 34112289b23..3834f729810 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -134,11 +134,20 @@ function getDropAreaParentProp(dropAreaId: string): string | null { return dropAreaId.split(':')[1] || null; } -function deleteOrphanedLayoutComponents( +function removeMaybeNode(dom: appDom.AppDom, nodeId: NodeId): appDom.AppDom { + if (appDom.getMaybeNode(dom, nodeId)) { + return appDom.removeNode(dom, nodeId); + } + return dom; +} + +function deleteOrphanedLayoutNodes( draftDom: appDom.AppDom, movedOrDeletedNode: appDom.ElementNode, moveTargetNodeId: NodeId | null = null, ): appDom.AppDom { + let orphanedLayoutNodeIds: NodeId[] = []; + const movedOrDeletedNodeParentProp = movedOrDeletedNode.parentProp; const parent = appDom.getParent(draftDom, movedOrDeletedNode); @@ -184,71 +193,75 @@ function deleteOrphanedLayoutComponents( if (lastContainerChild.parentProp) { if ( - moveTargetNodeId !== parent.id && - moveTargetNodeId !== lastContainerChild.id && - isPageLayoutComponent(parentParent) + parentParent.parentIndex && + parentParentParent && + appDom.isElement(parentParentParent) && + isParentOnlyLayoutContainerChild && + moveTargetNodeId !== parentParent.id && + moveTargetNodeId !== lastContainerChild.id ) { draftDom = appDom.moveNode( draftDom, lastContainerChild, - parentParent, + parentParentParent, lastContainerChild.parentProp, - parent.parentIndex, + parentParent.parentIndex, ); - if (isPageColumn(parent)) { + if (isPageColumn(parentParent)) { draftDom = appDom.setNodeNamespacedProp( draftDom, lastContainerChild, 'layout', 'columnSize', - parent.layout?.columnSize || appDom.createConst(1), + parentParent.layout?.columnSize || appDom.createConst(1), ); } - draftDom = appDom.removeNode(draftDom, parent.id); + orphanedLayoutNodeIds = [...orphanedLayoutNodeIds, parentParent.id]; } if ( - parentParent.parentIndex && - parentParentParent && - appDom.isElement(parentParentParent) && - isParentOnlyLayoutContainerChild && - moveTargetNodeId !== parentParent.id && - moveTargetNodeId !== lastContainerChild.id + moveTargetNodeId !== parent.id && + moveTargetNodeId !== lastContainerChild.id && + isPageLayoutComponent(parentParent) ) { draftDom = appDom.moveNode( draftDom, lastContainerChild, - parentParentParent, + parentParent, lastContainerChild.parentProp, - parentParent.parentIndex, + parent.parentIndex, ); - if (isPageColumn(parentParent)) { + if (isPageColumn(parent)) { draftDom = appDom.setNodeNamespacedProp( draftDom, lastContainerChild, 'layout', 'columnSize', - parentParent.layout?.columnSize || appDom.createConst(1), + parent.layout?.columnSize || appDom.createConst(1), ); } - draftDom = appDom.removeNode(draftDom, parentParent.id); + orphanedLayoutNodeIds = [...orphanedLayoutNodeIds, parent.id]; } } } } if (isOnlyLayoutContainerChild) { - draftDom = appDom.removeNode(draftDom, parent.id); - if (isParentOnlyLayoutContainerChild && moveTargetNodeId !== parentParent.id) { - draftDom = appDom.removeNode(draftDom, parentParent.id); + orphanedLayoutNodeIds = [...orphanedLayoutNodeIds, parentParent.id]; } + + orphanedLayoutNodeIds = [...orphanedLayoutNodeIds, parent.id]; } + orphanedLayoutNodeIds.forEach((nodeId) => { + draftDom = removeMaybeNode(draftDom, nodeId); + }); + return draftDom; } @@ -377,27 +390,34 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { [canvasHostRef, draggedNodeId, selectionRects, dom, api], ); - const handleNodeDelete = React.useCallback( - (nodeId: NodeId) => (event?: React.MouseEvent) => { - if (event) { - event.stopPropagation(); - } - + const getDomWithRemovedNode = React.useCallback( + (removedNodeId: NodeId, moveTargetNodeId: NodeId | null = null) => { let draftDom = dom; - const toRemove = appDom.getNode(draftDom, nodeId); - - draftDom = appDom.removeNode(draftDom, toRemove.id); + const toRemove = appDom.getNode(draftDom, removedNodeId); if (appDom.isElement(toRemove)) { - draftDom = deleteOrphanedLayoutComponents(draftDom, toRemove); + draftDom = deleteOrphanedLayoutNodes(draftDom, toRemove, moveTargetNodeId); + draftDom = removeMaybeNode(draftDom, toRemove.id); } - updateDom(draftDom); + return draftDom; + }, + [dom], + ); + + const handleNodeDelete = React.useCallback( + (nodeId: NodeId) => (event?: React.MouseEvent) => { + if (event) { + event.stopPropagation(); + } + + const updatedDom = getDomWithRemovedNode(nodeId); + updateDom(updatedDom); api.deselect(); }, - [dom, updateDom, api], + [getDomWithRemovedNode, updateDom, api], ); const selectedRect = selectedNode ? nodesInfo[selectedNode.id]?.rect : null; @@ -1172,7 +1192,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { } if (selection) { - draftDom = deleteOrphanedLayoutComponents(draftDom, draggedNode, dragOverNodeId); + draftDom = getDomWithRemovedNode(draggedNode.id, dragOverNodeId); } updateDom(draftDom); @@ -1197,6 +1217,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { dragOverSlotParentProp, dragOverZone, draggedNode, + getDomWithRemovedNode, newNode, nodesInfo, selection, From ea42bfbb8012c295e324dda2ed23b2c9b19bd762 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 24 Nov 2022 19:41:16 +0000 Subject: [PATCH 08/17] Fix moving elements --- .../PageEditor/RenderPanel/RenderOverlay.tsx | 45 ++++++++----------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index 3834f729810..46c905c3179 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -142,21 +142,23 @@ function removeMaybeNode(dom: appDom.AppDom, nodeId: NodeId): appDom.AppDom { } function deleteOrphanedLayoutNodes( - draftDom: appDom.AppDom, + domBeforeChange: appDom.AppDom, + domAfterChange: appDom.AppDom, movedOrDeletedNode: appDom.ElementNode, moveTargetNodeId: NodeId | null = null, ): appDom.AppDom { + let draftDom = domAfterChange; let orphanedLayoutNodeIds: NodeId[] = []; const movedOrDeletedNodeParentProp = movedOrDeletedNode.parentProp; - const parent = appDom.getParent(draftDom, movedOrDeletedNode); - const parentParent = parent && appDom.getParent(draftDom, parent); - const parentParentParent = parentParent && appDom.getParent(draftDom, parentParent); + const parent = appDom.getParent(domBeforeChange, movedOrDeletedNode); + const parentParent = parent && appDom.getParent(domBeforeChange, parent); + const parentParentParent = parentParent && appDom.getParent(domBeforeChange, parentParent); const parentChildren = parent && movedOrDeletedNodeParentProp - ? (appDom.getChildNodes(draftDom, parent) as appDom.NodeChildren)[ + ? (appDom.getChildNodes(domBeforeChange, parent) as appDom.NodeChildren)[ movedOrDeletedNodeParentProp ] : []; @@ -172,7 +174,7 @@ function deleteOrphanedLayoutNodes( parent.parentProp && appDom.isElement(parentParent) && isPageLayoutComponent(parentParent) && - appDom.getChildNodes(draftDom, parentParent)[parent.parentProp].length === 1; + appDom.getChildNodes(domBeforeChange, parentParent)[parent.parentProp].length === 1; const isSecondLastLayoutContainerChild = parent && @@ -390,34 +392,26 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { [canvasHostRef, draggedNodeId, selectionRects, dom, api], ); - const getDomWithRemovedNode = React.useCallback( - (removedNodeId: NodeId, moveTargetNodeId: NodeId | null = null) => { + const handleNodeDelete = React.useCallback( + (nodeId: NodeId) => (event?: React.MouseEvent) => { + if (event) { + event.stopPropagation(); + } + let draftDom = dom; - const toRemove = appDom.getNode(draftDom, removedNodeId); + const toRemove = appDom.getNode(draftDom, nodeId); if (appDom.isElement(toRemove)) { - draftDom = deleteOrphanedLayoutNodes(draftDom, toRemove, moveTargetNodeId); draftDom = removeMaybeNode(draftDom, toRemove.id); + draftDom = deleteOrphanedLayoutNodes(dom, draftDom, toRemove); } - return draftDom; - }, - [dom], - ); - - const handleNodeDelete = React.useCallback( - (nodeId: NodeId) => (event?: React.MouseEvent) => { - if (event) { - event.stopPropagation(); - } - - const updatedDom = getDomWithRemovedNode(nodeId); - updateDom(updatedDom); + updateDom(draftDom); api.deselect(); }, - [getDomWithRemovedNode, updateDom, api], + [dom, updateDom, api], ); const selectedRect = selectedNode ? nodesInfo[selectedNode.id]?.rect : null; @@ -1192,7 +1186,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { } if (selection) { - draftDom = getDomWithRemovedNode(draggedNode.id, dragOverNodeId); + draftDom = deleteOrphanedLayoutNodes(dom, draftDom, draggedNode, dragOverNodeId); } updateDom(draftDom); @@ -1217,7 +1211,6 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { dragOverSlotParentProp, dragOverZone, draggedNode, - getDomWithRemovedNode, newNode, nodesInfo, selection, From 4ae076da3b421458fe113a49c3c2da355738be30 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Mon, 28 Nov 2022 18:46:11 +0000 Subject: [PATCH 09/17] Re-add throttling --- .../toolpad-app/src/toolpad/DomLoader.tsx | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/DomLoader.tsx b/packages/toolpad-app/src/toolpad/DomLoader.tsx index 0f1a7a85844..d533e67c37a 100644 --- a/packages/toolpad-app/src/toolpad/DomLoader.tsx +++ b/packages/toolpad-app/src/toolpad/DomLoader.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { NodeId, BindableAttrValue, BindableAttrValues } from '@mui/toolpad-core'; import invariant from 'invariant'; +import { throttle, DebouncedFunc } from 'lodash-es'; import * as appDom from '../appDom'; import { update } from '../utils/immutability'; import client from '../api'; @@ -245,9 +246,14 @@ export function domLoaderReducer(state: DomLoader, action: DomAction): DomLoader } } -function createDomApi(dispatch: React.Dispatch) { +function createDomApi( + dispatch: React.Dispatch, + scheduleHistoryUpdate?: DebouncedFunc<() => void>, +) { return { undo() { + scheduleHistoryUpdate?.flush(); + dispatch({ type: 'DOM_UNDO' }); }, redo() { @@ -429,15 +435,31 @@ export default function DomProvider({ appId, children }: DomContextProps) { undoStack: [dom], redoStack: [], }); + + const scheduleHistoryUpdate = React.useMemo( + () => + throttle( + () => { + dispatch({ type: 'DOM_UPDATE_HISTORY' }); + }, + 500, + { leading: false, trailing: true }, + ), + [], + ); + const dispatchWithHistory = useEvent((action: DomAction) => { dispatch(action); if (!SKIP_UNDO_ACTIONS.has(action.type)) { - dispatch({ type: 'DOM_UPDATE_HISTORY' }); + scheduleHistoryUpdate(); } }); - const api = React.useMemo(() => createDomApi(dispatchWithHistory), [dispatchWithHistory]); + const api = React.useMemo( + () => createDomApi(dispatchWithHistory, scheduleHistoryUpdate), + [dispatchWithHistory, scheduleHistoryUpdate], + ); const handleSave = React.useCallback(() => { if (!state.dom || state.saving || state.savedDom === state.dom) { From d5f6b692d7f2d8755103bd745bba840c29567e4e Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:45:38 +0000 Subject: [PATCH 10/17] Remove some domAPI methods --- .../AppEditor/ConnectionEditor/index.tsx | 7 +- .../CreateCodeComponentNodeDialog.tsx | 5 +- .../CreateConnectionNodeDialog.tsx | 5 +- .../CreatePageNodeDialog.tsx | 4 +- .../AppEditor/HierarchyExplorer/index.tsx | 7 +- .../PageEditor/NodeAttributeEditor.tsx | 8 +- .../AppEditor/PageEditor/PageModuleEditor.tsx | 12 +- .../PageEditor/QueryEditor/index.tsx | 9 +- .../PageEditor/RenderPanel/RenderOverlay.tsx | 5 +- .../PageEditor/RenderPanel/RenderPanel.tsx | 3 +- .../AppEditor/PageEditor/ThemeEditor.tsx | 40 +++-- .../AppEditor/PageEditor/UrlQueryEditor.tsx | 12 +- .../toolpad-app/src/toolpad/DomLoader.tsx | 152 +----------------- 13 files changed, 86 insertions(+), 183 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx b/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx index 73b859ce47a..e6e3f57688b 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx @@ -45,18 +45,21 @@ function ConnectionEditorContent

({ className, connectionNode, }: ConnectionEditorContentProps

) { + const dom = useDom(); const domApi = useDomApi(); const handleConnectionChange = React.useCallback( (connectionParams: P | null) => { - domApi.setNodeNamespacedProp( + const updatedDom = appDom.setNodeNamespacedProp( + dom, connectionNode, 'attributes', 'params', appDom.createSecret(connectionParams), ); + domApi.update(updatedDom); }, - [connectionNode, domApi], + [connectionNode, dom, domApi], ); const dataSourceId = connectionNode.attributes.dataSource.value; diff --git a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateCodeComponentNodeDialog.tsx b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateCodeComponentNodeDialog.tsx index 3de3a67ca70..a5462de8a0d 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateCodeComponentNodeDialog.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateCodeComponentNodeDialog.tsx @@ -103,7 +103,10 @@ export default function CreateCodeComponentDialog({ }, }); const appNode = appDom.getApp(dom); - domApi.addNode(newNode, appNode, 'codeComponents'); + + const updatedDom = appDom.addNode(dom, newNode, appNode, 'codeComponents'); + domApi.update(updatedDom); + onClose(); navigate(`/app/${appId}/codeComponents/${newNode.id}`); }} diff --git a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateConnectionNodeDialog.tsx b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateConnectionNodeDialog.tsx index 5cb9457946d..c9604d877e1 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateConnectionNodeDialog.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateConnectionNodeDialog.tsx @@ -79,7 +79,10 @@ export default function CreateConnectionDialog({ }, }); const appNode = appDom.getApp(dom); - domApi.addNode(newNode, appNode, 'connections'); + + const updatedDom = appDom.addNode(dom, newNode, appNode, 'connections'); + domApi.update(updatedDom); + onClose(); navigate(`/app/${appId}/connections/${newNode.id}`); }} diff --git a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreatePageNodeDialog.tsx b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreatePageNodeDialog.tsx index 5d80550e942..4179ef044bb 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreatePageNodeDialog.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreatePageNodeDialog.tsx @@ -69,7 +69,9 @@ export default function CreatePageDialog({ }, }); const appNode = appDom.getApp(dom); - domApi.addNode(newNode, appNode, 'pages'); + + const updatedDom = appDom.addNode(dom, newNode, appNode, 'pages'); + domApi.update(updatedDom); onClose(); navigate(`/app/${appId}/pages/${newNode.id}`); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/index.tsx b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/index.tsx index 00ae66809f0..735934815e3 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/index.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/index.tsx @@ -236,7 +236,8 @@ export default function HierarchyExplorer({ appId, className }: HierarchyExplore } } - domApi.removeNode(nodeId); + const updatedDom = appDom.removeNode(dom, nodeId); + domApi.update(updatedDom); if (redirectAfterDelete) { navigate(redirectAfterDelete); @@ -254,7 +255,9 @@ export default function HierarchyExplorer({ appId, className }: HierarchyExplore ); const fragment = appDom.cloneFragment(dom, nodeId); - domApi.addFragment(fragment, node.parentId, node.parentProp); + + const updatedDom = appDom.addFragment(dom, fragment, node.parentId, node.parentProp); + domApi.update(updatedDom); const newNode = appDom.getNode(fragment, fragment.root); const editorLink = getLinkToNodeEditor(appId, newNode); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx index 2b3426176fb..becc852176a 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { ArgTypeDefinition, BindableAttrValue } from '@mui/toolpad-core'; import { Alert } from '@mui/material'; import * as appDom from '../../../appDom'; -import { useDomApi } from '../../DomLoader'; +import { useDom, useDomApi } from '../../DomLoader'; import BindableEditor from './BindableEditor'; import { usePageEditorState } from './PageEditorProvider'; import { getDefaultControl } from '../../propertyControls'; @@ -20,13 +20,15 @@ export default function NodeAttributeEditor({ name, argType, }: NodeAttributeEditorProps) { + const dom = useDom(); const domApi = useDomApi(); const handlePropChange = React.useCallback( (newValue: BindableAttrValue | null) => { - domApi.setNodeNamespacedProp(node, namespace as any, name, newValue); + const updatedDom = appDom.setNodeNamespacedProp(dom, node, namespace as any, name, newValue); + domApi.update(updatedDom); }, - [domApi, node, namespace, name], + [dom, node, namespace, name, domApi], ); const propValue: BindableAttrValue | null = (node as any)[namespace]?.[name] ?? null; diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageModuleEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageModuleEditor.tsx index 13228914a4b..b4873f663c7 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageModuleEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageModuleEditor.tsx @@ -38,8 +38,16 @@ function PageModuleEditorDialog({ pageNodeId, open, onClose }: PageModuleEditorD const handleSave = React.useCallback(() => { const pretty = tryFormat(input); setInput(pretty); - domApi.setNodeNamespacedProp(page, 'attributes', 'module', appDom.createConst(pretty)); - }, [domApi, input, page]); + + const updatedDom = appDom.setNodeNamespacedProp( + dom, + page, + 'attributes', + 'module', + appDom.createConst(pretty), + ); + domApi.update(updatedDom); + }, [dom, domApi, input, page]); const handleSaveButton = React.useCallback(() => { handleSave(); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/QueryEditor/index.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/QueryEditor/index.tsx index 317b195387f..bd787b42d99 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/QueryEditor/index.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/QueryEditor/index.tsx @@ -132,7 +132,8 @@ export default function QueryEditor() { if (appDom.nodeExists(dom, node.id)) { domApi.saveNode(node); } else { - domApi.addNode(node, page, 'queries'); + const updatedDom = appDom.addNode(dom, node, page, 'queries'); + domApi.update(updatedDom); } setDialogState({ node, isDraft: false }); }, @@ -141,10 +142,12 @@ export default function QueryEditor() { const handleDeleteNode = React.useCallback( (nodeId: NodeId) => { - domApi.removeNode(nodeId); + const updatedDom = appDom.removeNode(dom, nodeId); + domApi.update(updatedDom); + handleEditStateDialogClose(); }, - [domApi, handleEditStateDialogClose], + [dom, domApi, handleEditStateDialogClose], ); const handleRemove = React.useCallback( diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index 46c905c3179..26c77792db2 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -444,9 +444,10 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { (node: appDom.ElementNode) => (event: React.MouseEvent) => { event.stopPropagation(); - domApi.duplicateNode(node); + const updatedDom = appDom.duplicateNode(dom, node); + domApi.update(updatedDom); }, - [domApi], + [dom, domApi], ); const handleEdgeDragStart = React.useCallback( diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx index 7a9f7602a94..8c6abf5f0e9 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx @@ -57,10 +57,11 @@ export default function RenderPanel({ className }: RenderPanelProps) { const newValue: unknown = typeof event.value === 'function' ? event.value(actual?.value) : event.value; - domApi.setNodeNamespacedProp(node, 'props', event.prop, { + const updatedDom = appDom.setNodeNamespacedProp(dom, node, 'props', event.prop, { type: 'const', value: newValue, }); + domApi.update(updatedDom); return; } case 'pageStateUpdated': { diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ThemeEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ThemeEditor.tsx index 4103b2a6234..a7fad07f0d5 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ThemeEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ThemeEditor.tsx @@ -82,7 +82,8 @@ export default function ComponentEditor({ className }: ComponentEditorProps) { theme: {}, attributes: {}, }); - domApi.addNode(newTheme, app, 'themes'); + const updatedDom = appDom.addNode(dom, newTheme, app, 'themes'); + domApi.update(updatedDom); }; return ( @@ -93,10 +94,11 @@ export default function ComponentEditor({ className }: ComponentEditorProps) { exclusive value={appDom.fromConstPropValue(theme.theme?.['palette.mode']) || 'light'} onChange={(event, newValue) => { - domApi.setNodeNamespacedProp(theme, 'theme', 'palette.mode', { + const updatedDom = appDom.setNodeNamespacedProp(dom, theme, 'theme', 'palette.mode', { type: 'const', value: newValue, }); + domApi.update(updatedDom); }} aria-label="Mode" > @@ -113,21 +115,35 @@ export default function ComponentEditor({ className }: ComponentEditorProps) { name="primary" value={appDom.fromConstPropValue(theme.theme?.['palette.primary.main']) || ''} onChange={(newValue) => { - domApi.setNodeNamespacedProp(theme, 'theme', 'palette.primary.main', { - type: 'const', - value: newValue, - }); + const updatedDom = appDom.setNodeNamespacedProp( + dom, + theme, + 'theme', + 'palette.primary.main', + { + type: 'const', + value: newValue, + }, + ); + domApi.update(updatedDom); }} /> - domApi.setNodeNamespacedProp(theme, 'theme', 'palette.secondary.main', { - type: 'const', - value: newValue, - }) - } + onChange={(newValue) => { + const updatedDom = appDom.setNodeNamespacedProp( + dom, + theme, + 'theme', + 'palette.secondary.main', + { + type: 'const', + value: newValue, + }, + ); + domApi.update(updatedDom); + }} /> ) : ( diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/UrlQueryEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/UrlQueryEditor.tsx index 1f71c77139e..f94bc37c3ff 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/UrlQueryEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/UrlQueryEditor.tsx @@ -39,9 +39,17 @@ export default function UrlQueryEditor({ pageNodeId }: UrlQueryEditorProps) { }, [dialogOpen, value]); const handleSave = React.useCallback(() => { - domApi.setNodeNamespacedProp(page, 'attributes', 'parameters', appDom.createConst(input || [])); + const updatedDom = appDom.setNodeNamespacedProp( + dom, + page, + 'attributes', + 'parameters', + appDom.createConst(input || []), + ); + domApi.update(updatedDom); + handleDialogClose(); - }, [domApi, page, input, handleDialogClose]); + }, [dom, page, input, domApi, handleDialogClose]); return ( diff --git a/packages/toolpad-app/src/toolpad/DomLoader.tsx b/packages/toolpad-app/src/toolpad/DomLoader.tsx index d533e67c37a..17053236fa0 100644 --- a/packages/toolpad-app/src/toolpad/DomLoader.tsx +++ b/packages/toolpad-app/src/toolpad/DomLoader.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { NodeId, BindableAttrValue, BindableAttrValues } from '@mui/toolpad-core'; +import { NodeId, BindableAttrValues } from '@mui/toolpad-core'; import invariant from 'invariant'; import { throttle, DebouncedFunc } from 'lodash-es'; import * as appDom from '../appDom'; @@ -39,13 +39,6 @@ export type DomAction = nodeId: NodeId; name: string; } - | { - type: 'DOM_SET_NODE_PROP'; - node: appDom.AppDomNode; - prop: string; - namespace: string; - value: BindableAttrValue | null; - } | { type: 'DOM_SET_NODE_NAMESPACE'; node: appDom.AppDomNode; @@ -56,35 +49,6 @@ export type DomAction = type: 'DOM_UPDATE'; updatedDom: appDom.AppDom; } - | { - type: 'DOM_ADD_NODE'; - node: appDom.AppDomNode; - parent: appDom.AppDomNode; - parentProp: string; - parentIndex?: string; - } - | { - type: 'DOM_ADD_FRAGMENT'; - fragment: appDom.AppDom; - parentId: NodeId; - parentProp: string; - parentIndex?: string; - } - | { - type: 'DOM_MOVE_NODE'; - node: appDom.AppDomNode; - parent: appDom.AppDomNode; - parentProp: string; - parentIndex?: string; - } - | { - type: 'DOM_DUPLICATE_NODE'; - node: appDom.AppDomNode; - } - | { - type: 'DOM_REMOVE_NODE'; - nodeId: NodeId; - } | { type: 'DOM_SAVE_NODE'; node: appDom.AppDomNode; @@ -97,57 +61,15 @@ export function domReducer(dom: appDom.AppDom, action: DomAction): appDom.AppDom const node = appDom.getNode(dom, action.nodeId); return appDom.setNodeName(dom, node, action.name); } - case 'DOM_SET_NODE_PROP': { - return appDom.setNodeNamespacedProp( - dom, - action.node, - action.namespace, - action.prop, - action.value, - ); - } case 'DOM_SET_NODE_NAMESPACE': { return appDom.setNodeNamespace(dom, action.node, action.namespace, action.value); } case 'DOM_UPDATE': { return action.updatedDom; } - case 'DOM_ADD_NODE': { - return appDom.addNode( - dom, - action.node, - action.parent, - action.parentProp, - action.parentIndex, - ); - } - case 'DOM_ADD_FRAGMENT': { - return appDom.addFragment( - dom, - action.fragment, - action.parentId, - action.parentProp, - action.parentIndex, - ); - } - case 'DOM_MOVE_NODE': { - return appDom.moveNode( - dom, - action.node, - action.parent, - action.parentProp, - action.parentIndex, - ); - } - case 'DOM_DUPLICATE_NODE': { - return appDom.duplicateNode(dom, action.node); - } case 'DOM_SAVE_NODE': { return appDom.saveNode(dom, action.node); } - case 'DOM_REMOVE_NODE': { - return appDom.removeNode(dom, action.nodeId); - } default: return dom; } @@ -268,84 +190,12 @@ function createDomApi( updatedDom: dom, }); }, - addNode( - node: Child, - parent: Parent, - parentProp: appDom.ParentPropOf, - parentIndex?: string, - ) { - dispatch({ - type: 'DOM_ADD_NODE', - node, - parent, - parentProp, - parentIndex, - }); - }, - addFragment( - fragment: appDom.AppDom, - parentId: NodeId, - parentProp: string, - parentIndex?: string, - ) { - dispatch({ - type: 'DOM_ADD_FRAGMENT', - fragment, - parentId, - parentProp, - parentIndex, - }); - }, - moveNode( - node: Child, - parent: Parent, - parentProp: appDom.ParentPropOf, - parentIndex?: string, - ) { - dispatch({ - type: 'DOM_MOVE_NODE', - node, - parent, - parentProp, - parentIndex, - }); - }, - duplicateNode(node: Child) { - dispatch({ - type: 'DOM_DUPLICATE_NODE', - node, - }); - }, - removeNode(nodeId: NodeId) { - dispatch({ - type: 'DOM_REMOVE_NODE', - nodeId, - }); - }, saveNode(node: appDom.AppDomNode) { dispatch({ type: 'DOM_SAVE_NODE', node, }); }, - setNodeNamespacedProp< - Node extends appDom.AppDomNode, - Namespace extends appDom.PropNamespaces, - Prop extends keyof NonNullable & string, - >( - node: Node, - namespace: Namespace, - prop: Prop, - value: NonNullable[Prop] | null, - ) { - dispatch({ - type: 'DOM_SET_NODE_PROP', - namespace, - node, - prop, - value: value as BindableAttrValue | null, - }); - }, setNodeNamespace>( node: Node, namespace: Namespace, From 0237579442995aa2f5588479048f9021577d8e55 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 30 Nov 2022 20:09:14 +0000 Subject: [PATCH 11/17] Fix type errors --- .../src/toolpad/AppEditor/ConnectionEditor/index.tsx | 2 +- .../src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx b/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx index 896c42fec0d..6bcd230b59f 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx @@ -49,7 +49,7 @@ function ConnectionEditorContent

({ className, connectionNode, }: ConnectionEditorContentProps

) { - const dom = useDom(); + const { dom } = useDom(); const domApi = useDomApi(); const handleConnectionChange = React.useCallback( diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx index becc852176a..84bd6a3ed8b 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx @@ -20,7 +20,7 @@ export default function NodeAttributeEditor({ name, argType, }: NodeAttributeEditorProps) { - const dom = useDom(); + const { dom } = useDom(); const domApi = useDomApi(); const handlePropChange = React.useCallback( From d7a5664f1be56975b74ab8d17a93d04cadf220ad Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 2 Dec 2022 18:16:04 +0000 Subject: [PATCH 12/17] Hide selected node while dragging new node --- .../ComponentCatalog/ComponentCatalog.tsx | 4 +--- .../PageEditor/RenderPanel/RenderOverlay.tsx | 11 ++++++----- packages/toolpad-app/src/toolpad/DomLoader.tsx | 13 +++++-------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalog.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalog.tsx index aec9b932ca4..164a6ca4a63 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalog.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalog.tsx @@ -7,7 +7,7 @@ import invariant from 'invariant'; import ComponentCatalogItem from './ComponentCatalogItem'; import CreateCodeComponentNodeDialog from '../../HierarchyExplorer/CreateCodeComponentNodeDialog'; import * as appDom from '../../../../appDom'; -import { useDom, useDomApi } from '../../../DomLoader'; +import { useDom } from '../../../DomLoader'; import { usePageEditorApi, usePageEditorState } from '../PageEditorProvider'; import { useToolpadComponents } from '../../toolpadComponents'; import useLocalStorageState from '../../../../utils/useLocalStorageState'; @@ -49,7 +49,6 @@ export default function ComponentCatalog({ className }: ComponentCatalogProps) { const api = usePageEditorApi(); const pageState = usePageEditorState(); const { dom } = useDom(); - const domApi = useDomApi(); const [openStart, setOpenStart] = React.useState(0); const [openCustomComponents, setOpenCustomComponents] = useLocalStorageState( @@ -90,7 +89,6 @@ export default function ComponentCatalog({ className }: ComponentCatalogProps) { const handleDragStart = (componentType: string) => (event: React.DragEvent) => { event.dataTransfer.dropEffect = 'copy'; const newNode = appDom.createElement(dom, componentType, {}); - domApi.deselectNode(true); api.newNodeDragStart(newNode); closeDrawer(0); }; diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index afca2d83f49..c811688f875 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -981,8 +981,10 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { const isOriginalParentColumn = originalParent && appDom.isElement(originalParent) ? isPageColumn(originalParent) : false; + const isMovingNode = selectedNodeId && !newNode; + let addOrMoveNode = appDom.addNode; - if (selectedNodeId) { + if (isMovingNode) { addOrMoveNode = appDom.moveNode; } @@ -1197,7 +1199,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { } } - const draggedNodeParent = selectedNodeId ? appDom.getParent(dom, draggedNode) : null; + const draggedNodeParent = isMovingNode ? appDom.getParent(dom, draggedNode) : null; if ( draggedNode.layout?.columnSize && draggedNodeParent && @@ -1213,13 +1215,12 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { } } - if (selectedNodeId) { + if (isMovingNode) { draftDom = deleteOrphanedLayoutNodes(dom, draftDom, draggedNode, dragOverNodeId); } updateDom(draftDom, newNode?.id || undefined); - api.dragEnd(); api.dragEnd(); if (newNode) { @@ -1531,7 +1532,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { const isPageRowChild = parent ? appDom.isElement(parent) && isPageRow(parent) : false; - const isSelected = selectedNode ? selectedNode.id === node.id : false; + const isSelected = selectedNode && !newNode ? selectedNode.id === node.id : false; const isInteractive = interactiveNodes.has(node.id) && !draggedEdge; const isVerticallyResizable = Boolean(nodeInfo?.componentConfig?.resizableHeightProp); diff --git a/packages/toolpad-app/src/toolpad/DomLoader.tsx b/packages/toolpad-app/src/toolpad/DomLoader.tsx index 4cd42a2bf6e..47abdf0bb36 100644 --- a/packages/toolpad-app/src/toolpad/DomLoader.tsx +++ b/packages/toolpad-app/src/toolpad/DomLoader.tsx @@ -13,7 +13,7 @@ import insecureHash from '../utils/insecureHash'; import useEvent from '../utils/useEvent'; import { NodeHashes } from '../types'; -export type DomAction = { skipHistory?: boolean } & ( +export type DomAction = | { type: 'DOM_UPDATE_HISTORY'; } @@ -60,8 +60,7 @@ export type DomAction = { skipHistory?: boolean } & ( } | { type: 'DESELECT_NODE'; - } -); + }; export function domReducer(dom: appDom.AppDom, action: DomAction): appDom.AppDom { switch (action.type) { @@ -240,17 +239,15 @@ function createDomApi( value: value as BindableAttrValues | null, }); }, - selectNode(nodeId: NodeId, skipHistory?: boolean) { + selectNode(nodeId: NodeId) { dispatch({ type: 'SELECT_NODE', nodeId, - skipHistory, }); }, - deselectNode(skipHistory?: boolean) { + deselectNode() { dispatch({ type: 'DESELECT_NODE', - skipHistory, }); }, }; @@ -353,7 +350,7 @@ export default function DomProvider({ appId, children }: DomContextProps) { const dispatchWithHistory = useEvent((action: DomAction) => { dispatch(action); - if (!SKIP_UNDO_ACTIONS.has(action.type) && !action.skipHistory) { + if (!SKIP_UNDO_ACTIONS.has(action.type)) { dispatch({ type: 'DOM_UPDATE_HISTORY' }); } }); From 892e2ec4b113d66dffcae5eed6730d691acbe069 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 2 Dec 2022 18:50:25 +0000 Subject: [PATCH 13/17] Fix not being able to drop in selected node when dragging new node --- .../PageEditor/RenderPanel/RenderOverlay.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index c811688f875..bdbdd2aeb71 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -439,7 +439,7 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { [dom, updateDom], ); - const selectedRect = selectedNode ? nodesInfo[selectedNode.id]?.rect : null; + const selectedRect = selectedNode && !newNode ? nodesInfo[selectedNode.id]?.rect : null; const interactiveNodes = React.useMemo>(() => { if (!selectedNode) { @@ -509,12 +509,13 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { * i.e. Exclude all descendants of the current selection since inserting in one of * them would create a cyclic structure. */ - const excludedNodes = selectedNode - ? new Set([selectedNode, ...appDom.getDescendants(dom, selectedNode)]) - : new Set(); + const excludedNodes = + selectedNode && !newNode + ? new Set([selectedNode, ...appDom.getDescendants(dom, selectedNode)]) + : new Set(); return pageNodes.filter((n) => !excludedNodes.has(n)); - }, [dom, draggedNode, pageNodes, selectedNode]); + }, [dom, draggedNode, newNode, pageNodes, selectedNode]); const availableDropTargetIds = React.useMemo( () => new Set(availableDropTargets.map((n) => n.id)), From baabb445233342b9129332d58f1a78577cb7a9ac Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 14 Dec 2022 22:07:01 +0000 Subject: [PATCH 14/17] Remove batching test --- test/integration/undo-redo/index.spec.ts | 40 ------------------------ 1 file changed, 40 deletions(-) diff --git a/test/integration/undo-redo/index.spec.ts b/test/integration/undo-redo/index.spec.ts index e79bfd6d1a5..4319e0da7b1 100644 --- a/test/integration/undo-redo/index.spec.ts +++ b/test/integration/undo-redo/index.spec.ts @@ -2,7 +2,6 @@ import * as path from 'path'; import { ToolpadEditor } from '../../models/ToolpadEditor'; import { test, expect } from '../../playwright/test'; import { readJsonFile } from '../../utils/fs'; -import clickCenter from '../../utils/clickCenter'; import generateId from '../../utils/generateId'; test('test basic undo and redo', async ({ page, browserName, api }) => { @@ -42,42 +41,3 @@ test('test basic undo and redo', async ({ page, browserName, api }) => { // Redo should bring back text field await expect(canvasInputLocator).toHaveCount(3); }); - -test('test batching quick actions into single undo entry', async ({ page, browserName, api }) => { - const dom = await readJsonFile(path.resolve(__dirname, './dom.json')); - - const app = await api.mutation.createApp(`App ${generateId()}`, { - from: { kind: 'dom', dom }, - }); - - const editorModel = new ToolpadEditor(page, browserName); - await editorModel.goto(app.id); - - await editorModel.waitForOverlay(); - - const input = editorModel.appCanvas.locator('input').first(); - - clickCenter(page, input); - - await editorModel.componentEditor.getByLabel('defaultValue').click(); - - await page.keyboard.type('some value'); - - // Wait for undo stack to be updated - await page.waitForTimeout(600); - - await page.keyboard.type(' hello'); - - await editorModel.componentEditor.getByLabel('defaultValue').blur(); - - await expect(input).toHaveValue('some value hello'); - - // Wait for undo stack to be updated - await page.waitForTimeout(600); - - // Undo changes - await page.keyboard.press('Control+Z'); - - // Asssert that batched changes were reverted - await expect(input).toHaveValue('some value'); -}); From ab64a1cb1662c1b3d23ca22aed74592e071cf3f1 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 15 Dec 2022 17:24:27 +0000 Subject: [PATCH 15/17] Debounce text input changes (#1459) --- .../AppEditor/PageEditor/EditorCanvasHost.tsx | 33 ++++++-------- .../toolpad/AppEditor/PageEditor/index.tsx | 11 +---- .../toolpad-app/src/toolpad/DomLoader.tsx | 42 +++++++++++------- test/integration/undo-redo/index.spec.ts | 44 +++++++++++++++++++ 4 files changed, 84 insertions(+), 46 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx index 0495489348e..992b66d46c7 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx @@ -13,7 +13,6 @@ import useEvent from '../../../utils/useEvent'; import { LogEntry } from '../../../components/Console'; import { Maybe } from '../../../utils/types'; import { useDomApi } from '../../DomLoader'; -import { hasFieldFocus } from '../../../utils/fields'; import createRuntimeState from '../../../createRuntimeState'; type IframeContentWindow = Window & typeof globalThis; @@ -149,24 +148,20 @@ export default React.forwardRef( const handleRuntimeEvent = useEvent(onRuntimeEvent); - const iframeKeyDownHandler = React.useCallback( - (iframeDocument: Document) => { - return (event: KeyboardEvent) => { - if (hasFieldFocus(iframeDocument)) { - return; - } + const keyDownHandler = React.useCallback( + (event: KeyboardEvent) => { + const isZ = event.key.toLowerCase() === 'z'; - const isZ = event.key.toLowerCase() === 'z'; + const undoShortcut = isZ && (event.metaKey || event.ctrlKey); + const redoShortcut = undoShortcut && event.shiftKey; - const undoShortcut = isZ && (event.metaKey || event.ctrlKey); - const redoShortcut = undoShortcut && event.shiftKey; - - if (redoShortcut) { - domApi.redo(); - } else if (undoShortcut) { - domApi.undo(); - } - }; + if (redoShortcut) { + event.preventDefault(); + domApi.redo(); + } else if (undoShortcut) { + event.preventDefault(); + domApi.undo(); + } }, [domApi], ); @@ -181,13 +176,11 @@ export default React.forwardRef( return; } - const keyDownHandler = iframeKeyDownHandler(iframeWindow.document); - iframeWindow?.addEventListener('keydown', keyDownHandler); iframeWindow?.addEventListener('unload', () => { iframeWindow?.removeEventListener('keydown', keyDownHandler); }); - }, [iframeKeyDownHandler]); + }, [keyDownHandler]); React.useEffect(() => { if (!contentWindow) { diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/index.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/index.tsx index 21af09a6a56..a120584352f 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/index.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/index.tsx @@ -14,7 +14,6 @@ import usePageTitle from '../../../utils/usePageTitle'; import useLocalStorageState from '../../../utils/useLocalStorageState'; import useDebouncedHandler from '../../../utils/useDebouncedHandler'; import useShortcut from '../../../utils/useShortcut'; -import { hasFieldFocus } from '../../../utils/fields'; const classes = { renderPanel: 'Toolpad_RenderPanel', @@ -76,16 +75,10 @@ export default function PageEditor({ appId }: PageEditorProps) { const { nodeId } = useParams(); const pageNode = appDom.getMaybeNode(dom, nodeId as NodeId, 'page'); - useShortcut({ key: 'z', metaKey: true, preventDefault: false }, () => { - if (hasFieldFocus()) { - return; - } + useShortcut({ key: 'z', metaKey: true, preventDefault: true }, () => { domApi.undo(); }); - useShortcut({ key: 'z', metaKey: true, shiftKey: true, preventDefault: false }, () => { - if (hasFieldFocus()) { - return; - } + useShortcut({ key: 'z', metaKey: true, shiftKey: true, preventDefault: true }, () => { domApi.redo(); }); diff --git a/packages/toolpad-app/src/toolpad/DomLoader.tsx b/packages/toolpad-app/src/toolpad/DomLoader.tsx index 47abdf0bb36..97a93a2a3df 100644 --- a/packages/toolpad-app/src/toolpad/DomLoader.tsx +++ b/packages/toolpad-app/src/toolpad/DomLoader.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { NodeId, BindableAttrValues } from '@mui/toolpad-core'; import invariant from 'invariant'; -import { throttle, DebouncedFunc } from 'lodash-es'; +import { debounce, DebouncedFunc } from 'lodash-es'; import * as appDom from '../appDom'; import { update } from '../utils/immutability'; import client from '../api'; @@ -12,6 +12,7 @@ import { mapValues } from '../utils/collections'; import insecureHash from '../utils/insecureHash'; import useEvent from '../utils/useEvent'; import { NodeHashes } from '../types'; +import { hasFieldFocus } from '../utils/fields'; export type DomAction = | { @@ -200,11 +201,11 @@ export function domLoaderReducer(state: DomLoader, action: DomAction): DomLoader function createDomApi( dispatch: React.Dispatch, - scheduleHistoryUpdate?: DebouncedFunc<() => void>, + scheduleTextInputHistoryUpdate?: DebouncedFunc<() => void>, ) { return { undo() { - scheduleHistoryUpdate?.flush(); + scheduleTextInputHistoryUpdate?.flush(); dispatch({ type: 'DOM_UNDO' }); }, @@ -335,29 +336,36 @@ export default function DomProvider({ appId, children }: DomContextProps) { redoStack: [], }); - const scheduleHistoryUpdate = React.useMemo( + const scheduleTextInputHistoryUpdate = React.useMemo( () => - throttle( - () => { - dispatch({ type: 'DOM_UPDATE_HISTORY' }); - }, - 500, - { leading: false, trailing: true }, - ), + debounce(() => { + dispatch({ type: 'DOM_UPDATE_HISTORY' }); + }, 500), [], ); + const scheduleHistoryUpdate = React.useMemo( + () => () => { + if (!hasFieldFocus()) { + dispatch({ type: 'DOM_UPDATE_HISTORY' }); + } else { + scheduleTextInputHistoryUpdate(); + } + }, + [scheduleTextInputHistoryUpdate], + ); + const dispatchWithHistory = useEvent((action: DomAction) => { dispatch(action); if (!SKIP_UNDO_ACTIONS.has(action.type)) { - dispatch({ type: 'DOM_UPDATE_HISTORY' }); + scheduleHistoryUpdate(); } }); const api = React.useMemo( - () => createDomApi(dispatchWithHistory, scheduleHistoryUpdate), - [dispatchWithHistory, scheduleHistoryUpdate], + () => createDomApi(dispatchWithHistory, scheduleTextInputHistoryUpdate), + [dispatchWithHistory, scheduleTextInputHistoryUpdate], ); const handleSave = React.useCallback(() => { @@ -377,11 +385,11 @@ export default function DomProvider({ appId, children }: DomContextProps) { }); }, [appId, state]); - const debouncedhandleSave = useDebouncedHandler(handleSave, 1000); + const debouncedHandleSave = useDebouncedHandler(handleSave, 1000); React.useEffect(() => { - debouncedhandleSave(); - }, [state.dom, debouncedhandleSave]); + debouncedHandleSave(); + }, [state.dom, debouncedHandleSave]); React.useEffect(() => { logUnsavedChanges(state.unsavedChanges); diff --git a/test/integration/undo-redo/index.spec.ts b/test/integration/undo-redo/index.spec.ts index 4319e0da7b1..c4b59a57330 100644 --- a/test/integration/undo-redo/index.spec.ts +++ b/test/integration/undo-redo/index.spec.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import { ToolpadEditor } from '../../models/ToolpadEditor'; import { test, expect } from '../../playwright/test'; import { readJsonFile } from '../../utils/fs'; +import clickCenter from '../../utils/clickCenter'; import generateId from '../../utils/generateId'; test('test basic undo and redo', async ({ page, browserName, api }) => { @@ -41,3 +42,46 @@ test('test basic undo and redo', async ({ page, browserName, api }) => { // Redo should bring back text field await expect(canvasInputLocator).toHaveCount(3); }); + +test('test batching text input actions into single undo entry', async ({ + page, + browserName, + api, +}) => { + const dom = await readJsonFile(path.resolve(__dirname, './dom.json')); + + const app = await api.mutation.createApp(`App ${generateId()}`, { + from: { kind: 'dom', dom }, + }); + + const editorModel = new ToolpadEditor(page, browserName); + await editorModel.goto(app.id); + + await editorModel.waitForOverlay(); + + const input = editorModel.appCanvas.locator('input').first(); + + clickCenter(page, input); + + await editorModel.componentEditor.getByLabel('defaultValue').click(); + + await page.keyboard.type('some value'); + + // Wait for undo stack to be updated + await page.waitForTimeout(500); + + await page.keyboard.type(' hello'); + + await editorModel.componentEditor.getByLabel('defaultValue').blur(); + + await expect(input).toHaveValue('some value hello'); + + // Wait for undo stack to be updated + await page.waitForTimeout(500); + + // Undo changes + await page.keyboard.press('Control+Z'); + + // Asssert that batched changes were reverted + await expect(input).toHaveValue('some value'); +}); From 1f7779e2e9d097c6d5607e11915a59d180e39bc3 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 16 Dec 2022 12:48:00 +0000 Subject: [PATCH 16/17] Use updater function in DOM API updates --- .../AppEditor/ConnectionEditor/index.tsx | 18 +- .../CreateCodeComponentNodeDialog.tsx | 3 +- .../CreateConnectionNodeDialog.tsx | 3 +- .../CreatePageNodeDialog.tsx | 3 +- .../AppEditor/HierarchyExplorer/index.tsx | 17 +- .../PageEditor/NodeAttributeEditor.tsx | 10 +- .../AppEditor/PageEditor/PageModuleEditor.tsx | 11 +- .../PageEditor/QueryEditor/index.tsx | 8 +- .../PageEditor/RenderPanel/RenderOverlay.tsx | 576 +++++++++--------- .../PageEditor/RenderPanel/RenderPanel.tsx | 11 +- .../AppEditor/PageEditor/ThemeEditor.tsx | 36 +- .../AppEditor/PageEditor/UrlQueryEditor.tsx | 17 +- .../toolpad-app/src/toolpad/DomLoader.tsx | 31 +- 13 files changed, 351 insertions(+), 393 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx b/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx index 6bcd230b59f..71fb2bdcfe7 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/ConnectionEditor/index.tsx @@ -49,21 +49,21 @@ function ConnectionEditorContent

({ className, connectionNode, }: ConnectionEditorContentProps

) { - const { dom } = useDom(); const domApi = useDomApi(); const handleConnectionChange = React.useCallback( (connectionParams: P | null) => { - const updatedDom = appDom.setNodeNamespacedProp( - dom, - connectionNode, - 'attributes', - 'params', - appDom.createSecret(connectionParams), + domApi.update((draft) => + appDom.setNodeNamespacedProp( + draft, + connectionNode, + 'attributes', + 'params', + appDom.createSecret(connectionParams), + ), ); - domApi.update(updatedDom); }, - [connectionNode, dom, domApi], + [connectionNode, domApi], ); const dataSourceId = connectionNode.attributes.dataSource.value; diff --git a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateCodeComponentNodeDialog.tsx b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateCodeComponentNodeDialog.tsx index 7f82846abc9..b460a185c34 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateCodeComponentNodeDialog.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateCodeComponentNodeDialog.tsx @@ -104,8 +104,7 @@ export default function CreateCodeComponentDialog({ }); const appNode = appDom.getApp(dom); - const updatedDom = appDom.addNode(dom, newNode, appNode, 'codeComponents'); - domApi.update(updatedDom); + domApi.update((draft) => appDom.addNode(draft, newNode, appNode, 'codeComponents')); onClose(); navigate(`/app/${appId}/codeComponents/${newNode.id}`); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateConnectionNodeDialog.tsx b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateConnectionNodeDialog.tsx index 72520581c94..fb2bfad446c 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateConnectionNodeDialog.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreateConnectionNodeDialog.tsx @@ -82,8 +82,7 @@ export default function CreateConnectionDialog({ }); const appNode = appDom.getApp(dom); - const updatedDom = appDom.addNode(dom, newNode, appNode, 'connections'); - domApi.update(updatedDom); + domApi.update((draft) => appDom.addNode(draft, newNode, appNode, 'connections')); onClose(); navigate(`/app/${appId}/connections/${newNode.id}`); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreatePageNodeDialog.tsx b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreatePageNodeDialog.tsx index 9b6cf5830af..67de77141f2 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreatePageNodeDialog.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/CreatePageNodeDialog.tsx @@ -70,8 +70,7 @@ export default function CreatePageDialog({ }); const appNode = appDom.getApp(dom); - const updatedDom = appDom.addNode(dom, newNode, appNode, 'pages'); - domApi.update(updatedDom); + domApi.update((draft) => appDom.addNode(draft, newNode, appNode, 'pages')); onClose(); navigate(`/app/${appId}/pages/${newNode.id}`); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/index.tsx b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/index.tsx index c7f90604fc9..aa77f7f8bb9 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/index.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/HierarchyExplorer/index.tsx @@ -237,8 +237,7 @@ export default function HierarchyExplorer({ appId, className }: HierarchyExplore } } - const updatedDom = appDom.removeNode(dom, nodeId); - domApi.update(updatedDom); + domApi.update((draft) => appDom.removeNode(draft, nodeId)); if (redirectAfterDelete) { navigate(redirectAfterDelete); @@ -250,15 +249,17 @@ export default function HierarchyExplorer({ appId, className }: HierarchyExplore const handleDuplicateNode = React.useCallback( (nodeId: NodeId) => { const node = appDom.getNode(dom, nodeId); - invariant( - node.parentId && node.parentProp, - 'Duplication should never be called on nodes that are not placed in the dom', - ); const fragment = appDom.cloneFragment(dom, nodeId); - const updatedDom = appDom.addFragment(dom, fragment, node.parentId, node.parentProp); - domApi.update(updatedDom); + domApi.update((draft) => { + invariant( + node.parentId && node.parentProp, + 'Duplication should never be called on nodes that are not placed in the dom', + ); + + return appDom.addFragment(draft, fragment, node.parentId, node.parentProp); + }); const newNode = appDom.getNode(fragment, fragment.root); const editorLink = getLinkToNodeEditor(appId, newNode); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx index 21510f08c84..7ce66ded843 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeAttributeEditor.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { ArgTypeDefinition, BindableAttrValue } from '@mui/toolpad-core'; import { Alert } from '@mui/material'; import * as appDom from '../../../appDom'; -import { useDom, useDomApi } from '../../DomLoader'; +import { useDomApi } from '../../DomLoader'; import BindableEditor from './BindableEditor'; import { usePageEditorState } from './PageEditorProvider'; import { getDefaultControl } from '../../propertyControls'; @@ -22,15 +22,15 @@ export default function NodeAttributeEditor

({ argType, props, }: NodeAttributeEditorProps

) { - const { dom } = useDom(); const domApi = useDomApi(); const handlePropChange = React.useCallback( (newValue: BindableAttrValue | null) => { - const updatedDom = appDom.setNodeNamespacedProp(dom, node, namespace as any, name, newValue); - domApi.update(updatedDom); + domApi.update((draft) => + appDom.setNodeNamespacedProp(draft, node, namespace as any, name, newValue), + ); }, - [dom, node, namespace, name, domApi], + [node, namespace, name, domApi], ); const propValue: BindableAttrValue | null = (node as any)[namespace]?.[name] ?? null; diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageModuleEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageModuleEditor.tsx index 41f6680abf6..3bcf610e4cc 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageModuleEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageModuleEditor.tsx @@ -39,15 +39,10 @@ function PageModuleEditorDialog({ pageNodeId, open, onClose }: PageModuleEditorD const pretty = tryFormat(input); setInput(pretty); - const updatedDom = appDom.setNodeNamespacedProp( - dom, - page, - 'attributes', - 'module', - appDom.createConst(pretty), + domApi.update((draft) => + appDom.setNodeNamespacedProp(draft, page, 'attributes', 'module', appDom.createConst(pretty)), ); - domApi.update(updatedDom); - }, [dom, domApi, input, page]); + }, [domApi, input, page]); const handleSaveButton = React.useCallback(() => { handleSave(); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/QueryEditor/index.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/QueryEditor/index.tsx index 9dc892da65f..fb4d7b67aae 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/QueryEditor/index.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/QueryEditor/index.tsx @@ -132,8 +132,7 @@ export default function QueryEditor() { if (appDom.nodeExists(dom, node.id)) { domApi.saveNode(node); } else { - const updatedDom = appDom.addNode(dom, node, page, 'queries'); - domApi.update(updatedDom); + domApi.update((draft) => appDom.addNode(draft, node, page, 'queries')); } setDialogState({ node, isDraft: false }); }, @@ -142,12 +141,11 @@ export default function QueryEditor() { const handleDeleteNode = React.useCallback( (nodeId: NodeId) => { - const updatedDom = appDom.removeNode(dom, nodeId); - domApi.update(updatedDom); + domApi.update((draft) => appDom.removeNode(draft, nodeId)); handleEditStateDialogClose(); }, - [dom, domApi, handleEditStateDialogClose], + [domApi, handleEditStateDialogClose], ); const handleRemove = React.useCallback( diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx index bdbdd2aeb71..46f62bea614 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -363,14 +363,6 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { [pageNode], ); - const updateDom = React.useCallback( - (draftDom: appDom.AppDom, newSelectedNodeId?: NodeId | null) => { - draftDom = normalizePageRowColumnSizes(draftDom); - domApi.update(draftDom, newSelectedNodeId); - }, - [domApi, normalizePageRowColumnSizes], - ); - const selectNode = React.useCallback( (nodeId: NodeId) => { if (selectedNodeId !== nodeId) { @@ -425,18 +417,18 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { event.stopPropagation(); } - let draftDom = dom; - - const toRemove = appDom.getNode(draftDom, nodeId); + domApi.update((draft) => { + const toRemove = appDom.getNode(draft, nodeId); - if (appDom.isElement(toRemove)) { - draftDom = removeMaybeNode(draftDom, toRemove.id); - draftDom = deleteOrphanedLayoutNodes(dom, draftDom, toRemove); - } + if (appDom.isElement(toRemove)) { + draft = removeMaybeNode(draft, toRemove.id); + draft = deleteOrphanedLayoutNodes(dom, draft, toRemove); + } - updateDom(draftDom, null); + return normalizePageRowColumnSizes(draft); + }, null); }, - [dom, updateDom], + [dom, domApi, normalizePageRowColumnSizes], ); const selectedRect = selectedNode && !newNode ? nodesInfo[selectedNode.id]?.rect : null; @@ -470,10 +462,12 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { (node: appDom.ElementNode) => (event: React.MouseEvent) => { event.stopPropagation(); - const updatedDom = appDom.duplicateNode(dom, node); - domApi.update(updatedDom); + domApi.update((draft) => { + draft = appDom.duplicateNode(draft, node); + return normalizePageRowColumnSizes(draft); + }); }, - [dom, domApi], + [domApi, normalizePageRowColumnSizes], ); const handleEdgeDragStart = React.useCallback( @@ -971,256 +965,248 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { const isDraggingOverLayoutSlot = dragOverSlot?.type === 'layout'; const isDraggingOverElement = appDom.isElement(dragOverNode); - let parent = appDom.getParent(dom, dragOverNode); - - const originalParent = parent; - const originalParentInfo = parent && nodesInfo[parent.id]; + domApi.update((draft) => { + let parent = appDom.getParent(draft, dragOverNode); - const isOriginalParentPage = originalParent ? appDom.isPage(originalParent) : false; - const isOriginalParentRow = - originalParent && appDom.isElement(originalParent) ? isPageRow(originalParent) : false; - const isOriginalParentColumn = - originalParent && appDom.isElement(originalParent) ? isPageColumn(originalParent) : false; + const originalParent = parent; + const originalParentInfo = parent && nodesInfo[parent.id]; - const isMovingNode = selectedNodeId && !newNode; + const isOriginalParentPage = originalParent ? appDom.isPage(originalParent) : false; + const isOriginalParentRow = + originalParent && appDom.isElement(originalParent) ? isPageRow(originalParent) : false; + const isOriginalParentColumn = + originalParent && appDom.isElement(originalParent) ? isPageColumn(originalParent) : false; - let addOrMoveNode = appDom.addNode; - if (isMovingNode) { - addOrMoveNode = appDom.moveNode; - } + const isMovingNode = selectedNodeId && !newNode; - let draftDom = dom; + let addOrMoveNode = appDom.addNode; + if (isMovingNode) { + addOrMoveNode = appDom.moveNode; + } - // Drop on page or layout slot - if (isDraggingOverPage || isDraggingOverLayoutSlot) { - const newParentIndex = - dragOverZone === DROP_ZONE_TOP - ? appDom.getNewFirstParentIndexInNode(draftDom, dragOverNode, 'children') - : appDom.getNewLastParentIndexInNode(draftDom, dragOverNode, 'children'); + // Drop on page or layout slot + if (isDraggingOverPage || isDraggingOverLayoutSlot) { + const newParentIndex = + dragOverZone === DROP_ZONE_TOP + ? appDom.getNewFirstParentIndexInNode(draft, dragOverNode, 'children') + : appDom.getNewLastParentIndexInNode(draft, dragOverNode, 'children'); - if (!isPageRow(draggedNode)) { - const rowContainer = appDom.createElement(draftDom, PAGE_ROW_COMPONENT_ID, {}); - draftDom = appDom.addNode( - draftDom, - rowContainer, - dragOverNode, - 'children', - newParentIndex, - ); - parent = rowContainer; + if (!isPageRow(draggedNode)) { + const rowContainer = appDom.createElement(draft, PAGE_ROW_COMPONENT_ID, {}); + draft = appDom.addNode(draft, rowContainer, dragOverNode, 'children', newParentIndex); + parent = rowContainer; - draftDom = addOrMoveNode(draftDom, draggedNode, rowContainer, 'children'); - } else { - draftDom = addOrMoveNode(draftDom, draggedNode, dragOverNode, 'children', newParentIndex); + draft = addOrMoveNode(draft, draggedNode, rowContainer, 'children'); + } else { + draft = addOrMoveNode(draft, draggedNode, dragOverNode, 'children', newParentIndex); + } } - } - if ( - isDraggingOverElement && - !isDraggingOverLayoutSlot && - parent && - (appDom.isPage(parent) || appDom.isElement(parent)) - ) { - const isDraggingOverRow = isDraggingOverElement && isPageRow(dragOverNode); + if ( + isDraggingOverElement && + !isDraggingOverLayoutSlot && + parent && + (appDom.isPage(parent) || appDom.isElement(parent)) + ) { + const isDraggingOverRow = isDraggingOverElement && isPageRow(dragOverNode); - const isDraggingOverHorizontalContainer = dragOverSlot - ? isHorizontalFlow(dragOverSlot.flowDirection) - : false; - const isDraggingOverVerticalContainer = dragOverSlot - ? isVerticalFlow(dragOverSlot.flowDirection) - : false; + const isDraggingOverHorizontalContainer = dragOverSlot + ? isHorizontalFlow(dragOverSlot.flowDirection) + : false; + const isDraggingOverVerticalContainer = dragOverSlot + ? isVerticalFlow(dragOverSlot.flowDirection) + : false; - if (dragOverZone === DROP_ZONE_CENTER && dragOverSlotParentProp) { - draftDom = addOrMoveNode(draftDom, draggedNode, dragOverNode, dragOverSlotParentProp); - } + if (dragOverZone === DROP_ZONE_CENTER && dragOverSlotParentProp) { + draft = addOrMoveNode(draft, draggedNode, dragOverNode, dragOverSlotParentProp); + } - if ([DROP_ZONE_TOP, DROP_ZONE_BOTTOM].includes(dragOverZone)) { - if (!isDraggingOverVerticalContainer) { - const newParentIndex = - dragOverZone === DROP_ZONE_TOP - ? appDom.getNewParentIndexBeforeNode(draftDom, dragOverNode, dragOverNodeParentProp) - : appDom.getNewParentIndexAfterNode(draftDom, dragOverNode, dragOverNodeParentProp); - - if (isDraggingOverRow && !isPageRow(draggedNode)) { - if (isOriginalParentPage) { - const rowContainer = appDom.createElement(draftDom, PAGE_ROW_COMPONENT_ID, {}); - draftDom = appDom.addNode( - draftDom, - rowContainer, + if ([DROP_ZONE_TOP, DROP_ZONE_BOTTOM].includes(dragOverZone)) { + if (!isDraggingOverVerticalContainer) { + const newParentIndex = + dragOverZone === DROP_ZONE_TOP + ? appDom.getNewParentIndexBeforeNode(draft, dragOverNode, dragOverNodeParentProp) + : appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp); + + if (isDraggingOverRow && !isPageRow(draggedNode)) { + if (isOriginalParentPage) { + const rowContainer = appDom.createElement(draft, PAGE_ROW_COMPONENT_ID, {}); + draft = appDom.addNode( + draft, + rowContainer, + parent, + dragOverNodeParentProp, + newParentIndex, + ); + parent = rowContainer; + + draft = addOrMoveNode(draft, draggedNode, parent, dragOverNodeParentProp); + } else { + draft = addOrMoveNode( + draft, + draggedNode, + parent, + dragOverNodeParentProp, + newParentIndex, + ); + } + } + + if (isOriginalParentRow) { + const columnContainer = appDom.createElement( + draft, + PAGE_COLUMN_COMPONENT_ID, + {}, + { + columnSize: dragOverNode.layout?.columnSize || appDom.createConst(1), + }, + ); + + draft = appDom.setNodeNamespacedProp( + draft, + dragOverNode, + 'layout', + 'columnSize', + appDom.createConst(1), + ); + + draft = appDom.addNode( + draft, + columnContainer, parent, dragOverNodeParentProp, - newParentIndex, + appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp), ); - parent = rowContainer; + parent = columnContainer; - draftDom = addOrMoveNode(draftDom, draggedNode, parent, dragOverNodeParentProp); - } else { - draftDom = addOrMoveNode( - draftDom, + // Move existing element inside column right away if drag over zone is bottom + if (dragOverZone === DROP_ZONE_BOTTOM) { + draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); + } + } + + if (!isDraggingOverRow || isPageRow(draggedNode)) { + draft = addOrMoveNode( + draft, draggedNode, parent, dragOverNodeParentProp, newParentIndex, ); } - } - - if (isOriginalParentRow) { - const columnContainer = appDom.createElement( - draftDom, - PAGE_COLUMN_COMPONENT_ID, - {}, - { - columnSize: dragOverNode.layout?.columnSize || appDom.createConst(1), - }, - ); - draftDom = appDom.setNodeNamespacedProp( - draftDom, - dragOverNode, - 'layout', - 'columnSize', - appDom.createConst(1), - ); - - draftDom = appDom.addNode( - draftDom, - columnContainer, - parent, - dragOverNodeParentProp, - appDom.getNewParentIndexAfterNode(draftDom, dragOverNode, dragOverNodeParentProp), - ); - parent = columnContainer; - - // Move existing element inside column right away if drag over zone is bottom - if (dragOverZone === DROP_ZONE_BOTTOM) { - draftDom = appDom.moveNode(draftDom, dragOverNode, parent, dragOverNodeParentProp); + // Only move existing element inside column in the end if drag over zone is top + if ( + isOriginalParentRow && + !isDraggingOverVerticalContainer && + dragOverZone === DROP_ZONE_TOP + ) { + draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); } } - if (!isDraggingOverRow || isPageRow(draggedNode)) { - draftDom = addOrMoveNode( - draftDom, + if (dragOverSlotParentProp && isDraggingOverVerticalContainer) { + const isDraggingOverDirectionStart = + dragOverZone === + (dragOverSlot?.flowDirection === 'column' ? DROP_ZONE_TOP : DROP_ZONE_BOTTOM); + + const newParentIndex = isDraggingOverDirectionStart + ? appDom.getNewFirstParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp) + : appDom.getNewLastParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp); + + draft = addOrMoveNode( + draft, draggedNode, - parent, - dragOverNodeParentProp, + dragOverNode, + dragOverSlotParentProp, newParentIndex, ); } - - // Only move existing element inside column in the end if drag over zone is top - if ( - isOriginalParentRow && - !isDraggingOverVerticalContainer && - dragOverZone === DROP_ZONE_TOP - ) { - draftDom = appDom.moveNode(draftDom, dragOverNode, parent, dragOverNodeParentProp); - } } - if (dragOverSlotParentProp && isDraggingOverVerticalContainer) { - const isDraggingOverDirectionStart = - dragOverZone === - (dragOverSlot?.flowDirection === 'column' ? DROP_ZONE_TOP : DROP_ZONE_BOTTOM); + if ([DROP_ZONE_RIGHT, DROP_ZONE_LEFT].includes(dragOverZone)) { + if (!isDraggingOverHorizontalContainer) { + if (isOriginalParentColumn) { + const rowContainer = appDom.createElement(draft, PAGE_ROW_COMPONENT_ID, { + justifyContent: appDom.createConst( + originalParentInfo?.props.alignItems || 'start', + ), + }); + draft = appDom.addNode( + draft, + rowContainer, + parent, + dragOverNodeParentProp, + appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp), + ); + parent = rowContainer; - const newParentIndex = isDraggingOverDirectionStart - ? appDom.getNewFirstParentIndexInNode(draftDom, dragOverNode, dragOverSlotParentProp) - : appDom.getNewLastParentIndexInNode(draftDom, dragOverNode, dragOverSlotParentProp); + // Move existing element inside right away if drag over zone is right + if (dragOverZone === DROP_ZONE_RIGHT) { + draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); + } + } - draftDom = addOrMoveNode( - draftDom, - draggedNode, - dragOverNode, - dragOverSlotParentProp, - newParentIndex, - ); - } - } + const newParentIndex = + dragOverZone === DROP_ZONE_RIGHT + ? appDom.getNewParentIndexAfterNode(draft, dragOverNode, dragOverNodeParentProp) + : appDom.getNewParentIndexBeforeNode(draft, dragOverNode, dragOverNodeParentProp); - if ([DROP_ZONE_RIGHT, DROP_ZONE_LEFT].includes(dragOverZone)) { - if (!isDraggingOverHorizontalContainer) { - if (isOriginalParentColumn) { - const rowContainer = appDom.createElement(draftDom, PAGE_ROW_COMPONENT_ID, { - justifyContent: appDom.createConst(originalParentInfo?.props.alignItems || 'start'), - }); - draftDom = appDom.addNode( - draftDom, - rowContainer, + draft = addOrMoveNode( + draft, + draggedNode, parent, dragOverNodeParentProp, - appDom.getNewParentIndexAfterNode(draftDom, dragOverNode, dragOverNodeParentProp), + newParentIndex, ); - parent = rowContainer; - // Move existing element inside right away if drag over zone is right - if (dragOverZone === DROP_ZONE_RIGHT) { - draftDom = appDom.moveNode(draftDom, dragOverNode, parent, dragOverNodeParentProp); + // Only move existing element inside column in the end if drag over zone is left + if (isOriginalParentColumn && dragOverZone === DROP_ZONE_LEFT) { + draft = appDom.moveNode(draft, dragOverNode, parent, dragOverNodeParentProp); } } - const newParentIndex = - dragOverZone === DROP_ZONE_RIGHT - ? appDom.getNewParentIndexAfterNode(draftDom, dragOverNode, dragOverNodeParentProp) - : appDom.getNewParentIndexBeforeNode( - draftDom, - dragOverNode, - dragOverNodeParentProp, - ); + if (dragOverSlotParentProp && isDraggingOverHorizontalContainer) { + const isDraggingOverDirectionStart = + dragOverZone === + (dragOverSlot?.flowDirection === 'row' ? DROP_ZONE_LEFT : DROP_ZONE_RIGHT); - draftDom = addOrMoveNode( - draftDom, - draggedNode, - parent, - dragOverNodeParentProp, - newParentIndex, - ); + const newParentIndex = isDraggingOverDirectionStart + ? appDom.getNewFirstParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp) + : appDom.getNewLastParentIndexInNode(draft, dragOverNode, dragOverSlotParentProp); - // Only move existing element inside column in the end if drag over zone is left - if (isOriginalParentColumn && dragOverZone === DROP_ZONE_LEFT) { - draftDom = appDom.moveNode(draftDom, dragOverNode, parent, dragOverNodeParentProp); + draft = addOrMoveNode( + draft, + draggedNode, + dragOverNode, + dragOverSlotParentProp, + newParentIndex, + ); } } - if (dragOverSlotParentProp && isDraggingOverHorizontalContainer) { - const isDraggingOverDirectionStart = - dragOverZone === - (dragOverSlot?.flowDirection === 'row' ? DROP_ZONE_LEFT : DROP_ZONE_RIGHT); - - const newParentIndex = isDraggingOverDirectionStart - ? appDom.getNewFirstParentIndexInNode(draftDom, dragOverNode, dragOverSlotParentProp) - : appDom.getNewLastParentIndexInNode(draftDom, dragOverNode, dragOverSlotParentProp); - - draftDom = addOrMoveNode( - draftDom, + const draggedNodeParent = isMovingNode ? appDom.getParent(draft, draggedNode) : null; + if ( + draggedNode.layout?.columnSize && + draggedNodeParent && + draggedNodeParent.id !== parent.id + ) { + draft = appDom.setNodeNamespacedProp( + draft, draggedNode, - dragOverNode, - dragOverSlotParentProp, - newParentIndex, + 'layout', + 'columnSize', + appDom.createConst(1), ); } } - const draggedNodeParent = isMovingNode ? appDom.getParent(dom, draggedNode) : null; - if ( - draggedNode.layout?.columnSize && - draggedNodeParent && - draggedNodeParent.id !== parent.id - ) { - draftDom = appDom.setNodeNamespacedProp( - draftDom, - draggedNode, - 'layout', - 'columnSize', - appDom.createConst(1), - ); + if (isMovingNode) { + draft = deleteOrphanedLayoutNodes(dom, draft, draggedNode, dragOverNodeId); } - } - if (isMovingNode) { - draftDom = deleteOrphanedLayoutNodes(dom, draftDom, draggedNode, dragOverNodeId); - } - - updateDom(draftDom, newNode?.id || undefined); + return normalizePageRowColumnSizes(draft); + }, newNode?.id); api.dragEnd(); @@ -1238,15 +1224,16 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { availableDropZones, canvasHostRef, dom, + domApi, dragOverNodeId, dragOverSlotParentProp, dragOverZone, draggedNode, newNode, nodesInfo, + normalizePageRowColumnSizes, selectedNodeId, setSelectedComponentPanelTab, - updateDom, ], ); @@ -1388,100 +1375,109 @@ export default function RenderOverlay({ canvasHostRef }: RenderOverlayProps) { const resizePreviewRect = resizePreviewElement?.getBoundingClientRect(); if (draggedNodeRect && resizePreviewRect) { - let draftDom = dom; - - if (draggedEdge === RECTANGLE_EDGE_LEFT || draggedEdge === RECTANGLE_EDGE_RIGHT) { - const parentChildren = parent ? appDom.getChildNodes(dom, parent).children : []; - const totalLayoutColumnSizes = parentChildren.reduce( - (acc, child) => acc + (nodesInfo[child.id]?.rect?.width || 0), - 0, - ); + domApi.update((draft) => { + if (draggedEdge === RECTANGLE_EDGE_LEFT || draggedEdge === RECTANGLE_EDGE_RIGHT) { + const parentChildren = parent ? appDom.getChildNodes(draft, parent).children : []; + const totalLayoutColumnSizes = parentChildren.reduce( + (acc, child) => acc + (nodesInfo[child.id]?.rect?.width || 0), + 0, + ); - const normalizeColumnSize = (size: number) => - Math.max(0, size * parentChildren.length) / totalLayoutColumnSizes; + const normalizeColumnSize = (size: number) => + Math.max(0, size * parentChildren.length) / totalLayoutColumnSizes; - if (draggedEdge === RECTANGLE_EDGE_LEFT) { - const previousSibling = appDom.getSiblingBeforeNode(dom, draggedNode, 'children'); + if (draggedEdge === RECTANGLE_EDGE_LEFT) { + const previousSibling = appDom.getSiblingBeforeNode(draft, draggedNode, 'children'); - if (previousSibling) { - const previousSiblingInfo = nodesInfo[previousSibling.id]; - const previousSiblingRect = previousSiblingInfo?.rect; + if (previousSibling) { + const previousSiblingInfo = nodesInfo[previousSibling.id]; + const previousSiblingRect = previousSiblingInfo?.rect; - if (previousSiblingRect) { - const updatedDraggedNodeColumnSize = normalizeColumnSize(resizePreviewRect.width); - const updatedPreviousSiblingColumnSize = normalizeColumnSize( - previousSiblingRect.width - (resizePreviewRect.width - draggedNodeRect.width), - ); + if (previousSiblingRect) { + const updatedDraggedNodeColumnSize = normalizeColumnSize(resizePreviewRect.width); + const updatedPreviousSiblingColumnSize = normalizeColumnSize( + previousSiblingRect.width - (resizePreviewRect.width - draggedNodeRect.width), + ); - draftDom = appDom.setNodeNamespacedProp( - draftDom, - draggedNode, - 'layout', - 'columnSize', - appDom.createConst(updatedDraggedNodeColumnSize), - ); - draftDom = appDom.setNodeNamespacedProp( - draftDom, - previousSibling, - 'layout', - 'columnSize', - appDom.createConst(updatedPreviousSiblingColumnSize), - ); + draft = appDom.setNodeNamespacedProp( + draft, + draggedNode, + 'layout', + 'columnSize', + appDom.createConst(updatedDraggedNodeColumnSize), + ); + draft = appDom.setNodeNamespacedProp( + draft, + previousSibling, + 'layout', + 'columnSize', + appDom.createConst(updatedPreviousSiblingColumnSize), + ); + } } } - } - if (draggedEdge === RECTANGLE_EDGE_RIGHT) { - const nextSibling = appDom.getSiblingAfterNode(dom, draggedNode, 'children'); + if (draggedEdge === RECTANGLE_EDGE_RIGHT) { + const nextSibling = appDom.getSiblingAfterNode(draft, draggedNode, 'children'); - if (nextSibling) { - const nextSiblingInfo = nodesInfo[nextSibling.id]; - const nextSiblingRect = nextSiblingInfo?.rect; + if (nextSibling) { + const nextSiblingInfo = nodesInfo[nextSibling.id]; + const nextSiblingRect = nextSiblingInfo?.rect; - if (nextSiblingRect) { - const updatedDraggedNodeColumnSize = normalizeColumnSize(resizePreviewRect.width); - const updatedNextSiblingColumnSize = normalizeColumnSize( - nextSiblingRect.width - (resizePreviewRect.width - draggedNodeRect.width), - ); + if (nextSiblingRect) { + const updatedDraggedNodeColumnSize = normalizeColumnSize(resizePreviewRect.width); + const updatedNextSiblingColumnSize = normalizeColumnSize( + nextSiblingRect.width - (resizePreviewRect.width - draggedNodeRect.width), + ); - draftDom = appDom.setNodeNamespacedProp( - draftDom, - draggedNode, - 'layout', - 'columnSize', - appDom.createConst(updatedDraggedNodeColumnSize), - ); - draftDom = appDom.setNodeNamespacedProp( - draftDom, - nextSibling, - 'layout', - 'columnSize', - appDom.createConst(updatedNextSiblingColumnSize), - ); + draft = appDom.setNodeNamespacedProp( + draft, + draggedNode, + 'layout', + 'columnSize', + appDom.createConst(updatedDraggedNodeColumnSize), + ); + draft = appDom.setNodeNamespacedProp( + draft, + nextSibling, + 'layout', + 'columnSize', + appDom.createConst(updatedNextSiblingColumnSize), + ); + } } } } - } - if (draggedEdge === RECTANGLE_EDGE_BOTTOM) { - const resizableHeightProp = draggedNodeInfo?.componentConfig?.resizableHeightProp; + if (draggedEdge === RECTANGLE_EDGE_BOTTOM) { + const resizableHeightProp = draggedNodeInfo?.componentConfig?.resizableHeightProp; - if (resizableHeightProp) { - draftDom = appDom.setNodeNamespacedProp( - draftDom, - draggedNode, - 'props', - resizableHeightProp, - appDom.createConst(resizePreviewRect.height), - ); + if (resizableHeightProp) { + draft = appDom.setNodeNamespacedProp( + draft, + draggedNode, + 'props', + resizableHeightProp, + appDom.createConst(resizePreviewRect.height), + ); + } } - } - updateDom(draftDom); + return normalizePageRowColumnSizes(draft); + }); } api.dragEnd(); }, - [api, dom, draggedEdge, draggedNode, nodesInfo, resizePreviewElement, updateDom], + [ + api, + dom, + domApi, + draggedEdge, + draggedNode, + nodesInfo, + normalizePageRowColumnSizes, + resizePreviewElement, + ], ); return ( diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx index 7e6a8c59299..20b0698057a 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx @@ -57,11 +57,12 @@ export default function RenderPanel({ className }: RenderPanelProps) { const newValue: unknown = typeof event.value === 'function' ? event.value(actual?.value) : event.value; - const updatedDom = appDom.setNodeNamespacedProp(dom, node, 'props', event.prop, { - type: 'const', - value: newValue, - }); - domApi.update(updatedDom); + domApi.update((draft) => + appDom.setNodeNamespacedProp(draft, node, 'props', event.prop, { + type: 'const', + value: newValue, + }), + ); return; } case 'pageStateUpdated': { diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ThemeEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ThemeEditor.tsx index b57f090ef4f..ae9968266c0 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ThemeEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ThemeEditor.tsx @@ -82,8 +82,7 @@ export default function ComponentEditor({ className }: ComponentEditorProps) { theme: {}, attributes: {}, }); - const updatedDom = appDom.addNode(dom, newTheme, app, 'themes'); - domApi.update(updatedDom); + domApi.update((draft) => appDom.addNode(draft, newTheme, app, 'themes')); }; return ( @@ -94,11 +93,12 @@ export default function ComponentEditor({ className }: ComponentEditorProps) { exclusive value={appDom.fromConstPropValue(theme.theme?.['palette.mode']) || 'light'} onChange={(event, newValue) => { - const updatedDom = appDom.setNodeNamespacedProp(dom, theme, 'theme', 'palette.mode', { - type: 'const', - value: newValue, - }); - domApi.update(updatedDom); + domApi.update((draft) => + appDom.setNodeNamespacedProp(draft, theme, 'theme', 'palette.mode', { + type: 'const', + value: newValue, + }), + ); }} aria-label="Mode" > @@ -115,34 +115,24 @@ export default function ComponentEditor({ className }: ComponentEditorProps) { name="primary" value={appDom.fromConstPropValue(theme.theme?.['palette.primary.main']) || ''} onChange={(newValue) => { - const updatedDom = appDom.setNodeNamespacedProp( - dom, - theme, - 'theme', - 'palette.primary.main', - { + domApi.update((draft) => + appDom.setNodeNamespacedProp(draft, theme, 'theme', 'palette.primary.main', { type: 'const', value: newValue, - }, + }), ); - domApi.update(updatedDom); }} /> { - const updatedDom = appDom.setNodeNamespacedProp( - dom, - theme, - 'theme', - 'palette.secondary.main', - { + domApi.update((draft) => + appDom.setNodeNamespacedProp(draft, theme, 'theme', 'palette.secondary.main', { type: 'const', value: newValue, - }, + }), ); - domApi.update(updatedDom); }} /> diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/UrlQueryEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/UrlQueryEditor.tsx index 8763bcdd4f4..b6608dcacf2 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/UrlQueryEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/UrlQueryEditor.tsx @@ -39,17 +39,18 @@ export default function UrlQueryEditor({ pageNodeId }: UrlQueryEditorProps) { }, [dialogOpen, value]); const handleSave = React.useCallback(() => { - const updatedDom = appDom.setNodeNamespacedProp( - dom, - page, - 'attributes', - 'parameters', - appDom.createConst(input || []), + domApi.update((draft) => + appDom.setNodeNamespacedProp( + draft, + page, + 'attributes', + 'parameters', + appDom.createConst(input || []), + ), ); - domApi.update(updatedDom); handleDialogClose(); - }, [dom, page, input, domApi, handleDialogClose]); + }, [domApi, handleDialogClose, input, page]); return ( diff --git a/packages/toolpad-app/src/toolpad/DomLoader.tsx b/packages/toolpad-app/src/toolpad/DomLoader.tsx index 97a93a2a3df..9ee4a3ce28a 100644 --- a/packages/toolpad-app/src/toolpad/DomLoader.tsx +++ b/packages/toolpad-app/src/toolpad/DomLoader.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { NodeId, BindableAttrValues } from '@mui/toolpad-core'; +import { NodeId } from '@mui/toolpad-core'; import invariant from 'invariant'; import { debounce, DebouncedFunc } from 'lodash-es'; import * as appDom from '../appDom'; @@ -40,15 +40,9 @@ export type DomAction = nodeId: NodeId; name: string; } - | { - type: 'DOM_SET_NODE_NAMESPACE'; - node: appDom.AppDomNode; - namespace: string; - value: BindableAttrValues | null; - } | { type: 'DOM_UPDATE'; - updatedDom: appDom.AppDom; + updater: (dom: appDom.AppDom) => appDom.AppDom; selectedNodeId?: NodeId | null; } | { @@ -70,11 +64,8 @@ export function domReducer(dom: appDom.AppDom, action: DomAction): appDom.AppDom const node = appDom.getNode(dom, action.nodeId); return appDom.setNodeName(dom, node, action.name); } - case 'DOM_SET_NODE_NAMESPACE': { - return appDom.setNodeNamespace(dom, action.node, action.namespace, action.value); - } case 'DOM_UPDATE': { - return action.updatedDom; + return action.updater(dom); } case 'DOM_SAVE_NODE': { return appDom.saveNode(dom, action.node); @@ -215,10 +206,10 @@ function createDomApi( setNodeName(nodeId: NodeId, name: string) { dispatch({ type: 'DOM_SET_NODE_NAME', nodeId, name }); }, - update(dom: appDom.AppDom, selectedNodeId?: NodeId | null) { + update(updater: (dom: appDom.AppDom) => appDom.AppDom, selectedNodeId?: NodeId | null) { dispatch({ type: 'DOM_UPDATE', - updatedDom: dom, + updater, selectedNodeId, }); }, @@ -228,18 +219,6 @@ function createDomApi( node, }); }, - setNodeNamespace>( - node: Node, - namespace: Namespace, - value: Node[Namespace] | null, - ) { - dispatch({ - type: 'DOM_SET_NODE_NAMESPACE', - namespace, - node, - value: value as BindableAttrValues | null, - }); - }, selectNode(nodeId: NodeId) { dispatch({ type: 'SELECT_NODE', From 5d1fd306248d99d75b51098c08c2a44b11338405 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 16 Dec 2022 12:53:01 +0000 Subject: [PATCH 17/17] Readd method --- .../toolpad-app/src/toolpad/DomLoader.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/toolpad-app/src/toolpad/DomLoader.tsx b/packages/toolpad-app/src/toolpad/DomLoader.tsx index 9ee4a3ce28a..844dcfdbcae 100644 --- a/packages/toolpad-app/src/toolpad/DomLoader.tsx +++ b/packages/toolpad-app/src/toolpad/DomLoader.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { NodeId } from '@mui/toolpad-core'; +import { NodeId, BindableAttrValues } from '@mui/toolpad-core'; import invariant from 'invariant'; import { debounce, DebouncedFunc } from 'lodash-es'; import * as appDom from '../appDom'; @@ -40,6 +40,12 @@ export type DomAction = nodeId: NodeId; name: string; } + | { + type: 'DOM_SET_NODE_NAMESPACE'; + node: appDom.AppDomNode; + namespace: string; + value: BindableAttrValues | null; + } | { type: 'DOM_UPDATE'; updater: (dom: appDom.AppDom) => appDom.AppDom; @@ -64,6 +70,9 @@ export function domReducer(dom: appDom.AppDom, action: DomAction): appDom.AppDom const node = appDom.getNode(dom, action.nodeId); return appDom.setNodeName(dom, node, action.name); } + case 'DOM_SET_NODE_NAMESPACE': { + return appDom.setNodeNamespace(dom, action.node, action.namespace, action.value); + } case 'DOM_UPDATE': { return action.updater(dom); } @@ -219,6 +228,18 @@ function createDomApi( node, }); }, + setNodeNamespace>( + node: Node, + namespace: Namespace, + value: Node[Namespace] | null, + ) { + dispatch({ + type: 'DOM_SET_NODE_NAMESPACE', + namespace, + node, + value: value as BindableAttrValues | null, + }); + }, selectNode(nodeId: NodeId) { dispatch({ type: 'SELECT_NODE',