diff --git a/docs/data/toolpad/reference/components/chart.md b/docs/data/toolpad/reference/components/chart.md index 1b3f52b15e0..1a071050abd 100644 --- a/docs/data/toolpad/reference/components/chart.md +++ b/docs/data/toolpad/reference/components/chart.md @@ -8,8 +8,7 @@ A chart component. ## Properties -| Name | Type | Default | Description | -| :------------------------------------ | :------------------------------------ | :------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| data | array | | The data to be displayed. | -| height | number | 300 | The height of the chart. | -| sx | object | | The [`sx` prop](https://mui.com/system/getting-started/the-sx-prop/) is used for defining custom styles that have access to the theme. All MUI System properties are available via the `sx` prop. In addition, the `sx` prop allows you to specify any other CSS rules you may need. | +| Name | Type | Default | Description | +| :---------------------------------- | :------------------------------------ | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| data | array | | The data to be displayed. | +| sx | object | | The [`sx` prop](https://mui.com/system/getting-started/the-sx-prop/) is used for defining custom styles that have access to the theme. All MUI System properties are available via the `sx` prop. In addition, the `sx` prop allows you to specify any other CSS rules you may need. | diff --git a/docs/data/toolpad/reference/components/data-grid.md b/docs/data/toolpad/reference/components/data-grid.md index e0b1b9dd08d..bc16562f0da 100644 --- a/docs/data/toolpad/reference/components/data-grid.md +++ b/docs/data/toolpad/reference/components/data-grid.md @@ -19,7 +19,6 @@ The datagrid lets users display tabular data in a flexible grid. | rowIdField | string | | Defines which column contains the [id](https://mui.com/x/react-data-grid/row-definition/#row-identifier) that uniquely identifies each row. | | selection | object | null | The currently selected row. Or `null` in case no row has been selected. | | density | string | "compact" | The [density](https://mui.com/x/react-data-grid/accessibility/#density-prop) of the rows. Possible values are `compact`, `standard`, or `comfortable`. | -| height | number | 350 | The height of the data grid. | | loading | boolean | | Displays a loading animation indicating the data grid isn't ready to present data yet. | | hideToolbar | boolean | | Hide the toolbar area that contains the data grid user controls. | | sx | object | | The [`sx` prop](https://mui.com/system/getting-started/the-sx-prop/) is used for defining custom styles that have access to the theme. All MUI System properties are available via the `sx` prop. In addition, the `sx` prop allows you to specify any other CSS rules you may need. | diff --git a/docs/data/toolpad/reference/components/index.md b/docs/data/toolpad/reference/components/index.md index 43b864aea74..2ab059b41c2 100644 --- a/docs/data/toolpad/reference/components/index.md +++ b/docs/data/toolpad/reference/components/index.md @@ -20,6 +20,7 @@ - [PageRow](/toolpad/reference/components/page-row/) - [Paper](/toolpad/reference/components/paper/) - [Select](/toolpad/reference/components/select/) +- [Spacer](/toolpad/reference/components/spacer/) - [Stack](/toolpad/reference/components/stack/) - [Tabs](/toolpad/reference/components/tabs/) - [Text](/toolpad/reference/components/text/) diff --git a/docs/data/toolpad/reference/components/manifest.json b/docs/data/toolpad/reference/components/manifest.json index 4b9f04a1960..2ad5bb43e2a 100644 --- a/docs/data/toolpad/reference/components/manifest.json +++ b/docs/data/toolpad/reference/components/manifest.json @@ -73,6 +73,10 @@ "title": "Select", "pathname": "/toolpad/reference/components/select" }, + { + "title": "Spacer", + "pathname": "/toolpad/reference/components/spacer" + }, { "title": "Stack", "pathname": "/toolpad/reference/components/stack" diff --git a/docs/data/toolpad/reference/components/spacer.md b/docs/data/toolpad/reference/components/spacer.md new file mode 100644 index 00000000000..7e9572506ac --- /dev/null +++ b/docs/data/toolpad/reference/components/spacer.md @@ -0,0 +1,14 @@ + + +# Spacer + +

API docs for the Toolpad Spacer component.

+ +Spacer component. +It allows for creating space between elements. + +## Properties + +| Name | Type | Default | Description | +| :-------------------------------- | :------------------------------------ | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| sx | object | | The [`sx` prop](https://mui.com/system/getting-started/the-sx-prop/) is used for defining custom styles that have access to the theme. All MUI System properties are available via the `sx` prop. In addition, the `sx` prop allows you to specify any other CSS rules you may need. | diff --git a/docs/pages/toolpad/reference/components/spacer.js b/docs/pages/toolpad/reference/components/spacer.js new file mode 100644 index 00000000000..bfad331aa56 --- /dev/null +++ b/docs/pages/toolpad/reference/components/spacer.js @@ -0,0 +1,9 @@ +/* ATTENTION: DO NOT EDIT! This file has been auto-generated using `pnpm docs:build:api`. */ + +import * as React from 'react'; +import MarkdownDocs from '@mui/monorepo/docs/src/modules/components/MarkdownDocs'; +import * as pageProps from '../../../../data/toolpad/reference/components/spacer.md?@mui/markdown'; + +export default function Page() { + return ; +} diff --git a/docs/schemas/v1/definitions.json b/docs/schemas/v1/definitions.json index 7a603836c30..9b28c57d5fa 100644 --- a/docs/schemas/v1/definitions.json +++ b/docs/schemas/v1/definitions.json @@ -579,6 +579,10 @@ "columnSize": { "type": "number", "description": "The width this element takes up, expressed in terms of columns on the page." + }, + "height": { + "type": "number", + "description": "The height this element takes up, in pixels." } }, "additionalProperties": false, diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index bc2d7ef2ec3..0c172e21a38 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -442,10 +442,8 @@ function getQueryConfigBindings({ enabled, refetchInterval }: appDom.QueryNode[' } function isBindableProp(componentConfig: ComponentConfig, propName: string) { - const isResizableHeightProp = propName === componentConfig.resizableHeightProp; const argType = componentConfig.argTypes?.[propName]; return ( - !isResizableHeightProp && argType?.control?.bindable !== false && argType?.type !== 'template' && argType?.type !== 'event' @@ -1195,6 +1193,8 @@ function RenderedNodeContent({ node, childNodeGroups, Component }: RenderedNodeC display: 'flex', alignItems: boundLayoutProps.verticalAlign, justifyContent: boundLayoutProps.horizontalAlign, + height: node.layout?.height ?? componentConfig.defaultLayoutHeight, + minHeight: '100%', }} ref={nodeRef} data-toolpad-node-id={nodeId} diff --git a/packages/toolpad-app/src/runtime/toolpadComponents/index.tsx b/packages/toolpad-app/src/runtime/toolpadComponents/index.tsx index 35baa6a4bd6..77bb5872a8b 100644 --- a/packages/toolpad-app/src/runtime/toolpadComponents/index.tsx +++ b/packages/toolpad-app/src/runtime/toolpadComponents/index.tsx @@ -2,6 +2,7 @@ import * as appDom from '@mui/toolpad-core/appDom'; export const PAGE_ROW_COMPONENT_ID = 'PageRow'; export const PAGE_COLUMN_COMPONENT_ID = 'PageColumn'; +export const SPACER_COMPONENT_ID = 'Spacer'; export const STACK_COMPONENT_ID = 'Stack'; export const FORM_COMPONENT_ID = 'Form'; diff --git a/packages/toolpad-app/src/server/localMode.ts b/packages/toolpad-app/src/server/localMode.ts index 28bfda9cc6f..db34b872c8a 100644 --- a/packages/toolpad-app/src/server/localMode.ts +++ b/packages/toolpad-app/src/server/localMode.ts @@ -518,6 +518,7 @@ function expandFromDom( name: node.name, layout: undefinedWhenEmpty({ columnSize: node.layout?.columnSize, + height: node.layout?.height, horizontalAlign: stringOnly(node.layout?.horizontalAlign), verticalAlign: stringOnly(node.layout?.verticalAlign), }), @@ -715,7 +716,14 @@ function mergePageIntoDom(dom: appDom.AppDom, pageName: string, pageFile: Page): return dom; } -function optimizePageElement(element: ElementType): ElementType { +function optimizePageElement( + element: ElementType, + isPageChild = false, +): ElementType | ElementType[] { + if (isPageChild && element.component === PAGE_COLUMN_COMPONENT_ID) { + return (element.children || []).flatMap((child) => optimizePageElement(child, true)); + } + const isLayoutElement = (possibleLayoutElement: ElementType): boolean => possibleLayoutElement.component === PAGE_ROW_COMPONENT_ID || possibleLayoutElement.component === PAGE_COLUMN_COMPONENT_ID; @@ -736,7 +744,7 @@ function optimizePageElement(element: ElementType): ElementType { return { ...element, - children: element.children && element.children.map(optimizePageElement), + children: element.children && element.children.flatMap((child) => optimizePageElement(child)), }; } @@ -745,7 +753,7 @@ function optimizePage(page: Page): Page { ...page, spec: { ...page.spec, - content: page.spec?.content?.map(optimizePageElement), + content: page.spec?.content?.flatMap((element) => optimizePageElement(element, true)), }, }; } diff --git a/packages/toolpad-app/src/server/schema.ts b/packages/toolpad-app/src/server/schema.ts index a44b496a6d2..44170619fe1 100644 --- a/packages/toolpad-app/src/server/schema.ts +++ b/packages/toolpad-app/src/server/schema.ts @@ -215,6 +215,7 @@ const baseElementSchema = z.object({ .number() .optional() .describe('The width this element takes up, expressed in terms of columns on the page.'), + height: z.number().optional().describe('The height this element takes up, in pixels.'), }) .optional() .describe('Layout properties for this element.'), diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalogItem.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalogItem.tsx index 5fe3f267f1d..f6e4e04ed6e 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalogItem.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalogItem.tsx @@ -33,6 +33,7 @@ import TagIcon from '@mui/icons-material/Tag'; import PasswordIcon from '@mui/icons-material/Password'; import LinkIcon from '@mui/icons-material/Link'; import TextFormatIcon from '@mui/icons-material/TextFormat'; +import SpaceBarIcon from '@mui/icons-material/SpaceBar'; import PieChartIcon from '@mui/icons-material/PieChart'; const iconMap = new Map>([ @@ -70,6 +71,7 @@ const iconMap = new Map>([ ['PageRow', TableRowsIcon], ['PageColumn', ViewColumnIcon], ['Metric', TagIcon], + ['Spacer', SpaceBarIcon], ]); type ComponentItemKind = 'future' | 'builtIn' | 'create' | 'custom'; diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentEditor.tsx index b4c3bb2efd9..d0e695155e2 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentEditor.tsx @@ -42,7 +42,6 @@ function shouldRenderControl

( propTypeDef: ArgTypeDefinition

, propName: keyof P, props: P, - componentConfig: ComponentConfig

, ) { if (propTypeDef.type === 'element' || propTypeDef.type === 'template') { return ( @@ -60,10 +59,6 @@ function shouldRenderControl

( return propTypeDef.visible(props); } - if (componentConfig.resizableHeightProp && propName === componentConfig.resizableHeightProp) { - return false; - } - return true; } @@ -141,7 +136,7 @@ function ComponentPropsEditor

({ {category}: {argTypeEntries.map(([propName, propTypeDef]) => - propTypeDef && shouldRenderControl(propTypeDef, propName, props, componentConfig) ? ( + propTypeDef && shouldRenderControl(propTypeDef, propName, props) ? (

HUD_HEIGHT ? HINT_POSITION_TOP : HINT_POSITION_BOTTOM; + let hintPosition: HintPosition = HINT_POSITION_BOTTOM; + if (rect.y > HUD_HEIGHT) { + hintPosition = HINT_POSITION_TOP; + } const interactiveNodeClipPath = React.useMemo( () => 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 154a8509540..10afae7efd2 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderOverlay.tsx @@ -24,6 +24,7 @@ import { PAGE_COLUMN_COMPONENT_ID, isFormComponent, FORM_COMPONENT_ID, + getElementNodeComponentId, } from '../../../../runtime/toolpadComponents'; import { getRectanglePointActiveEdge, @@ -50,9 +51,7 @@ import { removePageLayoutNode, } from '../../pageLayout'; -const VERTICAL_RESIZE_SNAP_UNITS = 2; // px - -const MIN_RESIZABLE_ELEMENT_HEIGHT = 100; // px +const VERTICAL_RESIZE_SNAP_UNITS = 4; // px const overlayClasses = { hud: 'Toolpad_Hud', @@ -306,7 +305,7 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) { const isFirstChild = parent && appDom.isElement(parent) && nodeParentProp - ? appDom.getNodeFirstChild(dom, parent, node.parentProp)?.id === node.id + ? appDom.getNodeFirstChild(dom, parent, nodeParentProp)?.id === node.id : false; const isLastChild = parent && appDom.isElement(parent) && nodeParentProp @@ -332,11 +331,18 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) { (event: React.MouseEvent) => { event.stopPropagation(); - api.edgeDragStart({ nodeId: node.id, edge }); + const parent = appDom.getParent(dom, node); - selectNode(node.id); + const isPageColumnChild = parent ? appDom.isElement(parent) && isPageColumn(parent) : false; + const isResizingVertically = edge === RECTANGLE_EDGE_TOP || edge === RECTANGLE_EDGE_BOTTOM; + + const nodeToResize = parent && isPageColumnChild && !isResizingVertically ? parent : node; + + api.edgeDragStart({ nodeId: nodeToResize.id, edge }); + + selectNode(nodeToResize.id); }, - [api, selectNode], + [api, dom, selectNode], ); const handleKeyDown = React.useCallback( @@ -1180,6 +1186,10 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) { const cursorPos = bridge?.canvasCommands.getViewCoordinates(event.clientX, event.clientY); + const previousSibling = appDom.getSiblingBeforeNode(dom, draggedNode, 'children'); + const previousSiblingInfo = previousSibling && nodesInfo[previousSibling.id]; + const previousSiblingRect = previousSiblingInfo?.rect; + if (draggedNodeRect && parentRect && resizePreviewElement && cursorPos) { if (draggedEdge === RECTANGLE_EDGE_LEFT || draggedEdge === RECTANGLE_EDGE_RIGHT) { let snappedToGridCursorRelativePosX = cursorPos.x - draggedNodeRect.x; @@ -1197,10 +1207,6 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) { } } - const previousSibling = appDom.getSiblingBeforeNode(dom, draggedNode, 'children'); - const previousSiblingInfo = previousSibling && nodesInfo[previousSibling.id]; - const previousSiblingRect = previousSiblingInfo?.rect; - if ( draggedEdge === RECTANGLE_EDGE_LEFT && cursorPos.x > @@ -1235,9 +1241,18 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) { } } + const defaultMinimumResizableHeight = 16; + + const minimumVerticalResizeHeight = + draggedNodeInfo.componentConfig?.minimumLayoutHeight ?? defaultMinimumResizableHeight; + + const previousSiblingMinimumVerticalResizeHeight = + previousSiblingInfo?.componentConfig?.minimumLayoutHeight ?? + defaultMinimumResizableHeight; + if ( draggedEdge === RECTANGLE_EDGE_BOTTOM && - cursorPos.y > draggedNodeRect.y + MIN_RESIZABLE_ELEMENT_HEIGHT + cursorPos.y > draggedNodeRect.y + minimumVerticalResizeHeight ) { const snappedToGridCursorRelativePosY = Math.ceil((cursorPos.y - draggedNodeRect.y) / VERTICAL_RESIZE_SNAP_UNITS) * @@ -1248,6 +1263,27 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) { resizePreviewElement.style.transformOrigin = '50% 0'; resizePreviewElement.style.transform = `scaleY(${updatedTransformScale})`; } + + if ( + draggedEdge === RECTANGLE_EDGE_TOP && + cursorPos.y < draggedNodeRect.y + draggedNodeRect.height - minimumVerticalResizeHeight && + (!previousSiblingRect || + cursorPos.y > + draggedNodeRect.y - + previousSiblingRect.height + + previousSiblingMinimumVerticalResizeHeight) + ) { + const snappedToGridCursorRelativePosY = + Math.ceil( + (draggedNodeRect.y + draggedNodeRect.height - cursorPos.y) / + VERTICAL_RESIZE_SNAP_UNITS, + ) * VERTICAL_RESIZE_SNAP_UNITS; + + const updatedTransformScale = snappedToGridCursorRelativePosY / draggedNodeRect.height; + + resizePreviewElement.style.transformOrigin = '50% 100%'; + resizePreviewElement.style.transform = `scaleY(${updatedTransformScale})`; + } } }, [bridge, dom, draggedEdge, draggedNode, nodesInfo], @@ -1269,41 +1305,41 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) { if (draggedNodeRect && resizePreviewRect) { domApi.update((draft) => { + const previousSibling = appDom.getSiblingBeforeNode(draft, draggedNode, 'children'); + + let previousSiblingInfo = null; + let previousSiblingRect = null; + if (previousSibling) { + previousSiblingInfo = nodesInfo[previousSibling.id]; + previousSiblingRect = previousSiblingInfo?.rect; + } + if (draggedEdge === RECTANGLE_EDGE_LEFT || draggedEdge === RECTANGLE_EDGE_RIGHT) { if (draggedEdge === RECTANGLE_EDGE_LEFT) { - const previousSibling = appDom.getSiblingBeforeNode(draft, draggedNode, 'children'); + if (previousSibling && previousSiblingRect) { + const totalResizedColumnsSize = + (draggedNode.layout?.columnSize || 1) + (previousSibling.layout?.columnSize || 1); + const totalResizedColumnsWidth = draggedNodeRect.width + previousSiblingRect.width; - if (previousSibling) { - const previousSiblingInfo = nodesInfo[previousSibling.id]; - const previousSiblingRect = previousSiblingInfo?.rect; - - if (previousSiblingRect) { - const totalResizedColumnsSize = - (draggedNode.layout?.columnSize || 1) + - (previousSibling.layout?.columnSize || 1); - const totalResizedColumnsWidth = - draggedNodeRect.width + previousSiblingRect.width; + const updatedDraggedNodeColumnSize = + (resizePreviewRect.width / totalResizedColumnsWidth) * totalResizedColumnsSize; + const updatedPreviousSiblingColumnSize = + totalResizedColumnsSize - updatedDraggedNodeColumnSize; - const updatedDraggedNodeColumnSize = - (resizePreviewRect.width / totalResizedColumnsWidth) * totalResizedColumnsSize; - const updatedPreviousSiblingColumnSize = - totalResizedColumnsSize - updatedDraggedNodeColumnSize; - - draft = appDom.setNodeNamespacedProp( - draft, - draggedNode, - 'layout', - 'columnSize', - updatedDraggedNodeColumnSize, - ); - draft = appDom.setNodeNamespacedProp( - draft, - previousSibling, - 'layout', - 'columnSize', - updatedPreviousSiblingColumnSize, - ); - } + draft = appDom.setNodeNamespacedProp( + draft, + draggedNode, + 'layout', + 'columnSize', + updatedDraggedNodeColumnSize, + ); + draft = appDom.setNodeNamespacedProp( + draft, + previousSibling, + 'layout', + 'columnSize', + updatedPreviousSiblingColumnSize, + ); } } if (draggedEdge === RECTANGLE_EDGE_RIGHT) { @@ -1342,18 +1378,32 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) { } } - if (draggedEdge === RECTANGLE_EDGE_BOTTOM) { - const resizableHeightProp = draggedNodeInfo?.componentConfig?.resizableHeightProp; + if (draggedEdge === RECTANGLE_EDGE_BOTTOM || draggedEdge === RECTANGLE_EDGE_TOP) { + const isValidTopResize = + draggedEdge === RECTANGLE_EDGE_TOP && + previousSibling && + previousSiblingRect && + !isPageRow(previousSibling); - if (resizableHeightProp) { + if (draggedEdge === RECTANGLE_EDGE_BOTTOM || isValidTopResize) { draft = appDom.setNodeNamespacedProp( draft, draggedNode, - 'props', - resizableHeightProp, + 'layout', + 'height', resizePreviewRect.height, ); } + + if (isValidTopResize && previousSiblingRect) { + draft = appDom.setNodeNamespacedProp( + draft, + previousSibling, + 'layout', + 'height', + previousSiblingRect.height + (draggedNodeRect.height - resizePreviewRect.height), + ); + } } return draft; @@ -1400,16 +1450,33 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) { const parent = appDom.getParent(dom, node); const isPageNode = appDom.isPage(node); + const isElementNode = appDom.isElement(node); const isPageRowChild = parent ? appDom.isElement(parent) && isPageRow(parent) : false; const isPageColumnChild = parent ? appDom.isElement(parent) && isPageColumn(parent) : false; + // @TODO: Improve solution for resizing from top, it's still not a great UX + // const nodeParentProp = node.parentProp; + // const isFirstChild = + // parent && appDom.isElement(parent) && nodeParentProp + // ? appDom.getNodeFirstChild(dom, parent, nodeParentProp)?.id === node.id + // : false; + const isSelected = selectedNode && !newNode ? selectedNode.id === node.id : false; const isHovered = hoveredNodeId === node.id; - const isHorizontallyResizable = isSelected && (isPageRowChild || isPageColumnChild); + const isHorizontallyResizable = isPageRowChild || isPageColumnChild; + + const nodeComponentId = isElementNode ? getElementNodeComponentId(node) : null; + + // @TODO: Enable vertical resizing for all component types when there is a better solution for adjusting size of other elements in same row after resizing const isVerticallyResizable = - isSelected && Boolean(nodeInfo?.componentConfig?.resizableHeightProp); + isElementNode && + !isPageRow(node) && + !isPageColumn(node) && + (nodeComponentId === 'Chart' || + nodeComponentId === 'DataGrid' || + nodeComponentId === 'Spacer'); const isResizing = Boolean(draggedEdge); const isResizingNode = isResizing && node.id === draggedNodeId; @@ -1432,16 +1499,18 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) { onNodeDragStart={handleNodeDragStart(node as appDom.ElementNode)} onDuplicate={handleNodeDuplicate(node as appDom.ElementNode)} draggableEdges={[ - ...getNodeDraggableHorizontalEdges(parent && isPageColumnChild ? parent : node), - ...(isVerticallyResizable ? [RECTANGLE_EDGE_BOTTOM as RectangleEdge] : []), + ...(isHorizontallyResizable + ? getNodeDraggableHorizontalEdges(parent && isPageColumnChild ? parent : node) + : []), + ...(isVerticallyResizable + ? [ + RECTANGLE_EDGE_BOTTOM as RectangleEdge, + // @TODO: Improve solution for resizing from top, it's still not a great UX + // ...(!isFirstChild ? [RECTANGLE_EDGE_TOP as RectangleEdge] : []), + ] + : []), ]} - onEdgeDragStart={ - isHorizontallyResizable || isVerticallyResizable - ? handleEdgeDragStart( - parent && isPageColumnChild && !isVerticallyResizable ? parent : node, - ) - : undefined - } + onEdgeDragStart={isSelected ? handleEdgeDragStart(node) : undefined} onDelete={handleNodeDelete(node.id)} isResizing={isResizingNode} resizePreviewElementRef={resizePreviewElementRef} diff --git a/packages/toolpad-app/src/toolpad/AppEditor/pageLayout.ts b/packages/toolpad-app/src/toolpad/AppEditor/pageLayout.ts index 1b21dd22f5f..b68b5eba6ba 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/pageLayout.ts +++ b/packages/toolpad-app/src/toolpad/AppEditor/pageLayout.ts @@ -57,7 +57,6 @@ export function deleteOrphanedLayoutNodes( if ( lastContainerChild.parentProp && parentParent.parentIndex && - moveTargetNodeId !== parentParent.id && moveTargetNodeId !== lastContainerChild.id ) { if ( @@ -88,6 +87,10 @@ export function deleteOrphanedLayoutNodes( ); } + if (isPageColumn(lastContainerChild) && appDom.isPage(parentParent)) { + updatedDom = appDom.spreadNode(updatedDom, lastContainerChild); + } + orphanedLayoutNodeIds = [...orphanedLayoutNodeIds, parent.id]; } diff --git a/packages/toolpad-app/src/toolpad/AppEditor/toolpadComponents.tsx b/packages/toolpad-app/src/toolpad/AppEditor/toolpadComponents.tsx index 05e4743b616..1a8dca21e54 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/toolpadComponents.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/toolpadComponents.tsx @@ -5,6 +5,7 @@ import { FORM_COMPONENT_ID, PAGE_COLUMN_COMPONENT_ID, PAGE_ROW_COMPONENT_ID, + SPACER_COMPONENT_ID, STACK_COMPONENT_ID, } from '../../runtime/toolpadComponents'; import { useProject } from '../../project'; @@ -114,6 +115,10 @@ export const INTERNAL_COMPONENTS = new Map([ }, ], [FORM_COMPONENT_ID, { displayName: 'Form', builtIn: 'Form', synonyms: [] }], + [ + SPACER_COMPONENT_ID, + { displayName: 'Spacer', builtIn: 'Spacer', synonyms: ['margin', 'blank', 'empty', 'void'] }, + ], [ 'Password', { diff --git a/packages/toolpad-components/src/Chart.tsx b/packages/toolpad-components/src/Chart.tsx index db1fbae5b20..2aafb26088c 100644 --- a/packages/toolpad-components/src/Chart.tsx +++ b/packages/toolpad-components/src/Chart.tsx @@ -58,10 +58,9 @@ interface ChartProps extends BoxProps { data?: ChartData; loading?: boolean; error?: Error | string; - height: number; } -function Chart({ data = [], loading, error, height, sx }: ChartProps) { +function Chart({ data = [], loading, error, sx }: ChartProps) { const hasData = data.length > 0 && data.some((dataSeries) => (dataSeries.data ? dataSeries.data.length > 0 : false)); @@ -171,7 +170,7 @@ function Chart({ data = [], loading, error, height, sx }: ChartProps) { const firstDataSeries = chartSeries[0]; return ( - + {displayError ? : null} {loading && !error ? (
, @@ -1244,54 +1243,48 @@ const DataGridComponent = React.forwardRef(function DataGridComponent( return ( -
-
- - - - - -
+ + + + + + setActionResult(null)} apiRef={apiRef} /> -
+
); }); @@ -1302,7 +1295,8 @@ export default createBuiltin(DataGridComponent, { errorProp: 'error', loadingPropSource: ['rows', 'columns'], loadingProp: 'loading', - resizableHeightProp: 'height', + defaultLayoutHeight: 360, + minimumLayoutHeight: 100, argTypes: { rowsSource: { helperText: 'Defines how rows are provided to the grid.', @@ -1383,12 +1377,6 @@ export default createBuiltin(DataGridComponent, { enum: ['compact', 'standard', 'comfortable'], default: 'compact', }, - height: { - helperText: 'The height of the data grid.', - type: 'number', - default: 350, - minimum: 100, - }, loading: { helperText: "Displays a loading animation indicating the data grid isn't ready to present data yet.", diff --git a/packages/toolpad-components/src/Spacer.tsx b/packages/toolpad-components/src/Spacer.tsx new file mode 100644 index 00000000000..92f48675f94 --- /dev/null +++ b/packages/toolpad-components/src/Spacer.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { Box, BoxProps } from '@mui/material'; +import createBuiltin from './createBuiltin'; +import { SX_PROP_HELPER_TEXT } from './constants'; + +const SPACER_MINIMUM_HEIGHT = 60; // pixels + +function Spacer(props: BoxProps) { + return ; +} + +export default createBuiltin(Spacer, { + helperText: 'Spacer component.\nIt allows for creating space between elements.', + minimumLayoutHeight: SPACER_MINIMUM_HEIGHT, + argTypes: { + sx: { + helperText: SX_PROP_HELPER_TEXT, + type: 'object', + }, + }, +}); diff --git a/packages/toolpad-components/src/index.tsx b/packages/toolpad-components/src/index.tsx index d0f112ad58c..0ed2e3c78b1 100644 --- a/packages/toolpad-components/src/index.tsx +++ b/packages/toolpad-components/src/index.tsx @@ -36,6 +36,8 @@ export { default as Checkbox } from './Checkbox'; export { default as Form } from './Form'; +export { default as Spacer } from './Spacer'; + export { default as Metric } from './Metric'; export type { ColorScale, ColorScaleStop } from './Metric'; diff --git a/packages/toolpad-core/src/appDom.ts b/packages/toolpad-core/src/appDom.ts index 741fccb8d10..e14bef164b2 100644 --- a/packages/toolpad-core/src/appDom.ts +++ b/packages/toolpad-core/src/appDom.ts @@ -125,6 +125,7 @@ export interface ElementNode

extends AppDomNodeBase { readonly horizontalAlign?: BoxProps['justifyContent']; readonly verticalAlign?: BoxProps['alignItems']; readonly columnSize?: number; + readonly height?: number; }; } @@ -822,6 +823,22 @@ export function moveNode( return setNodeParent(dom, node, parent.id, parentProp, parentIndex); } +export function spreadNode(dom: AppDom, node: Child) { + const parent = getParent(dom, node); + const parentProp = node.parentProp; + + let draft = dom; + if (parent && parentProp && isElement(node)) { + for (const child of getChildNodes(draft, node).children) { + const parentIndex = getNewParentIndexBeforeNode(draft, node, parentProp); + draft = setNodeParent(draft, child, parent.id, parentProp, parentIndex); + } + draft = removeNode(draft, node.id); + } + + return draft; +} + export function nodeExists(dom: AppDom, nodeId: NodeId): boolean { return !!getMaybeNode(dom, nodeId); } @@ -1025,8 +1042,18 @@ export function duplicateNode( throw new Error(`Node "${node.id}" can't be duplicated, it must have a parent`); } - const fragment = cloneFragment(dom, node.id); - return addFragment(dom, fragment, parent.id, parentProp); + if (isElement(node)) { + const fragment = cloneFragment(dom, node.id); + return addFragment( + dom, + fragment, + parent.id, + parentProp, + getNewParentIndexAfterNode(dom, node, parentProp), + ); + } + + return dom; } const RENDERTREE_NODES = ['app', 'page', 'element', 'query', 'mutation', 'theme'] as const; diff --git a/packages/toolpad-core/src/types.ts b/packages/toolpad-core/src/types.ts index 05bb5f88a0a..3b1e4991c28 100644 --- a/packages/toolpad-core/src/types.ts +++ b/packages/toolpad-core/src/types.ts @@ -397,7 +397,7 @@ export type RuntimeEvent = { [K in keyof RuntimeEvents]: { type: K } & RuntimeEvents[K]; }[keyof RuntimeEvents]; -export interface ComponentConfig

{ +export interface ComponentConfig

> { /** * A short explanatory text that'll be shown in the editor UI when this component is referenced. * May contain Markdown. @@ -426,10 +426,13 @@ export interface ComponentConfig

{ */ layoutDirection?: 'vertical' | 'horizontal' | 'both'; /** - * Designates a property as "the resizable height property". If Toolpad detects any - * vertical resizing of the component it will forward it to this property. + * Initial height of the component container box. */ - resizableHeightProp?: keyof P & string; + defaultLayoutHeight?: number; + /** + * Minimum height that the component container box can be resized to. + */ + minimumLayoutHeight?: number; /** * Describes the individual properties for this component. */ diff --git a/test/models/ToolpadEditor.ts b/test/models/ToolpadEditor.ts index c7ee8b99c92..bce9c05aad6 100644 --- a/test/models/ToolpadEditor.ts +++ b/test/models/ToolpadEditor.ts @@ -1,6 +1,7 @@ import { setTimeout } from 'timers/promises'; import { expect, FrameLocator, Locator, Page } from '@playwright/test'; import { gotoIfNotCurrent } from './shared'; +import { waitForBoundingBox } from '../utils/locators'; class CreateComponentDialog { readonly page: Page; @@ -119,7 +120,7 @@ export class ToolpadEditor { await this.page.mouse.move( sourceBoundingBox!.x + sourceBoundingBox!.width / 2, sourceBoundingBox!.y + sourceBoundingBox!.height / 2, - { steps: 10 }, + { steps }, ); await this.page.mouse.down(); @@ -146,22 +147,20 @@ export class ToolpadEditor { await this.componentCatalog.hover(); - let pageRootBoundingBox; - await expect(async () => { - pageRootBoundingBox = await this.pageRoot.boundingBox(); - expect(pageRootBoundingBox).toBeTruthy(); - }).toPass(); + const pageRootBoundingBox = await waitForBoundingBox(this.pageRoot); if (!moveTargetX) { moveTargetX = pageRootBoundingBox!.x + pageRootBoundingBox!.width / 2; } if (!moveTargetY) { - moveTargetY = pageRootBoundingBox!.y + pageRootBoundingBox!.height / 2; + moveTargetY = pageRootBoundingBox!.y + pageRootBoundingBox!.height + 12; } const sourceLocator = this.getComponentCatalogItem(componentName); await expect(sourceLocator).toBeVisible(); + await sourceLocator.hover(); + await this.dragTo(sourceLocator, moveTargetX!, moveTargetY!, hasDrop, steps); await style.evaluate((elm) => elm.parentNode?.removeChild(elm)); diff --git a/test/utils/locators.ts b/test/utils/locators.ts index 56ac14c82da..a3dc2cf9fbe 100644 --- a/test/utils/locators.ts +++ b/test/utils/locators.ts @@ -2,15 +2,23 @@ import { Page, Locator, expect } from '../playwright/test'; type BoundingBox = NonNullable>>; -export async function getCenter(locator: Locator) { - let targetBoundingBox: BoundingBox | null = null; +export async function waitForBoundingBox( + locator: Locator, +): Promise> { + let boundingBox: BoundingBox | null = null; await expect(async () => { - targetBoundingBox = await locator.boundingBox(); - expect(targetBoundingBox).toBeTruthy(); - expect(targetBoundingBox!.width).toBeGreaterThan(0); - expect(targetBoundingBox!.height).toBeGreaterThan(0); + boundingBox = await locator.boundingBox(); + expect(boundingBox).toBeTruthy(); + expect(boundingBox!.width).toBeGreaterThan(0); + expect(boundingBox!.height).toBeGreaterThan(0); }).toPass(); + return boundingBox!; +} + +export async function getCenter(locator: Locator) { + const targetBoundingBox = await waitForBoundingBox(locator); + return { x: targetBoundingBox!.x + targetBoundingBox!.width / 2, y: targetBoundingBox!.y + targetBoundingBox!.height / 2, diff --git a/test/visual/components/fixture/toolpad/application.yml b/test/visual/components/fixture/toolpad/application.yml new file mode 100644 index 00000000000..6b008228134 --- /dev/null +++ b/test/visual/components/fixture/toolpad/application.yml @@ -0,0 +1,3 @@ +apiVersion: v1 +kind: application +spec: {} diff --git a/test/visual/components/fixture/toolpad/pages/blank/page.yml b/test/visual/components/fixture/toolpad/pages/blank/page.yml index dc75ccf1a19..a4a31d642ce 100644 --- a/test/visual/components/fixture/toolpad/pages/blank/page.yml +++ b/test/visual/components/fixture/toolpad/pages/blank/page.yml @@ -1,6 +1,9 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.50/docs/schemas/v1/definitions.json#properties/Page + apiVersion: v1 kind: page spec: - id: QYjq2fJ title: blank display: shell + alias: + - QYjq2fJ diff --git a/test/visual/components/fixture/toolpad/pages/components/page.yml b/test/visual/components/fixture/toolpad/pages/components/page.yml index a194a9adf87..c0d087af411 100644 --- a/test/visual/components/fixture/toolpad/pages/components/page.yml +++ b/test/visual/components/fixture/toolpad/pages/components/page.yml @@ -1,7 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.50/docs/schemas/v1/definitions.json#properties/Page + apiVersion: v1 kind: page spec: - id: f703ps3 title: components content: - component: Button @@ -116,3 +117,5 @@ spec: props: mode: switch label: Switch + alias: + - f703ps3 diff --git a/test/visual/components/fixture/toolpad/pages/dragdrop/page.yml b/test/visual/components/fixture/toolpad/pages/dragdrop/page.yml index f68ca1d9d0d..a1fc3c229f8 100644 --- a/test/visual/components/fixture/toolpad/pages/dragdrop/page.yml +++ b/test/visual/components/fixture/toolpad/pages/dragdrop/page.yml @@ -1,7 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.50/docs/schemas/v1/definitions.json#properties/Page + apiVersion: v1 kind: page spec: - id: 8ixPqyI title: dragdrop display: shell content: @@ -19,3 +20,5 @@ spec: content: contained - component: Paper name: paper1 + alias: + - 8ixPqyI diff --git a/test/visual/components/fixture/toolpad/pages/grids/page.yml b/test/visual/components/fixture/toolpad/pages/grids/page.yml new file mode 100644 index 00000000000..83b532d77bf --- /dev/null +++ b/test/visual/components/fixture/toolpad/pages/grids/page.yml @@ -0,0 +1,49 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.50/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: grids + display: shell + content: + - component: PageRow + name: pageRow + props: + justifyContent: start + children: + - component: PageColumn + name: pageColumn + layout: + columnSize: 1 + children: + - component: DataGrid + name: dataGrid + layout: + columnSize: 1 + height: 120 + props: + rows: + - id: one + columns: + - field: id + type: string + - component: DataGrid + name: dataGrid2 + props: + rows: + - id: two + columns: + - field: id + type: string + layout: + height: 120 + - component: DataGrid + name: dataGrid1 + props: + rows: + - id: three + columns: + - field: id + type: string + alias: + - Dh9u36B diff --git a/test/visual/components/fixture/toolpad/pages/rows/page.yml b/test/visual/components/fixture/toolpad/pages/rows/page.yml index fa6cb1e2196..6aaaf9868b6 100644 --- a/test/visual/components/fixture/toolpad/pages/rows/page.yml +++ b/test/visual/components/fixture/toolpad/pages/rows/page.yml @@ -1,7 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.50/docs/schemas/v1/definitions.json#properties/Page + apiVersion: v1 kind: page spec: - id: 5YDOftB title: rows display: shell content: @@ -14,3 +15,5 @@ spec: ] props: justifyContent: start + alias: + - 5YDOftB diff --git a/test/visual/components/fixture/toolpad/pages/text/page.yml b/test/visual/components/fixture/toolpad/pages/text/page.yml index 37e2ce79792..77701e105c8 100644 --- a/test/visual/components/fixture/toolpad/pages/text/page.yml +++ b/test/visual/components/fixture/toolpad/pages/text/page.yml @@ -1,7 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.50/docs/schemas/v1/definitions.json#properties/Page + apiVersion: v1 kind: page spec: - id: QYjq2fJ title: text display: shell content: @@ -59,3 +60,5 @@ spec: props: mode: link value: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + alias: + - QYjq2fJ diff --git a/test/visual/components/index.spec.ts b/test/visual/components/index.spec.ts index 5e4f217cde8..44fe4dd1a97 100644 --- a/test/visual/components/index.spec.ts +++ b/test/visual/components/index.spec.ts @@ -3,7 +3,7 @@ import * as url from 'url'; import { ToolpadEditor } from '../../models/ToolpadEditor'; import { ToolpadRuntime } from '../../models/ToolpadRuntime'; import { expect, test } from '../../playwright/localTest'; -import { clickCenter } from '../../utils/locators'; +import { clickCenter, waitForBoundingBox } from '../../utils/locators'; const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); @@ -114,6 +114,67 @@ test('showing grid while resizing elements', async ({ page, argosScreenshot }) = await argosScreenshot('resize-grid'); }); +test('resizing element heights', async ({ page, argosScreenshot }) => { + const editorModel = new ToolpadEditor(page); + await editorModel.goToPage('grids'); + + await editorModel.waitForOverlay(); + + const appCanvasBoundingBox = await editorModel.appCanvas.locator('body').boundingBox(); + + const screenshotConfig = { + clip: appCanvasBoundingBox || undefined, + }; + + const firstGrid = editorModel.appCanvas.getByRole('grid').nth(0); + + await clickCenter(page, firstGrid); + await argosScreenshot('vertical-resize-before', screenshotConfig); + + const firstGridBoundingBox = await waitForBoundingBox(firstGrid); + + await page.mouse.move( + firstGridBoundingBox!.x + firstGridBoundingBox!.width / 2, + firstGridBoundingBox!.y + firstGridBoundingBox!.height - 4, + { steps: 10 }, + ); + + await page.mouse.down(); + + await page.mouse.move( + firstGridBoundingBox!.x + firstGridBoundingBox!.width / 2, + firstGridBoundingBox!.y + firstGridBoundingBox!.height + 100, + { steps: 10 }, + ); + + await page.mouse.up(); + + const thirdGrid = editorModel.appCanvas.getByRole('grid').nth(2); + + await clickCenter(page, thirdGrid); + + const thirdGridBoundingBox = await waitForBoundingBox(thirdGrid); + + await page.mouse.move( + thirdGridBoundingBox!.x + thirdGridBoundingBox!.width / 2, + thirdGridBoundingBox!.y + thirdGridBoundingBox!.height - 4, + { steps: 10 }, + ); + + await page.mouse.down(); + + await page.mouse.move( + thirdGridBoundingBox!.x + thirdGridBoundingBox!.width / 2, + thirdGridBoundingBox!.y + thirdGridBoundingBox!.height + 100, + { steps: 10 }, + ); + + await page.mouse.up(); + + await clickCenter(page, firstGrid); + await argosScreenshot('vertical-resize-after', screenshotConfig); +}); + test('showing drag-and-drop previews', async ({ page, argosScreenshot }) => { const editorModel = new ToolpadEditor(page); await editorModel.goToPage('dragdrop');