feat: modify pivot style & add operation menu to pivot item (#1726)

This commit is contained in:
Qi
2023-03-28 18:16:47 +08:00
committed by GitHub
parent 99be6183e6
commit 751ad9716f
22 changed files with 374 additions and 122 deletions

View File

@@ -0,0 +1,10 @@
import { useTranslation } from '@affine/i18n';
import { StyledCollapseItem } from '../shared-styles';
export const EmptyItem = () => {
const { t } = useTranslation();
return <StyledCollapseItem disable={true}>{t('No item')}</StyledCollapseItem>;
};
export default EmptyItem;

View File

@@ -1,5 +1,4 @@
import { MuiCollapse } from '@affine/component'; import { MuiCollapse } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons'; import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -8,13 +7,13 @@ import { useMemo } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms'; import { workspacePreferredModeAtom } from '../../../../atoms';
import type { FavoriteListProps } from '../index'; import type { FavoriteListProps } from '../index';
import { StyledCollapseItem } from '../shared-styles'; import { StyledCollapseItem } from '../shared-styles';
import EmptyItem from './empty-item';
export const FavoriteList = ({ export const FavoriteList = ({
pageMeta, pageMeta,
openPage, openPage,
showList, showList,
}: FavoriteListProps) => { }: FavoriteListProps) => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation();
const record = useAtomValue(workspacePreferredModeAtom); const record = useAtomValue(workspacePreferredModeAtom);
const favoriteList = useMemo( const favoriteList = useMemo(
@@ -60,9 +59,7 @@ export const FavoriteList = ({
</div> </div>
); );
})} })}
{favoriteList.length === 0 && ( {favoriteList.length === 0 && <EmptyItem />}
<StyledCollapseItem disable={true}>{t('No item')}</StyledCollapseItem>
)}
</MuiCollapse> </MuiCollapse>
); );
}; };

View File

@@ -0,0 +1,110 @@
import {
IconButton,
MenuItem,
MuiClickAwayListener,
PureMenu,
} from '@affine/component';
import { useTranslation } from '@affine/i18n';
import {
CopyIcon,
DeleteTemporarilyIcon,
MoreVerticalIcon,
MoveToIcon,
PenIcon,
PlusIcon,
} from '@blocksuite/icons';
import { useRouter } from 'next/router';
import { useCallback, useState } from 'react';
import { toast } from '../../../../utils';
export const OperationButton = ({
onAdd,
onDelete,
}: {
onAdd: () => void;
onDelete: () => void;
}) => {
const { t } = useTranslation();
const router = useRouter();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [open, setOpen] = useState(false);
const copyUrl = useCallback(() => {
const workspaceId = router.query.workspaceId;
navigator.clipboard.writeText(window.location.href);
toast(t('Copied link to clipboard'));
}, [router.query.workspaceId, t]);
return (
<MuiClickAwayListener
onClickAway={() => {
setOpen(false);
}}
>
<div
onClick={e => {
e.stopPropagation();
}}
onMouseLeave={() => {
setOpen(false);
}}
>
<IconButton
ref={ref => setAnchorEl(ref)}
size="small"
className="operation-button"
onClick={event => {
event.stopPropagation();
setOpen(!open);
}}
>
<MoreVerticalIcon />
</IconButton>
<PureMenu
anchorEl={anchorEl}
placement="bottom-start"
open={open && anchorEl !== null}
zIndex={11111}
>
<MenuItem
icon={<PlusIcon />}
onClick={() => {
onAdd();
setOpen(false);
}}
>
{t('Add a subpage inside')}
</MenuItem>
<MenuItem icon={<MoveToIcon />} disabled={true}>
{t('Move to')}
</MenuItem>
<MenuItem icon={<PenIcon />} disabled={true}>
{t('Rename')}
</MenuItem>
<MenuItem
icon={<DeleteTemporarilyIcon />}
onClick={() => {
onDelete();
setOpen(false);
}}
>
{t('Move to Trash')}
</MenuItem>
<MenuItem
icon={<CopyIcon />}
disabled={true}
// onClick={() => {
// const workspaceId = router.query.workspaceId;
// navigator.clipboard.writeText(window.location.href);
// toast(t('Copied link to clipboard'));
// }}
>
{t('Copy Link')}
</MenuItem>
</PureMenu>
</div>
</MuiClickAwayListener>
);
};

View File

@@ -1,7 +1,7 @@
import { MuiCollapse, TreeView } from '@affine/component'; import { MuiCollapse, TreeView } from '@affine/component';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { ArrowDownSmallIcon, FolderIcon } from '@blocksuite/icons'; import { ArrowDownSmallIcon, PivotsIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
@@ -9,6 +9,7 @@ import { useCallback, useMemo, useState } from 'react';
import { useBlockSuiteWorkspaceHelper } from '../../../../hooks/use-blocksuite-workspace-helper'; import { useBlockSuiteWorkspaceHelper } from '../../../../hooks/use-blocksuite-workspace-helper';
import { usePageMetaHelper } from '../../../../hooks/use-page-meta'; import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
import type { RemWorkspace } from '../../../../shared'; import type { RemWorkspace } from '../../../../shared';
import EmptyItem from '../favorite/empty-item';
import { StyledCollapseButton, StyledListItem } from '../shared-styles'; import { StyledCollapseButton, StyledListItem } from '../shared-styles';
import type { TreeNode } from './types'; import type { TreeNode } from './types';
import { flattenToTree } from './utils'; import { flattenToTree } from './utils';
@@ -197,6 +198,11 @@ export const Pivot = ({
const [showPivot, setShowPivot] = useState(true); const [showPivot, setShowPivot] = useState(true);
const isPivotEmpty = useMemo(
() => allMetas.filter(meta => !meta.trash).length === 0,
[allMetas]
);
return ( return (
<> <>
<StyledListItem> <StyledListItem>
@@ -208,7 +214,7 @@ export const Pivot = ({
> >
<ArrowDownSmallIcon /> <ArrowDownSmallIcon />
</StyledCollapseButton> </StyledCollapseButton>
<FolderIcon /> <PivotsIcon />
{t('Pivots')} {t('Pivots')}
</StyledListItem> </StyledListItem>
@@ -220,11 +226,15 @@ export const Pivot = ({
overflowY: 'auto', overflowY: 'auto',
}} }}
> >
<PivotInternal {isPivotEmpty ? (
currentWorkspace={currentWorkspace} <EmptyItem />
openPage={openPage} ) : (
allMetas={allMetas} <PivotInternal
/> currentWorkspace={currentWorkspace}
openPage={openPage}
allMetas={allMetas}
/>
)}
</MuiCollapse> </MuiCollapse>
</> </>
); );

View File

@@ -1,23 +1,16 @@
import { IconButton } from '@affine/component'; import { ArrowDownSmallIcon, EdgelessIcon, PageIcon } from '@blocksuite/icons';
import {
ArrowDownSmallIcon,
EdgelessIcon,
// DeleteTemporarilyIcon,
// PlusIcon,
MoreVerticalIcon,
PageIcon,
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { workspacePreferredModeAtom } from '../../../../atoms'; import { workspacePreferredModeAtom } from '../../../../atoms';
import { StyledCollapseButton, StyledCollapseItem } from '../shared-styles'; import { StyledCollapseButton, StyledCollapseItem } from '../shared-styles';
import { OperationButton } from './OperationButton';
import type { TreeNode } from './types'; import type { TreeNode } from './types';
export const TreeNodeRender: TreeNode['render'] = ( export const TreeNodeRender: TreeNode['render'] = (
node, node,
{ onAdd, onDelete, collapsed, setCollapsed }, { isOver, onAdd, onDelete, collapsed, setCollapsed },
extendProps extendProps
) => { ) => {
const { openPage, pageMeta } = extendProps as { const { openPage, pageMeta } = extendProps as {
@@ -37,6 +30,7 @@ export const TreeNodeRender: TreeNode['render'] = (
} }
openPage(node.id); openPage(node.id);
}} }}
isOver={isOver}
active={active} active={active}
> >
<StyledCollapseButton <StyledCollapseButton
@@ -51,37 +45,7 @@ export const TreeNodeRender: TreeNode['render'] = (
</StyledCollapseButton> </StyledCollapseButton>
{record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />} {record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
<span>{node.title || 'Untitled'}</span> <span>{node.title || 'Untitled'}</span>
<IconButton <OperationButton onAdd={onAdd} onDelete={onDelete} />
size="small"
className="operation-button"
onClick={e => {
e.stopPropagation();
}}
>
<MoreVerticalIcon />
</IconButton>
{/*<IconButton*/}
{/* onClick={e => {*/}
{/* e.stopPropagation();*/}
{/* onAdd();*/}
{/* }}*/}
{/* size="small"*/}
{/* className="operation-button"*/}
{/*>*/}
{/* <PlusIcon />*/}
{/*</IconButton>*/}
{/*<IconButton*/}
{/* onClick={e => {*/}
{/* e.stopPropagation();*/}
{/* onDelete();*/}
{/* }}*/}
{/* size="small"*/}
{/* className="operation-button"*/}
{/*>*/}
{/* <DeleteTemporarilyIcon />*/}
{/*</IconButton>*/}
</StyledCollapseItem> </StyledCollapseItem>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { displayFlex, styled, textEllipsis } from '@affine/component'; import { alpha, displayFlex, styled, textEllipsis } from '@affine/component';
export const StyledListItem = styled('div')<{ export const StyledListItem = styled('div')<{
active?: boolean; active?: boolean;
@@ -53,10 +53,11 @@ export const StyledCollapseButton = styled('button')<{
}; };
}); });
export const StyledCollapseItem = styled('button')<{ export const StyledCollapseItem = styled('div')<{
disable?: boolean; disable?: boolean;
active?: boolean; active?: boolean;
}>(({ disable = false, active = false, theme }) => { isOver?: boolean;
}>(({ disable = false, active = false, theme, isOver }) => {
return { return {
width: '100%', width: '100%',
height: '32px', height: '32px',
@@ -70,6 +71,7 @@ export const StyledCollapseItem = styled('button')<{
? theme.colors.primaryColor ? theme.colors.primaryColor
: theme.colors.textColor, : theme.colors.textColor,
cursor: disable ? 'not-allowed' : 'pointer', cursor: disable ? 'not-allowed' : 'pointer',
background: isOver ? alpha(theme.colors.primaryColor, 0.06) : '',
span: { span: {
flexGrow: '1', flexGrow: '1',
@@ -83,7 +85,7 @@ export const StyledCollapseItem = styled('button')<{
color: active ? theme.colors.primaryColor : theme.colors.iconColor, color: active ? theme.colors.primaryColor : theme.colors.iconColor,
}, },
'.operation-button': { '.operation-button': {
display: 'none', visibility: 'hidden',
}, },
':hover': disable ':hover': disable
@@ -91,7 +93,7 @@ export const StyledCollapseItem = styled('button')<{
: { : {
backgroundColor: theme.colors.hoverBackground, backgroundColor: theme.colors.hoverBackground,
'.operation-button': { '.operation-button': {
display: 'flex', visibility: 'visible',
}, },
}, },
}; };

View File

@@ -16,6 +16,7 @@ export const StyledSliderBar = styled('div')<{ show: boolean }>(
flexShrink: 0, flexShrink: 0,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
overflow: 'hidden',
}; };
} }
); );

View File

@@ -1,5 +1,6 @@
import { CssBaseline } from '@mui/material'; import { CssBaseline } from '@mui/material';
import { import {
alpha,
createTheme as createMuiTheme, createTheme as createMuiTheme,
css, css,
keyframes, keyframes,
@@ -11,7 +12,7 @@ import { useMemo } from 'react';
import type { AffineTheme } from './types'; import type { AffineTheme } from './types';
export { css, keyframes, styled }; export { alpha, css, keyframes, styled };
export const ThemeProvider = ({ export const ThemeProvider = ({
theme, theme,

View File

@@ -6,12 +6,13 @@ export type IconMenuProps = PropsWithChildren<{
isDir?: boolean; isDir?: boolean;
icon?: ReactElement; icon?: ReactElement;
iconSize?: [number, number]; iconSize?: [number, number];
disabled?: boolean;
}> & }> &
HTMLAttributes<HTMLButtonElement>; HTMLAttributes<HTMLButtonElement>;
export const MenuItem = forwardRef<HTMLButtonElement, IconMenuProps>( export const MenuItem = forwardRef<HTMLButtonElement, IconMenuProps>(
({ isDir = false, icon, iconSize, children, ...props }, ref) => { ({ isDir = false, icon, iconSize, children, ...props }, ref) => {
const [iconWidth, iconHeight] = iconSize || [16, 16]; const [iconWidth, iconHeight] = iconSize || [20, 20];
return ( return (
<StyledMenuItem ref={ref} {...props}> <StyledMenuItem ref={ref} {...props}>
{icon && {icon &&
@@ -19,7 +20,7 @@ export const MenuItem = forwardRef<HTMLButtonElement, IconMenuProps>(
width: iconWidth, width: iconWidth,
height: iconHeight, height: iconHeight,
style: { style: {
marginRight: 14, marginRight: 12,
...icon.props?.style, ...icon.props?.style,
}, },
})} })}

View File

@@ -0,0 +1,20 @@
import type { CSSProperties } from 'react';
import type { PurePopperProps } from '../popper';
import { PurePopper } from '../popper';
import { StyledMenuWrapper } from './styles';
export const PureMenu = ({
children,
placement,
width,
...otherProps
}: PurePopperProps & { width?: CSSProperties['width'] }) => {
return (
<PurePopper placement={placement} {...otherProps}>
<StyledMenuWrapper width={width} placement={placement}>
{children}
</StyledMenuWrapper>
</PurePopper>
);
};

View File

@@ -1,3 +1,4 @@
export * from './Menu'; export * from './Menu';
// export { StyledMenuItem as MenuItem } from './styles'; // export { StyledMenuItem as MenuItem } from './styles';
export * from './MenuItem'; export * from './MenuItem';
export * from './PureMenu';

View File

@@ -28,7 +28,8 @@ export const StyledArrow = styled(ArrowRightSmallIcon)({
export const StyledMenuItem = styled('button')<{ export const StyledMenuItem = styled('button')<{
isDir?: boolean; isDir?: boolean;
}>(({ theme, isDir = false }) => { disabled?: boolean;
}>(({ theme, isDir = false, disabled = false }) => {
return { return {
width: '100%', width: '100%',
borderRadius: '5px', borderRadius: '5px',
@@ -39,10 +40,25 @@ export const StyledMenuItem = styled('button')<{
cursor: isDir ? 'pointer' : '', cursor: isDir ? 'pointer' : '',
position: 'relative', position: 'relative',
backgroundColor: 'transparent', backgroundColor: 'transparent',
color: theme.colors.textColor, color: disabled ? theme.colors.disableColor : theme.colors.textColor,
':hover': { svg: {
color: theme.colors.primaryColor, color: disabled ? theme.colors.disableColor : theme.colors.iconColor,
backgroundColor: theme.colors.hoverBackground,
}, },
...(disabled
? {
cursor: 'not-allowed',
pointerEvents: 'none',
}
: {}),
':hover': disabled
? {}
: {
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
svg: {
color: theme.colors.primaryColor,
},
},
}; };
}); });

View File

@@ -11,7 +11,7 @@ export const PopperArrow = forwardRef<HTMLElement, PopperArrowProps>(
); );
const getArrowStyle = ( const getArrowStyle = (
placement: PopperArrowProps['placement'], placement: PopperArrowProps['placement'] = 'bottom',
backgroundColor: CSSProperties['backgroundColor'] backgroundColor: CSSProperties['backgroundColor']
) => { ) => {
if (placement.indexOf('bottom') === 0) { if (placement.indexOf('bottom') === 0) {
@@ -72,7 +72,7 @@ const getArrowStyle = (
}; };
const StyledArrow = styled('span')<{ const StyledArrow = styled('span')<{
placement: PopperArrowProps['placement']; placement?: PopperArrowProps['placement'];
}>(({ placement, theme }) => { }>(({ placement, theme }) => {
return { return {
position: 'absolute', position: 'absolute',

View File

@@ -1,6 +1,7 @@
import ClickAwayListener from '@mui/base/ClickAwayListener'; import ClickAwayListener from '@mui/base/ClickAwayListener';
import PopperUnstyled from '@mui/base/PopperUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled';
import Grow from '@mui/material/Grow'; import Grow from '@mui/material/Grow';
import type { CSSProperties, PointerEvent } from 'react';
import { import {
cloneElement, cloneElement,
useEffect, useEffect,
@@ -33,6 +34,8 @@ export const Popper = ({
popperHandlerRef, popperHandlerRef,
onClick, onClick,
onClickAway, onClickAway,
onPointerEnter,
onPointerLeave,
...popperProps ...popperProps
}: PopperProps) => { }: PopperProps) => {
const [anchorEl, setAnchorEl] = useState<VirtualElement>(); const [anchorEl, setAnchorEl] = useState<VirtualElement>();
@@ -58,7 +61,8 @@ export const Popper = ({
); );
}, [trigger]); }, [trigger]);
const onPointerEnterHandler = () => { const onPointerEnterHandler = (e: PointerEvent<HTMLDivElement>) => {
onPointerEnter?.(e);
if (!hasHoverTrigger || visibleControlledByParent) { if (!hasHoverTrigger || visibleControlledByParent) {
return; return;
} }
@@ -69,7 +73,9 @@ export const Popper = ({
}, pointerEnterDelay); }, pointerEnterDelay);
}; };
const onPointerLeaveHandler = () => { const onPointerLeaveHandler = (e: PointerEvent<HTMLDivElement>) => {
onPointerLeave?.(e);
if (!hasHoverTrigger || visibleControlledByParent) { if (!hasHoverTrigger || visibleControlledByParent) {
return; return;
} }
@@ -151,7 +157,7 @@ export const Popper = ({
onPointerLeave={onPointerLeaveHandler} onPointerLeave={onPointerLeaveHandler}
style={popoverStyle} style={popoverStyle}
className={popoverClassName} className={popoverClassName}
onClick={e => { onClick={() => {
if (hasClickTrigger && !visibleControlledByParent) { if (hasClickTrigger && !visibleControlledByParent) {
setVisible(false); setVisible(false);
} }
@@ -178,11 +184,11 @@ const Container = styled('div')({
display: 'contents', display: 'contents',
}); });
const BasicStyledPopper = styled(PopperUnstyled, { export const BasicStyledPopper = styled(PopperUnstyled, {
shouldForwardProp: (propName: string) => shouldForwardProp: (propName: string) =>
!['zIndex'].some(name => name === propName), !['zIndex'].some(name => name === propName),
})<{ })<{
zIndex?: number; zIndex?: CSSProperties['zIndex'];
}>(({ zIndex, theme }) => { }>(({ zIndex, theme }) => {
return { return {
zIndex: zIndex ?? theme.zIndex.popover, zIndex: zIndex ?? theme.zIndex.popover,

View File

@@ -0,0 +1,67 @@
import type { PopperUnstyledProps } from '@mui/base/PopperUnstyled';
import Grow from '@mui/material/Grow';
import type { CSSProperties, PropsWithChildren } from 'react';
import { useState } from 'react';
import { PopperArrow } from './PopoverArrow';
import { BasicStyledPopper } from './Popper';
import { PopperWrapper } from './styles';
export type PurePopperProps = {
zIndex?: CSSProperties['zIndex'];
offset?: [number, number];
showArrow?: boolean;
} & PopperUnstyledProps &
PropsWithChildren;
export const PurePopper = (props: PurePopperProps) => {
const {
children,
zIndex,
offset,
showArrow = false,
modifiers = [],
placement,
...otherProps
} = props;
const [arrowRef, setArrowRef] = useState<HTMLElement | null>();
// @ts-ignore
return (
<BasicStyledPopper
zIndex={zIndex}
transition
modifiers={[
{
name: 'offset',
options: {
offset,
},
},
{
name: 'arrow',
enabled: showArrow,
options: {
element: arrowRef,
},
},
...modifiers,
]}
placement={placement}
{...otherProps}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<PopperWrapper>
{showArrow && (
<PopperArrow placement={placement} ref={setArrowRef} />
)}
{children}
</PopperWrapper>
</Grow>
)}
</BasicStyledPopper>
);
};

View File

@@ -1,2 +1,3 @@
export * from './interface'; export * from './interface';
export * from './Popper'; export * from './Popper';
export * from './PurePopper';

View File

@@ -13,7 +13,7 @@ export type PopperHandler = {
}; };
export type PopperArrowProps = { export type PopperArrowProps = {
placement: PopperPlacementType; placement?: PopperPlacementType;
}; };
export type PopperProps = { export type PopperProps = {

View File

@@ -0,0 +1,7 @@
import { styled } from '../../styles';
export const PopperWrapper = styled('div')(() => {
return {
position: 'relative',
};
});

View File

@@ -1,13 +1,18 @@
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useDrag, useDrop } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd';
import { import {
StyledCollapse, StyledCollapse,
StyledNodeLine, StyledNodeLine,
StyledTreeNodeContainer, StyledTreeNodeContainer,
StyledTreeNodeItem, StyledTreeNodeWrapper,
} from './styles'; } from './styles';
import type { Node, NodeLIneProps, TreeNodeProps } from './types'; import type {
Node,
NodeLIneProps,
TreeNodeItemProps,
TreeNodeProps,
} from './types';
const NodeLine = <N,>({ const NodeLine = <N,>({
node, node,
@@ -39,30 +44,21 @@ const NodeLine = <N,>({
return <StyledNodeLine ref={drop} show={isOver && allowDrop} isTop={isTop} />; return <StyledNodeLine ref={drop} show={isOver && allowDrop} isTop={isTop} />;
}; };
const TreeNodeItem = <N,>({
export const TreeNode = <N,>({
node, node,
index, allowDrop,
allDrop = true, collapsed,
setCollapsed,
...otherProps ...otherProps
}: TreeNodeProps<N>) => { }: TreeNodeItemProps<N>) => {
const { onAdd, onDelete, onDrop, indent } = otherProps; const { onAdd, onDelete, onDrop } = otherProps;
const [collapsed, setCollapsed] = useState(false);
const [{ isDragging }, drag] = useDrag(() => ({
type: 'node',
item: node,
collect: monitor => ({
isDragging: monitor.isDragging(),
}),
}));
const [{ canDrop, isOver }, drop] = useDrop( const [{ canDrop, isOver }, drop] = useDrop(
() => ({ () => ({
accept: 'node', accept: 'node',
drop: (item: Node<N>, monitor) => { drop: (item: Node<N>, monitor) => {
const didDrop = monitor.didDrop(); const didDrop = monitor.didDrop();
if (didDrop || item.id === node.id || !allDrop) { if (didDrop || item.id === node.id || !allowDrop) {
return; return;
} }
onDrop?.(item, node, { onDrop?.(item, node, {
@@ -73,42 +69,75 @@ export const TreeNode = <N,>({
}, },
collect: monitor => ({ collect: monitor => ({
isOver: monitor.isOver(), isOver: monitor.isOver(),
canDrop: monitor.canDrop(), canDrop: monitor.canDrop() && allowDrop,
}), }),
}), }),
[onDrop, allDrop] [onDrop, allowDrop]
); );
useEffect(() => {
if (isOver && canDrop) {
setCollapsed(false);
}
}, [isOver, canDrop]);
return (
<div ref={drop}>
{node.render?.(node, {
isOver: !!(isOver && canDrop),
onAdd: () => onAdd?.(node),
onDelete: () => onDelete?.(node),
collapsed,
setCollapsed,
})}
</div>
);
};
export const TreeNode = <N,>({
node,
index,
allowDrop = true,
...otherProps
}: TreeNodeProps<N>) => {
const { indent } = otherProps;
const [collapsed, setCollapsed] = useState(false);
const [{ isDragging }, drag] = useDrag(() => ({
type: 'node',
item: node,
collect: monitor => ({
isDragging: monitor.isDragging(),
}),
}));
return ( return (
<StyledTreeNodeContainer ref={drag} isDragging={isDragging}> <StyledTreeNodeContainer ref={drag} isDragging={isDragging}>
<StyledTreeNodeItem <StyledTreeNodeWrapper>
ref={drop}
isOver={isOver && !isDragging}
canDrop={canDrop}
>
{index === 0 && ( {index === 0 && (
<NodeLine <NodeLine
node={node} node={node}
{...otherProps} {...otherProps}
allowDrop={!isDragging && allDrop} allowDrop={!isDragging && allowDrop}
isTop={true} isTop={true}
/> />
)} )}
{node.render?.(node, { <TreeNodeItem
onAdd: () => onAdd?.(node), node={node}
onDelete: () => onDelete?.(node), index={index}
collapsed, allowDrop={allowDrop}
setCollapsed, collapsed={collapsed}
})} setCollapsed={setCollapsed}
{...otherProps}
/>
{(!node.children?.length || collapsed) && ( {(!node.children?.length || collapsed) && (
<NodeLine <NodeLine
node={node} node={node}
{...otherProps} {...otherProps}
allowDrop={!isDragging && allDrop} allowDrop={!isDragging && allowDrop}
/> />
)} )}
</StyledTreeNodeItem> </StyledTreeNodeWrapper>
<StyledCollapse in={!collapsed} indent={indent}> <StyledCollapse in={!collapsed} indent={indent}>
{node.children && {node.children &&
node.children.map((childNode, index) => ( node.children.map((childNode, index) => (
@@ -116,7 +145,7 @@ export const TreeNode = <N,>({
key={childNode.id} key={childNode.id}
node={childNode} node={childNode}
index={index} index={index}
allDrop={isDragging ? false : allDrop} allowDrop={isDragging ? false : allowDrop}
{...otherProps} {...otherProps}
/> />
))} ))}

View File

@@ -1,7 +1,7 @@
import MuiCollapse from '@mui/material/Collapse'; import MuiCollapse from '@mui/material/Collapse';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { styled } from '../../styles'; import { alpha, styled } from '../../styles';
export const StyledCollapse = styled(MuiCollapse)<{ export const StyledCollapse = styled(MuiCollapse)<{
indent?: CSSProperties['paddingLeft']; indent?: CSSProperties['paddingLeft'];
@@ -10,12 +10,8 @@ export const StyledCollapse = styled(MuiCollapse)<{
paddingLeft: indent, paddingLeft: indent,
}; };
}); });
export const StyledTreeNodeItem = styled('div')<{ export const StyledTreeNodeWrapper = styled('div')(() => {
isOver?: boolean;
canDrop?: boolean;
}>(({ isOver, canDrop, theme }) => {
return { return {
background: isOver && canDrop ? theme.colors.hoverBackground : '',
position: 'relative', position: 'relative',
}; };
}); });
@@ -23,6 +19,7 @@ export const StyledTreeNodeContainer = styled('div')<{ isDragging: boolean }>(
({ isDragging, theme }) => { ({ isDragging, theme }) => {
return { return {
background: isDragging ? theme.colors.hoverBackground : '', background: isDragging ? theme.colors.hoverBackground : '',
// opacity: isDragging ? 0.4 : 1,
}; };
} }
); );
@@ -32,11 +29,14 @@ export const StyledNodeLine = styled('div')<{ show: boolean; isTop?: boolean }>(
return { return {
position: 'absolute', position: 'absolute',
left: '0', left: '0',
...(isTop ? { top: '0' } : { bottom: '0' }), ...(isTop ? { top: '-1px' } : { bottom: '-1px' }),
width: '100%', width: '100%',
paddingTop: '3px', paddingTop: '2x',
borderBottom: '3px solid', borderTop: '2px solid',
borderColor: show ? theme.colors.primaryColor : 'transparent', borderColor: show ? theme.colors.primaryColor : 'transparent',
boxShadow: show
? `0px 0px 8px ${alpha(theme.colors.primaryColor, 0.35)}`
: 'none',
zIndex: 1, zIndex: 1,
}; };
} }

View File

@@ -6,6 +6,7 @@ export type Node<N> = {
render?: ( render?: (
node: Node<N>, node: Node<N>,
eventsAndStatus: { eventsAndStatus: {
isOver: boolean;
onAdd: () => void; onAdd: () => void;
onDelete: () => void; onDelete: () => void;
collapsed: boolean; collapsed: boolean;
@@ -33,9 +34,14 @@ type CommonProps<N> = {
export type TreeNodeProps<N> = { export type TreeNodeProps<N> = {
node: Node<N>; node: Node<N>;
index: number; index: number;
allDrop?: boolean; allowDrop?: boolean;
} & CommonProps<N>; } & CommonProps<N>;
export type TreeNodeItemProps<N> = {
collapsed: boolean;
setCollapsed: (collapsed: boolean) => void;
} & TreeNodeProps<N>;
export type TreeViewProps<N> = { export type TreeViewProps<N> = {
data: Node<N>[]; data: Node<N>[];
} & CommonProps<N>; } & CommonProps<N>;

View File

@@ -194,5 +194,8 @@
"Please make sure you are online": "Please make sure you are online", "Please make sure you are online": "Please make sure you are online",
"Workspace Owner": "Workspace Owner", "Workspace Owner": "Workspace Owner",
"Members": "Members", "Members": "Members",
"Pivots": "Pivots" "Pivots": "Pivots",
"Add a subpage inside": "Add a subpage inside",
"Rename": "Rename",
"Move to": "Move to"
} }