diff --git a/packages/toolpad-app/src/components/EditableTreeItem.tsx b/packages/toolpad-app/src/components/EditableTreeItem.tsx index 82a0f2be4a7..1d9f4026bfa 100644 --- a/packages/toolpad-app/src/components/EditableTreeItem.tsx +++ b/packages/toolpad-app/src/components/EditableTreeItem.tsx @@ -81,10 +81,10 @@ export default function EditableTreeItem({ handleCancel(); return; } + if (onEdit) { onEdit(itemNameInput); } - setItemNameInput(''); setIsInternalEditing(false); }, [handleCancel, itemNameInput, newItemValidationResult.isValid, onEdit]); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/index.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/index.tsx index b535ed7333b..33f9c97311e 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/index.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { styled, Box, IconButton, Stack } from '@mui/material'; +import { styled, Box, IconButton, Stack, Tooltip } from '@mui/material'; import { TreeView, treeItemClasses } from '@mui/x-tree-view'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; @@ -10,7 +10,7 @@ import invariant from 'invariant'; import { alphabeticComparator, createPropComparator } from '@mui/toolpad-utils/comparators'; import useBoolean from '@mui/toolpad-utils/hooks/useBoolean'; import * as appDom from '@mui/toolpad-core/appDom'; -import { useAppStateApi, useAppState, useDomApi } from '../../AppState'; +import { useAppStateApi, useAppState } from '../../AppState'; import useLocalStorageState from '../../../utils/useLocalStorageState'; import NodeMenu from '../NodeMenu'; import { DomView } from '../../../utils/domView'; @@ -61,6 +61,7 @@ function PagesExplorerTreeItem(props: StyledTreeItemProps) { nodeId, labelIcon, labelText, + title, onRenameNode, onDeleteNode, onDuplicateNode, @@ -99,33 +100,35 @@ function PagesExplorerTreeItem(props: StyledTreeItemProps) { nodeId={nodeId} labelText={labelText} renderLabel={(children) => ( - - {labelIcon} - {children} - {toolpadNodeId ? ( - ( - - - - )} - nodeId={toolpadNodeId} - renameLabelText={renameLabelText} - deleteLabelText={deleteLabelText} - duplicateLabelText={duplicateLabelText} - onRenameNode={startEditing} - onDeleteNode={onDeleteNode} - onDuplicateNode={onDuplicateNode} - /> - ) : null} - + + + {labelIcon} + {children} + {toolpadNodeId ? ( + ( + + + + )} + nodeId={toolpadNodeId} + renameLabelText={renameLabelText} + deleteLabelText={deleteLabelText} + duplicateLabelText={duplicateLabelText} + onRenameNode={startEditing} + onDeleteNode={onDeleteNode} + onDuplicateNode={onDuplicateNode} + /> + ) : null} + + )} suggestedNewItemName={labelText} onCancel={stopEditing} @@ -155,7 +158,6 @@ export interface PagesExplorerProps { export default function PagesExplorer({ className }: PagesExplorerProps) { const projectApi = useProjectApi(); const { dom, currentView } = useAppState(); - const domApi = useDomApi(); const appStateApi = useAppStateApi(); const app = appDom.getApp(dom); @@ -276,23 +278,30 @@ export default function PagesExplorer({ className }: PagesExplorerProps) { if (nodeId === activePage?.id) { const siblings = appDom.getSiblings(dom, deletedNode); const firstSiblingOfType = siblings.find((sibling) => sibling.type === deletedNode.type); - domViewAfterDelete = firstSiblingOfType && getNodeEditorDomView(firstSiblingOfType); + domViewAfterDelete = firstSiblingOfType + ? getNodeEditorDomView(firstSiblingOfType) + : { kind: 'page' }; } await projectApi.methods.deletePage(deletedNode.name); - appStateApi.update( - (draft) => appDom.removeNode(draft, nodeId), - domViewAfterDelete || { kind: 'page' }, - ); + appStateApi.update((draft) => appDom.removeNode(draft, nodeId), domViewAfterDelete); }, [projectApi, activePage?.id, appStateApi, dom], ); const handleRenameNode = React.useCallback( (nodeId: NodeId, updatedName: string) => { - domApi.setNodeName(nodeId, updatedName); - appStateApi.setView({ kind: 'page', name: updatedName }); + appStateApi.update( + (draft) => { + const page = appDom.getNode(draft, nodeId, 'page'); + return appDom.setNodeName(draft, page, updatedName); + }, + { + kind: 'page', + name: updatedName, + }, + ); const oldNameNode = dom.nodes[nodeId]; if (oldNameNode.type === 'page' && updatedName !== oldNameNode.name) { @@ -301,7 +310,7 @@ export default function PagesExplorer({ className }: PagesExplorerProps) { }, 300); } }, - [projectApi, dom.nodes, domApi, appStateApi], + [appStateApi, dom.nodes, projectApi.methods], ); const handleDuplicateNode = React.useCallback( @@ -368,8 +377,8 @@ export default function PagesExplorer({ className }: PagesExplorerProps) { key={page.id} nodeId={page.id} toolpadNodeId={page.id} - labelText={appDom.getPageDisplayName(page)} - title={page.name} + labelText={page.name} + title={appDom.getPageDisplayName(page)} onRenameNode={handleRenameNode} onDuplicateNode={handleDuplicateNode} onDeleteNode={handleDeletePage} diff --git a/test/integration/duplication/index.spec.ts b/test/integration/duplication/index.spec.ts index c4a68d7ffea..ebb581600c3 100644 --- a/test/integration/duplication/index.spec.ts +++ b/test/integration/duplication/index.spec.ts @@ -19,7 +19,7 @@ test('duplication', async ({ page }) => { await editorModel.goto(); { - await editorModel.openPageExplorerMenu('Page 1'); + await editorModel.openPageExplorerMenu('page1'); const duplicateMenuItem = page.getByRole('menuitem', { name: 'Duplicate' }); await duplicateMenuItem.click(); @@ -28,12 +28,12 @@ test('duplication', async ({ page }) => { const button = editorModel.appCanvas.getByRole('button', { name: 'hello world' }); await expect(button).toBeVisible(); - await editorModel.openPageExplorerMenu('Page 2'); + await editorModel.openPageExplorerMenu('page2'); const deleteMenuItem = page.getByRole('menuitem', { name: 'Delete' }); await deleteMenuItem.click(); const deleteButton = editorModel.confirmationDialog.getByRole('button', { name: 'Delete' }); await deleteButton.click(); - await expect(editorModel.getExplorerItem('Page 2')).toBeHidden(); + await expect(editorModel.getExplorerItem('page2')).toBeHidden(); } }); diff --git a/test/integration/editor/new.spec.ts b/test/integration/editor/new.spec.ts index 95fadfda6c3..d49b9620af1 100644 --- a/test/integration/editor/new.spec.ts +++ b/test/integration/editor/new.spec.ts @@ -1,7 +1,7 @@ import path from 'path'; import invariant from 'invariant'; import { fileExists, folderExists } from '@mui/toolpad-utils/fs'; -import { test, expect } from '../../playwright/localTest'; +import { test, expect, Locator } from '../../playwright/localTest'; import { ToolpadEditor } from '../../models/ToolpadEditor'; test.use({ @@ -63,27 +63,39 @@ test('can create/delete page', async ({ page, localApp }) => { await editorModel.goto(); await editorModel.createPage('someOtherPage'); + await editorModel.createPage('andOneMorePage'); - const pageMenuItem = editorModel.getExplorerItem('Some Other Page'); - const pageFolder = path.resolve(localApp.dir, './toolpad/pages/someOtherPage'); - const pageFile = path.resolve(pageFolder, './page.yml'); + const deletePageFromExplorer = async (pageMenuItem: Locator) => { + await pageMenuItem.hover(); - await expect(pageMenuItem).toBeVisible(); - await expect.poll(async () => folderExists(pageFolder)).toBe(true); - await expect.poll(async () => fileExists(pageFile)).toBe(true); + await pageMenuItem.getByRole('button', { name: 'Open page explorer menu' }).click(); + + await page.getByRole('menuitem', { name: 'Delete' }).click(); + + await page + .getByRole('dialog', { name: 'Confirm' }) + .getByRole('button', { name: 'Delete' }) + .click(); + }; - await pageMenuItem.hover(); + // Delete another page - await pageMenuItem.getByRole('button', { name: 'Open page explorer menu' }).click(); + const anotherPageMenuItem = editorModel.getExplorerItem('someOtherPage'); + await deletePageFromExplorer(anotherPageMenuItem); + await expect(anotherPageMenuItem).toBeHidden(); - await page.getByRole('menuitem', { name: 'Delete' }).click(); + // Delete current page - await page - .getByRole('dialog', { name: 'Confirm' }) - .getByRole('button', { name: 'Delete' }) - .click(); + const currentPageMenuItem = editorModel.getExplorerItem('andOneMorePage'); + const pageFolder = path.resolve(localApp.dir, './toolpad/pages/andOneMorePage'); + const pageFile = path.resolve(pageFolder, './page.yml'); + + await expect(currentPageMenuItem).toBeVisible(); + await expect.poll(async () => folderExists(pageFolder)).toBe(true); + await expect.poll(async () => fileExists(pageFile)).toBe(true); - await expect(pageMenuItem).toBeHidden(); + await deletePageFromExplorer(currentPageMenuItem); + await expect(currentPageMenuItem).toBeHidden(); await expect.poll(async () => folderExists(pageFolder)).toBe(false); }); diff --git a/test/integration/pages/index.spec.ts b/test/integration/pages/index.spec.ts index 87bbcda482c..699d83fbc26 100644 --- a/test/integration/pages/index.spec.ts +++ b/test/integration/pages/index.spec.ts @@ -56,7 +56,7 @@ test('can rename page', async ({ page, localApp }) => { const oldPageFolder = path.resolve(localApp.dir, './toolpad/pages/page2'); await expect.poll(async () => folderExists(oldPageFolder)).toBe(true); - await editorModel.explorer.getByText('Page 2').dblclick(); + await editorModel.explorer.getByText('page2').dblclick(); await page.keyboard.type('renamedpage'); await page.keyboard.press('Enter'); diff --git a/test/integration/undo-redo/multiple-pages.spec.ts b/test/integration/undo-redo/multiple-pages.spec.ts index 038c0958461..2061ea6f382 100644 --- a/test/integration/undo-redo/multiple-pages.spec.ts +++ b/test/integration/undo-redo/multiple-pages.spec.ts @@ -25,7 +25,7 @@ test('test undo and redo through different pages', async ({ page }) => { }); await expect(pageButton1).toBeVisible(); - await editorModel.explorer.getByText('Page 2').click(); + await editorModel.explorer.getByText('page2').click(); const pageButton2 = editorModel.appCanvas.getByRole('button', { name: 'page2Button',