Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rename, duplicate and delete for Page Hierarchy #3336

Merged
merged 6 commits into from
Mar 28, 2024
Merged
Original file line number Diff line number Diff line change
@@ -1,22 +1,53 @@
import * as React from 'react';
import clsx from 'clsx';
import { NodeId } from '@toolpad/studio-runtime';
import { Box, Typography, styled } from '@mui/material';
import { SimpleTreeView, TreeItem, TreeItemProps } from '@mui/x-tree-view';
import { Box, Typography, styled, IconButton, SxProps } from '@mui/material';
import { SimpleTreeView, TreeItem, TreeItemProps, treeItemClasses } from '@mui/x-tree-view';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import useBoolean from '@toolpad/utils/hooks/useBoolean';
import * as appDom from '@toolpad/studio-runtime/appDom';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import invariant from 'invariant';
import { useAppState, useDomApi, useAppStateApi } from '../../AppState';
import { ComponentIcon } from '../PageEditor/ComponentCatalog/ComponentCatalogItem';
import { DomView } from '../../../utils/domView';
import { removePageLayoutNode } from '../pageLayout';
import EditableTreeItem from '../../../components/EditableTreeItem';
import EditableTreeItem, { EditableTreeItemProps } from '../../../components/EditableTreeItem';
import ExplorerHeader from '../ExplorerHeader';
import NodeMenu from '../NodeMenu';

const CollapseIcon = styled(ExpandMoreIcon)({ fontSize: '0.9rem', opacity: 0.5 });
const ExpandIcon = styled(ChevronRightIcon)({ fontSize: '0.9rem', opacity: 0.5 });

export interface CustomTreeItemProps extends TreeItemProps {
const classes = {
treeItemMenuButton: 'Toolpad__HierarchyListItem',
treeItemMenuOpen: 'Toolpad__HierarchyListItemMenuOpen',
};

const StyledTreeItem = styled(EditableTreeItem)({
[`& .${classes.treeItemMenuButton}`]: {
visibility: 'hidden',
},
[`
& .${treeItemClasses.content}:hover .${classes.treeItemMenuButton},
& .${classes.treeItemMenuOpen}
`]: {
visibility: 'visible',
},
});

interface StyledTreeItemProps extends TreeItemProps, EditableTreeItemProps {
labelTextSx?: SxProps;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify this a bit, I think you can just supply values for these directly to the NodeMenu component. I see we're exporting CustomTreeItemProps, I don't think this is necessary, we can keep the component local to this module for now.

labelIconId?: string;
labelIconSx?: SxProps;
createLabelText?: string;
deleteLabelText?: string;
renameLabelText?: string;
duplicateLabelText?: string;
}

export interface CustomTreeItemProps extends StyledTreeItemProps {
ref?: React.RefObject<HTMLLIElement>;
node: appDom.ElementNode;
}
Expand All @@ -26,9 +57,20 @@ function CustomTreeItem(props: CustomTreeItemProps) {
const { dom } = useAppState();
const appStateApi = useAppStateApi();

const { label, node, ...other } = props;
const {
label,
node,
renameLabelText = 'Rename',
duplicateLabelText = 'Duplicate',
deleteLabelText = 'Delete',
...other
} = props;

const { value: domNodeEditing, setFalse: stopDomNodeEditing } = useBoolean(false);
const {
value: domNodeEditing,
setTrue: startDomNodeEditing,
setFalse: stopDomNodeEditing,
} = useBoolean(false);

const existingNames = React.useMemo(() => appDom.getExistingNamesForNode(dom, node), [dom, node]);

Expand All @@ -50,8 +92,41 @@ function CustomTreeItem(props: CustomTreeItemProps) {
const handleNameSave = React.useCallback(
(newName: string) => {
domApi.setNodeName(node.id, newName);
stopDomNodeEditing();
},
[domApi, node.id, stopDomNodeEditing],
);

const handleNodeDelete = React.useCallback(
(nodeId: NodeId) => {
domApi.update((draft) => {
const toRemove = appDom.getNode(draft, nodeId);
if (appDom.isElement(toRemove)) {
draft = removePageLayoutNode(draft, toRemove);
}

return draft;
});
},
[domApi],
);

const handleNodeDuplicate = React.useCallback(
(nodeId: NodeId) => {
const currentNode = appDom.getNode(dom, nodeId);

invariant(
node.parentId && node.parentProp,
'Duplication should never be called on nodes that are not placed in the dom',
);

domApi.update((draft) => {
draft = appDom.duplicateNode(draft, currentNode);

return draft;
});
},
[domApi, node.id],
[dom, domApi, node.parentId, node.parentProp],
);

const handleNodeHover = React.useCallback(
Expand All @@ -66,7 +141,7 @@ function CustomTreeItem(props: CustomTreeItemProps) {
}, [appStateApi]);

return (
<EditableTreeItem
<StyledTreeItem
key={node.id}
labelText={node.name}
renderLabel={(children) => (
Expand All @@ -83,6 +158,29 @@ function CustomTreeItem(props: CustomTreeItemProps) {
sx={{ marginRight: 1, fontSize: 18, opacity: 0.5 }}
/>
{children}
{node.id ? (
<NodeMenu
renderButton={({ buttonProps, menuProps }) => (
<IconButton
className={clsx(classes.treeItemMenuButton, {
[classes.treeItemMenuOpen]: menuProps.open,
})}
aria-label="Open hierarchy menu"
size="small"
{...buttonProps}
>
<MoreVertIcon fontSize="inherit" />
</IconButton>
)}
nodeId={node.id}
renameLabelText={renameLabelText}
duplicateLabelText={duplicateLabelText}
deleteLabelText={deleteLabelText}
onRenameNode={startDomNodeEditing}
onDuplicateNode={handleNodeDuplicate}
onDeleteNode={handleNodeDelete}
/>
) : null}
</Box>
)}
isEditing={domNodeEditing}
Expand Down
Loading