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

[feat] Add page structure explorer #2246

Merged
merged 25 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8ab8842
WIP page structure explorer
bharatkashyap Jun 29, 2023
26b3132
WIP add page structure explorer
bharatkashyap Jun 30, 2023
f37f538
v1 page structure explorer
bharatkashyap Jun 30, 2023
aeedf77
don't crash on renderItem node
bharatkashyap Jun 30, 2023
f7b6dfa
Merge branch 'master' into page-tree-view
bharatkashyap Jun 30, 2023
46b0059
Merge branch 'master' into page-tree-view
bharatkashyap Jul 9, 2023
587d5b3
Fix: Make side panel leaner
bharatkashyap Jul 9, 2023
108ff97
Fix: better nested element expansion, delete on backspace
bharatkashyap Jul 11, 2023
b76a300
Merge branch 'master' of github.com:mui/mui-toolpad into page-tree-view
bharatkashyap Jul 12, 2023
72a8f2b
Fix: accidental commit
bharatkashyap Jul 12, 2023
964cc01
[WIP] Extract normalize function
bharatkashyap Jul 14, 2023
aad5904
Fix: Remove unnecessary ternary through type check
bharatkashyap Jul 14, 2023
34ad45c
Extract delete functions for reusability
bharatkashyap Jul 14, 2023
ae60aa1
Merge branch 'master' of github.com:mui/mui-toolpad into page-tree-view
bharatkashyap Jul 14, 2023
f3d2109
Merge branch 'master' into page-tree-view
bharatkashyap Jul 14, 2023
37e3691
Revert "[WIP] Extract normalize function"
bharatkashyap Jul 18, 2023
02fa34e
Revert "Extract delete functions for reusability"
bharatkashyap Jul 18, 2023
b37715b
only make call to appStateApi, parallel effort ongoing
bharatkashyap Jul 18, 2023
f1b5705
Merge branch 'master' of github.com:mui/mui-toolpad into page-tree-view
bharatkashyap Jul 18, 2023
19dafdd
fix: better scrollbars
bharatkashyap Jul 18, 2023
e634a33
Merge branch 'master' into page-tree-view
bharatkashyap Jul 19, 2023
3191ae2
Merge branch 'master' of github.com:mui/mui-toolpad into page-tree-view
bharatkashyap Jul 19, 2023
b2badeb
Merge branch 'master' into page-tree-view
bharatkashyap Jul 20, 2023
078a4d8
Merge branch 'master' into page-tree-view
bharatkashyap Jul 21, 2023
5ce4365
Merge branch 'master' into page-tree-view
bharatkashyap Jul 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/toolpad-app/src/appDom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,6 @@ export function getApp(dom: AppDom): AppNode {

export type NodeChildren<N extends AppDomNode = any> = ChildNodesOf<N>;

// TODO: memoize the result of this function per dom in a WeakMap
const childrenMemo = new WeakMap<AppDom, Map<NodeId, NodeChildren<any>>>();
export function getChildNodes<N extends AppDomNode>(dom: AppDom, parent: N): NodeChildren<N> {
let domChildrenMemo = childrenMemo.get(dom);
Expand Down
8 changes: 6 additions & 2 deletions packages/toolpad-app/src/components/EditableText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface EditableTextProps {
onChange?: (newValue: string) => void;
onSave?: (newValue: string) => void;
onClose?: () => void;
onDoubleClick?: () => void;
size?: 'small' | 'medium';
sx?: SxProps;
value?: string;
Expand All @@ -34,6 +35,7 @@ const EditableText = React.forwardRef<HTMLInputElement, EditableTextProps>(
error,
onChange,
onClose,
onDoubleClick,
onSave,
size,
sx,
Expand Down Expand Up @@ -113,17 +115,19 @@ const EditableText = React.forwardRef<HTMLInputElement, EditableTextProps>(
error={error}
helperText={helperText}
ref={ref}
onDoubleClick={onDoubleClick}
inputRef={appTitleInput}
inputProps={{
tabIndex: editable ? 0 : -1,
// tabIndex: editable ? 0 : -1,
'aria-readonly': !editable,
sx: (theme: Theme) => ({
// Handle overflow
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
fontSize: theme.typography[typographyVariant].fontSize,
height: `1.1em`,
height: theme.typography.pxToRem(8),
cursor: 'pointer',
}),
}}
onKeyUp={handleInput}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { TreeView } from '@mui/lab';
import { Typography, styled, Box, IconButton } from '@mui/material';
import * as React from 'react';
import TreeItem, { treeItemClasses, TreeItemProps } from '@mui/lab/TreeItem';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import AddIcon from '@mui/icons-material/Add';
import { NodeId } from '@mui/toolpad-core';
Expand Down Expand Up @@ -72,14 +72,14 @@ function HierarchyTreeItem(props: StyledTreeItemProps) {
return (
<StyledTreeItem
label={
<Box sx={{ display: 'flex', alignItems: 'center', p: 0.5, pr: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', p: 0.1, pr: 0 }}>
{labelIcon}
<Typography variant="body2" sx={{ fontWeight: 'inherit', flexGrow: 1 }} noWrap>
{labelText}
</Typography>
{onCreate ? (
<IconButton aria-label={createLabelText} onClick={onCreate}>
<AddIcon />
<IconButton aria-label={createLabelText} onClick={onCreate} size="small">
<AddIcon fontSize="inherit" />
</IconButton>
) : null}
{toolpadNodeId ? (
Expand All @@ -90,9 +90,10 @@ function HierarchyTreeItem(props: StyledTreeItemProps) {
[classes.treeItemMenuOpen]: menuProps.open,
})}
aria-label="Open hierarchy menu"
size="small"
{...buttonProps}
>
<MoreVertIcon />
<MoreVertIcon fontSize="inherit" />
</IconButton>
)}
nodeId={toolpadNodeId}
Expand Down Expand Up @@ -251,8 +252,8 @@ export default function HierarchyExplorer({ className }: HierarchyExplorerProps)
expanded={expanded}
onNodeToggle={handleToggle}
multiSelect
defaultCollapseIcon={<ArrowDropDownIcon />}
defaultExpandIcon={<ArrowRightIcon />}
defaultCollapseIcon={<ExpandMoreIcon sx={{ fontSize: '0.9rem', opacity: 0.5 }} />}
defaultExpandIcon={<ChevronRightIcon sx={{ fontSize: '0.9rem', opacity: 0.5 }} />}
>
<HierarchyTreeItem
nodeId=":pages"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import PlaceIcon from '@mui/icons-material/Place';
import ViewSidebarIcon from '@mui/icons-material/ViewSidebar';
import MoodIcon from '@mui/icons-material/Mood';
import HtmlIcon from '@mui/icons-material/Html';
import { ButtonBase } from '@mui/material';
import TableRowsIcon from '@mui/icons-material/TableRows';
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
import { ButtonBase, SxProps } from '@mui/material';

const iconMap = new Map<string, React.ComponentType<SvgIconProps>>([
['Autocomplete', ManageSearchIcon],
Expand Down Expand Up @@ -58,18 +60,21 @@ const iconMap = new Map<string, React.ComponentType<SvgIconProps>>([
['Drawer', ViewSidebarIcon],
['Icon', MoodIcon],
['Html', HtmlIcon],
['PageRow', TableRowsIcon],
['PageColumn', ViewColumnIcon],
]);

type ComponentItemKind = 'future' | 'builtIn' | 'create' | 'custom';

interface ComponentIconProps {
id: string;
kind?: ComponentItemKind;
sx?: SxProps;
}

function ComponentIcon({ id: componentId, kind }: ComponentIconProps) {
export function ComponentIcon({ id: componentId, kind, sx }: ComponentIconProps) {
const Icon = iconMap.get(kind === 'custom' ? 'CodeComponent' : componentId);
return Icon ? <Icon fontSize="medium" opacity={kind === 'future' ? 0.75 : 1} /> : null;
return Icon ? <Icon sx={{ fontSize: 24, opacity: kind === 'future' ? 0.75 : 1, ...sx }} /> : null;
}

interface ComponentCatalogItemProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function stopPropagationHandler(event: React.SyntheticEvent) {

const nodeHudClasses = {
selected: 'NodeHud_Selected',
hovered: 'NodeHud_Hovered',
selectionHint: 'NodeHud_SelectionHint',
};

Expand All @@ -41,7 +42,7 @@ const NodeHudWrapper = styled('div', {
userSelect: 'none',
outline: `1px dotted ${isOutlineVisible ? theme.palette.primary[500] : 'transparent'}`,
zIndex: 80,
'&:hover': {
[`&:hover, &.${nodeHudClasses.hovered}`]: {
outline: `2px dashed ${isHoverable ? theme.palette.primary[500] : 'transparent'}`,
},
[`.${nodeHudClasses.selected}`]: {
Expand Down Expand Up @@ -150,6 +151,7 @@ interface NodeHudProps {
onDuplicate?: (event: React.MouseEvent) => void;
isOutlineVisible?: boolean;
isHoverable?: boolean;
isHovered?: boolean;
}

export default function NodeHud({
Expand All @@ -167,6 +169,7 @@ export default function NodeHud({
onDuplicate,
isOutlineVisible = false,
isHoverable = true,
isHovered = false,
Copy link
Member

@apedroferreira apedroferreira Jul 3, 2023

Choose a reason for hiding this comment

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

Since the dashed border doesn't really just mean the element is "hovered" anymore we might want to give this prop a different name just to avoid confusion. Maybe isPreviewed? Not sure, also not a super big deal.
I understand this would have to change the wording in a lot of places now.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think isHovered captures the state still, since the user is "hovering" over the element albeit inside the structure explorer, so an indirect form of hovering.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, I think it's fine to keep it.

}: NodeHudProps) {
const hintPosition = rect.y > HUD_HEIGHT ? HINT_POSITION_TOP : HINT_POSITION_BOTTOM;

Expand Down Expand Up @@ -205,6 +208,7 @@ export default function NodeHud({
}
: {}),
}}
className={isHovered ? nodeHudClasses.hovered : ''}
isOutlineVisible={isOutlineVisible}
isHoverable={isHoverable}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ function removeMaybeNode(dom: appDom.AppDom, nodeId: NodeId): appDom.AppDom {
return dom;
}

function deleteOrphanedLayoutNodes(
export function deleteOrphanedLayoutNodes(
domBeforeChange: appDom.AppDom,
domAfterChange: appDom.AppDom,
movedOrDeletedNode: appDom.ElementNode,
Expand Down Expand Up @@ -276,6 +276,7 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) {
const { dom } = useDom();
const { currentView } = useAppState();
const selectedNodeId = currentView.kind === 'page' ? currentView.selectedNodeId : null;
const hoveredNodeId = currentView.kind === 'page' ? currentView.hoveredNodeId : null;

const domApi = useDomApi();
const appStateApi = useAppStateApi();
Expand Down Expand Up @@ -1586,6 +1587,7 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) {
const isPageColumnChild = parent ? appDom.isElement(parent) && isPageColumn(parent) : false;

const isSelected = selectedNode && !newNode ? selectedNode.id === node.id : false;
const isHovered = hoveredNodeId === node.id;

const isHorizontallyResizable = isSelected && (isPageRowChild || isPageColumnChild);
const isVerticallyResizable =
Expand Down Expand Up @@ -1625,6 +1627,7 @@ export default function RenderOverlay({ bridge }: RenderOverlayProps) {
onDelete={handleNodeDelete(node.id)}
isResizing={isResizingNode}
resizePreviewElementRef={resizePreviewElementRef}
isHovered={isHovered}
isHoverable={!isResizing && !isDraggingOver}
isOutlineVisible={isDraggingOver}
/>
Expand Down
9 changes: 7 additions & 2 deletions packages/toolpad-app/src/toolpad/AppEditor/PagePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from 'react';
import { styled, SxProps, Box, Divider, Typography } from '@mui/material';
import HierarchyExplorer from './HierarchyExplorer';
import PagesHierarchyExplorer from './HierarchyExplorer';
import PageStructureExplorer from './StructureExplorer';
import SplitPane from '../../components/SplitPane';
import { useDom } from '../AppState';
import AppOptions from '../AppOptions';
import config from '../../config';
Expand Down Expand Up @@ -36,7 +38,10 @@ export default function PagePanel({ className, sx }: ComponentPanelProps) {
<AppOptions dom={dom} />
</Box>
<Divider />
<HierarchyExplorer />
<SplitPane sx={{ flex: 1 }} split="horizontal" defaultSize={200} minSize={100} maxSize={400}>
<PagesHierarchyExplorer />
<PageStructureExplorer />
</SplitPane>
</PagePanelRoot>
);
}
Loading