feat: support pivots menu (#1755)

This commit is contained in:
Qi
2023-03-30 17:37:41 +08:00
committed by GitHub
parent 4dd1490eef
commit b6ded30770
40 changed files with 1513 additions and 665 deletions

View File

@@ -0,0 +1,24 @@
import { MenuItem } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { CopyIcon } from '@blocksuite/icons';
// import { useRouter } from "next/router";
// import { useCallback } from "react";
//
// import { toast } from "../../../utils";
export const CopyLink = () => {
const { t } = useTranslation();
// const router = useRouter();
// 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 (
<>
<MenuItem onClick={() => {}} icon={<CopyIcon />} disabled={true}>
{t('Copy Link')}
</MenuItem>
</>
);
};

View File

@@ -0,0 +1,50 @@
import { Menu, MenuItem } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import {
ArrowRightSmallIcon,
ExportIcon,
ExportToHtmlIcon,
ExportToMarkdownIcon,
} from '@blocksuite/icons';
export const Export = () => {
const { t } = useTranslation();
return (
<Menu
width={248}
placement="left-start"
trigger="click"
content={
<>
<MenuItem
onClick={() => {
// @ts-expect-error
globalThis.currentEditor.contentParser.onExportHtml();
}}
icon={<ExportToHtmlIcon />}
>
{t('Export to HTML')}
</MenuItem>
<MenuItem
onClick={() => {
// @ts-expect-error
globalThis.currentEditor.contentParser.onExportMarkdown();
}}
icon={<ExportToMarkdownIcon />}
>
{t('Export to Markdown')}
</MenuItem>
</>
}
>
<MenuItem
icon={<ExportIcon />}
endIcon={<ArrowRightSmallIcon />}
onClick={e => e.stopPropagation()}
>
{t('Export')}
</MenuItem>
</Menu>
);
};

View File

@@ -0,0 +1,46 @@
import { MenuItem } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { ArrowRightSmallIcon, MoveToIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useRef, useState } from 'react';
import type { BlockSuiteWorkspace } from '../../../shared';
import { PivotsMenu } from '../pivots';
export const MoveTo = ({
metas,
currentMeta,
blockSuiteWorkspace,
}: {
metas: PageMeta[];
currentMeta: PageMeta;
blockSuiteWorkspace: BlockSuiteWorkspace;
}) => {
const { t } = useTranslation();
const ref = useRef<HTMLButtonElement>(null);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = anchorEl !== null;
return (
<>
<MenuItem
ref={ref}
onClick={e => {
e.stopPropagation();
setAnchorEl(ref.current);
}}
icon={<MoveToIcon />}
endIcon={<ArrowRightSmallIcon />}
>
{t('Move to')}
</MenuItem>
<PivotsMenu
anchorEl={anchorEl}
open={open}
placement="left-start"
metas={metas.filter(meta => !meta.trash)}
currentMeta={currentMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
</>
);
};

View File

@@ -0,0 +1,60 @@
import { Confirm, MenuItem } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { DeleteTemporarilyIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useState } from 'react';
import { usePageMetaHelper } from '../../../hooks/use-page-meta';
import type { BlockSuiteWorkspace } from '../../../shared';
import { toast } from '../../../utils';
export const MoveToTrash = ({
currentMeta,
blockSuiteWorkspace,
testId,
}: {
currentMeta: PageMeta;
blockSuiteWorkspace: BlockSuiteWorkspace;
testId?: string;
}) => {
const { t } = useTranslation();
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [open, setOpen] = useState(false);
return (
<>
<MenuItem
data-testid={testId}
onClick={() => {
setOpen(true);
}}
icon={<DeleteTemporarilyIcon />}
>
{t('Move to Trash')}
</MenuItem>
<Confirm
title={t('Delete page?')}
content={t('will be moved to Trash', {
title: currentMeta.title || 'Untitled',
})}
confirmText={t('Delete')}
confirmType="danger"
open={open}
onConfirm={() => {
toast(t('Moved to Trash'));
setOpen(false);
setPageMeta(currentMeta.id, {
trash: true,
trashDate: +new Date(),
});
}}
onClose={() => {
setOpen(false);
}}
onCancel={() => {
setOpen(false);
}}
/>
</>
);
};

View File

@@ -0,0 +1,4 @@
export * from './CopyLink';
export * from './Export';
export * from './MoveTo';
export * from './MoveToTrash';

View File

@@ -0,0 +1,103 @@
import { MuiClickAwayListener } from '@affine/component';
import { MoreVerticalIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useTheme } from '@mui/material';
import { useMemo, useState } from 'react';
import type { BlockSuiteWorkspace } from '../../../shared';
import { OperationMenu } from './OperationMenu';
import { PivotsMenu } from './PivotsMenu/PivotsMenu';
import { StyledOperationButton } from './styles';
export type OperationButtonProps = {
onAdd: () => void;
onDelete: () => void;
metas: PageMeta[];
currentMeta: PageMeta;
blockSuiteWorkspace: BlockSuiteWorkspace;
isHover: boolean;
onMenuClose?: () => void;
};
export const OperationButton = ({
onAdd,
onDelete,
metas,
currentMeta,
blockSuiteWorkspace,
isHover,
onMenuClose,
}: OperationButtonProps) => {
const {
zIndex: { modal: modalIndex },
} = useTheme();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [operationOpen, setOperationOpen] = useState(false);
const [pivotsMenuOpen, setPivotsMenuOpen] = useState(false);
const menuIndex = useMemo(() => modalIndex + 1, [modalIndex]);
return (
<MuiClickAwayListener
onClickAway={() => {
setOperationOpen(false);
setPivotsMenuOpen(false);
}}
>
<div
onClick={e => {
e.stopPropagation();
}}
onMouseLeave={() => {
setOperationOpen(false);
setPivotsMenuOpen(false);
}}
>
<StyledOperationButton
ref={ref => setAnchorEl(ref)}
size="small"
onClick={() => {
setOperationOpen(!operationOpen);
}}
visible={isHover}
>
<MoreVerticalIcon />
</StyledOperationButton>
<OperationMenu
anchorEl={anchorEl}
open={operationOpen}
placement="bottom-start"
zIndex={menuIndex}
onSelect={type => {
switch (type) {
case 'add':
onAdd();
break;
case 'move':
setPivotsMenuOpen(true);
break;
case 'delete':
onDelete();
break;
}
setOperationOpen(false);
onMenuClose?.();
}}
currentMeta={currentMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
<PivotsMenu
anchorEl={anchorEl}
open={pivotsMenuOpen}
placement="bottom-start"
zIndex={menuIndex}
metas={metas}
currentMeta={currentMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
showRemovePivots={true}
/>
</div>
</MuiClickAwayListener>
);
};

View File

@@ -0,0 +1,74 @@
import type { PureMenuProps } from '@affine/component';
import { MenuItem, PureMenu } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { MoveToIcon, PenIcon, PlusIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import type { ReactElement } from 'react';
import type { BlockSuiteWorkspace } from '../../../shared';
import { CopyLink, MoveToTrash } from '../operation-menu-items';
export type OperationMenuProps = {
onSelect: (type: OperationMenuItems['type']) => void;
blockSuiteWorkspace: BlockSuiteWorkspace;
currentMeta: PageMeta;
} & PureMenuProps;
export type OperationMenuItems = {
label: string;
icon: ReactElement;
type: 'add' | 'move' | 'rename' | 'delete' | 'copy';
disabled?: boolean;
};
const menuItems: OperationMenuItems[] = [
{
label: 'Add a subpage inside',
icon: <PlusIcon />,
type: 'add',
},
{
label: 'Move to',
icon: <MoveToIcon />,
type: 'move',
},
{
label: 'Rename',
icon: <PenIcon />,
type: 'rename',
disabled: true,
},
];
export const OperationMenu = ({
onSelect,
blockSuiteWorkspace,
currentMeta,
...menuProps
}: OperationMenuProps) => {
const { t } = useTranslation();
return (
<PureMenu width={256} {...menuProps}>
{menuItems.map((item, index) => {
return (
<MenuItem
key={index}
onClick={() => {
onSelect(item.type);
}}
icon={item.icon}
disabled={!!item.disabled}
>
{t(item.label)}
</MenuItem>
);
})}
<MoveToTrash
currentMeta={currentMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
<CopyLink />
</PureMenu>
);
};

View File

@@ -0,0 +1,65 @@
import { ArrowDownSmallIcon, EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { workspacePreferredModeAtom } from '../../../atoms';
import { OperationButton } from './OperationButton';
import { StyledCollapsedButton, StyledPivot } from './styles';
import type { TreeNode } from './types';
export const PivotRender: TreeNode['render'] = (
node,
{ isOver, onAdd, onDelete, collapsed, setCollapsed, isSelected },
renderProps
) => {
const {
onClick,
showOperationButton = false,
currentMeta,
metas = [],
blockSuiteWorkspace,
} = renderProps!;
const record = useAtomValue(workspacePreferredModeAtom);
const router = useRouter();
const [isHover, setIsHover] = useState(false);
const active = router.query.pageId === node.id;
return (
<StyledPivot
onClick={e => {
onClick?.(e, node);
}}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
isOver={isOver || isSelected}
active={active}
>
<StyledCollapsedButton
collapse={collapsed}
show={!!node.children?.length}
onClick={e => {
e.stopPropagation();
setCollapsed(node.id, !collapsed);
}}
>
<ArrowDownSmallIcon />
</StyledCollapsedButton>
{record[node.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
<span>{currentMeta?.title || 'Untitled'}</span>
{showOperationButton && (
<OperationButton
onAdd={onAdd}
onDelete={onDelete}
metas={metas}
currentMeta={currentMeta!}
blockSuiteWorkspace={blockSuiteWorkspace!}
isHover={isHover}
onMenuClose={() => setIsHover(false)}
/>
)}
</StyledPivot>
);
};

View File

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

View File

@@ -0,0 +1,85 @@
import { MuiCollapse, TreeView } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { ArrowDownSmallIcon, PivotsIcon } from '@blocksuite/icons';
import type { MouseEvent } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
import { usePivotData } from '../hooks/usePivotData';
import { usePivotHandler } from '../hooks/usePivotHandler';
import { PivotRender } from '../PivotRender';
import { StyledCollapsedButton, StyledPivot } from '../styles';
import EmptyItem from './EmptyItem';
import type { PivotsMenuProps } from './PivotsMenu';
export const Pivots = ({
metas,
blockSuiteWorkspace,
currentMeta,
}: Pick<PivotsMenuProps, 'metas' | 'blockSuiteWorkspace' | 'currentMeta'>) => {
const { t } = useTranslation();
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [showPivot, setShowPivot] = useState(true);
const { handleDrop } = usePivotHandler({
blockSuiteWorkspace,
metas,
});
const { data } = usePivotData({
metas,
pivotRender: PivotRender,
blockSuiteWorkspace,
onClick: (e, node) => {
handleDrop(currentMeta.id, node.id, {
bottomLine: false,
topLine: false,
internal: true,
});
},
});
const isPivotEmpty = useMemo(
() => metas.filter(meta => !meta.trash).length === 0,
[metas]
);
return (
<>
<StyledPivot
onClick={() => {
setPageMeta(currentMeta.id, { isPivots: true });
}}
>
<StyledCollapsedButton
onClick={useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setShowPivot(!showPivot);
},
[showPivot]
)}
collapse={showPivot}
>
<ArrowDownSmallIcon />
</StyledCollapsedButton>
<PivotsIcon />
{t('Pivots')}
</StyledPivot>
<MuiCollapse
in={showPivot}
style={{
maxHeight: 300,
paddingLeft: '16px',
overflowY: 'auto',
}}
>
{isPivotEmpty ? (
<EmptyItem />
) : (
<TreeView data={data} indent={16} enableDnd={false} />
)}
</MuiCollapse>
</>
);
};
export default Pivots;

View File

@@ -0,0 +1,109 @@
import type { PureMenuProps } from '@affine/component';
import { Input, PureMenu } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { RemoveIcon, SearchIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import React, { useState } from 'react';
import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
import type { BlockSuiteWorkspace } from '../../../../shared';
import {
StyledMenuContent,
StyledMenuFooter,
StyledMenuSubTitle,
StyledPivot,
StyledSearchContainer,
} from '../styles';
import { Pivots } from './Pivots';
export type PivotsMenuProps = {
metas: PageMeta[];
currentMeta: PageMeta;
blockSuiteWorkspace: BlockSuiteWorkspace;
showRemovePivots?: boolean;
} & PureMenuProps;
export const PivotsMenu = ({
metas,
currentMeta,
blockSuiteWorkspace,
showRemovePivots = false,
...pureMenuProps
}: PivotsMenuProps) => {
const { t } = useTranslation();
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [query, setQuery] = useState('');
const isSearching = query.length > 0;
const searchResult = metas.filter(
meta => !meta.trash && meta.title.includes(query)
);
return (
<PureMenu width={320} height={480} {...pureMenuProps}>
<StyledSearchContainer>
<label>
<SearchIcon />
</label>
<Input
value={query}
onChange={setQuery}
placeholder={t('Move page to...')}
height={32}
noBorder={true}
onClick={e => e.stopPropagation()}
/>
</StyledSearchContainer>
<StyledMenuContent>
{isSearching && (
<>
<StyledMenuSubTitle>
{searchResult.length
? t('Find results', { number: searchResult.length })
: t('Find 0 result')}
</StyledMenuSubTitle>
{searchResult.map(meta => {
return <StyledPivot key={meta.id}>{meta.title}</StyledPivot>;
})}
</>
)}
{!isSearching && (
<>
<StyledMenuSubTitle>Suggested</StyledMenuSubTitle>
<Pivots
metas={metas}
blockSuiteWorkspace={blockSuiteWorkspace}
currentMeta={currentMeta}
/>
</>
)}
</StyledMenuContent>
{showRemovePivots && (
<StyledMenuFooter>
<StyledPivot
onClick={() => {
setPageMeta(currentMeta.id, { isPivots: false });
const parentMeta = metas.find(m =>
m.subpageIds.includes(currentMeta.id)
);
if (!parentMeta) return;
const newSubpageIds = [...parentMeta.subpageIds];
const deleteIndex = newSubpageIds.findIndex(
id => id === currentMeta.id
);
newSubpageIds.splice(deleteIndex, 1);
setPageMeta(parentMeta.id, { subpageIds: newSubpageIds });
}}
>
<RemoveIcon />
{t('Remove from Pivots')}
</StyledPivot>
<p>{t('RFP')}</p>
</StyledMenuFooter>
)}
</PureMenu>
);
};

View File

@@ -1,13 +1,14 @@
import type { PageMeta } from '@blocksuite/store';
import { useMemo } from 'react';
import { TreeNodeRender } from './TreeNodeRender';
import type { TreeNode } from './types';
export const flattenToTree = (
handleMetas: PageMeta[],
renderProps: { openPage: (pageId: string) => void }
import type { RenderProps, TreeNode } from '../types';
const flattenToTree = (
metas: PageMeta[],
pivotRender: TreeNode['render'],
renderProps: RenderProps
): TreeNode[] => {
// Compatibility process: the old data not has `subpageIds`, it is a root page
const metas = JSON.parse(JSON.stringify(handleMetas)) as PageMeta[];
const rootMetas = metas
.filter(meta => {
if (meta.subpageIds) {
@@ -19,29 +20,55 @@ export const flattenToTree = (
}
return true;
})
.filter(meta => !meta.trash);
.filter(meta => meta.isPivots === true);
const helper = (internalMetas: PageMeta[]): TreeNode[] => {
return internalMetas.reduce<TreeNode[]>((returnedMetas, internalMeta) => {
const { subpageIds = [] } = internalMeta;
const childrenMetas = subpageIds
.map(id => metas.find(m => m.id === id)!)
.filter(meta => !meta.trash);
// FIXME: remove ts-ignore after blocksuite update
.filter(m => m);
// @ts-ignore
const returnedMeta: TreeNode = {
...internalMeta,
children: helper(childrenMetas),
render: (node, props) =>
TreeNodeRender!(node, props, {
pageMeta: internalMeta,
pivotRender(node, props, {
...renderProps,
currentMeta: internalMeta,
metas,
}),
};
// @ts-ignore
returnedMetas.push(returnedMeta);
return returnedMetas;
}, []);
};
return helper(rootMetas);
};
export const usePivotData = ({
metas,
pivotRender,
blockSuiteWorkspace,
onClick,
showOperationButton,
}: {
metas: PageMeta[];
pivotRender: TreeNode['render'];
} & RenderProps) => {
const data = useMemo(
() =>
flattenToTree(metas, pivotRender, {
blockSuiteWorkspace,
onClick,
showOperationButton,
}),
[blockSuiteWorkspace, metas, onClick, pivotRender, showOperationButton]
);
return {
data,
};
};
export default usePivotData;

View File

@@ -0,0 +1,198 @@
import type { TreeViewProps } from '@affine/component';
import { DebugLogger } from '@affine/debug';
import type { PageMeta } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import { useCallback } from 'react';
import { useBlockSuiteWorkspaceHelper } from '../../../../hooks/use-blocksuite-workspace-helper';
import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
import type { BlockSuiteWorkspace } from '../../../../shared';
import type { NodeRenderProps, TreeNode } from '../types';
const logger = new DebugLogger('pivot');
const findRootIds = (metas: PageMeta[], id: string): string[] => {
const parentMeta = metas.find(m => m.subpageIds?.includes(id));
if (!parentMeta) {
return [id];
}
return [parentMeta.id, ...findRootIds(metas, parentMeta.id)];
};
export const usePivotHandler = ({
blockSuiteWorkspace,
metas,
onAdd,
onDelete,
onDrop,
}: {
blockSuiteWorkspace: BlockSuiteWorkspace;
metas: PageMeta[];
onAdd?: (addedId: string, parentId: string) => void;
onDelete?: TreeViewProps<NodeRenderProps>['onDelete'];
onDrop?: TreeViewProps<NodeRenderProps>['onDrop'];
}) => {
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { getPageMeta, setPageMeta, shiftPageMeta } =
usePageMetaHelper(blockSuiteWorkspace);
const handleAdd = useCallback(
(node: TreeNode) => {
const id = nanoid();
createPage(id, node.id);
onAdd?.(id, node.id);
},
[createPage, onAdd]
);
const handleDelete = useCallback(
(node: TreeNode) => {
const removeToTrash = (currentMeta: PageMeta) => {
const { subpageIds = [] } = currentMeta;
setPageMeta(currentMeta.id, {
trash: true,
trashDate: +new Date(),
});
subpageIds.forEach(id => {
const subcurrentMeta = getPageMeta(id);
subcurrentMeta && removeToTrash(subcurrentMeta);
});
};
removeToTrash(metas.find(m => m.id === node.id)!);
onDelete?.(node);
},
[metas, getPageMeta, onDelete, setPageMeta]
);
const handleDrop = useCallback(
(
dragId: string,
dropId: string,
position: {
topLine: boolean;
bottomLine: boolean;
internal: boolean;
}
) => {
if (dragId === dropId) {
return;
}
const dropRootIds = findRootIds(metas, dropId);
if (dropRootIds.includes(dragId)) {
return;
}
const { topLine, bottomLine } = position;
logger.info('handleDrop', {
dragId,
dropId,
bottomLine,
metas,
});
const dragParentMeta = metas.find(meta =>
meta.subpageIds?.includes(dragId)
);
if (bottomLine || topLine) {
const insertOffset = bottomLine ? 1 : 0;
const dropParentMeta = metas.find(m => m.subpageIds?.includes(dropId));
if (!dropParentMeta) {
// drop into root
logger.info('drop into root and resort');
if (dragParentMeta) {
const newSubpageIds = [...(dragParentMeta.subpageIds ?? [])];
const deleteIndex = dragParentMeta.subpageIds?.findIndex(
id => id === dragId
);
newSubpageIds.splice(deleteIndex, 1);
setPageMeta(dragParentMeta.id, {
subpageIds: newSubpageIds,
});
}
logger.info('resort root meta');
const insertIndex =
metas.findIndex(m => m.id === dropId) + insertOffset;
shiftPageMeta(dragId, insertIndex);
return onDrop?.(dragId, dropId, position);
}
if (
dragParentMeta &&
(dragParentMeta.id === dropId ||
dragParentMeta.id === dropParentMeta!.id)
) {
logger.info('drop to resort');
// need to resort
const newSubpageIds = [...(dragParentMeta.subpageIds ?? [])];
const deleteIndex = newSubpageIds.findIndex(id => id === dragId);
newSubpageIds.splice(deleteIndex, 1);
const insertIndex =
newSubpageIds.findIndex(id => id === dropId) + insertOffset;
newSubpageIds.splice(insertIndex, 0, dragId);
setPageMeta(dropParentMeta.id, {
subpageIds: newSubpageIds,
});
return onDrop?.(dragId, dropId, position);
}
logger.info('drop into drop node parent and resort');
if (dragParentMeta) {
const metaIndex = dragParentMeta.subpageIds.findIndex(
id => id === dragId
);
const newSubpageIds = [...dragParentMeta.subpageIds];
newSubpageIds.splice(metaIndex, 1);
setPageMeta(dragParentMeta.id, {
subpageIds: newSubpageIds,
});
}
const newSubpageIds = [...(dropParentMeta!.subpageIds ?? [])];
const insertIndex = newSubpageIds.findIndex(id => id === dropId) + 1;
newSubpageIds.splice(insertIndex, 0, dragId);
setPageMeta(dropParentMeta.id, {
subpageIds: newSubpageIds,
});
return onDrop?.(dragId, dropId, position);
}
logger.info('drop into the drop node');
// drop into the node
if (dragParentMeta && dragParentMeta.id === dropId) {
return;
}
if (dragParentMeta) {
const metaIndex = dragParentMeta.subpageIds.findIndex(
id => id === dragId
);
const newSubpageIds = [...dragParentMeta.subpageIds];
newSubpageIds.splice(metaIndex, 1);
setPageMeta(dragParentMeta.id, {
subpageIds: newSubpageIds,
});
}
const dropMeta = metas.find(meta => meta.id === dropId)!;
const newSubpageIds = [dragId, ...(dropMeta.subpageIds ?? [])];
setPageMeta(dropMeta.id, {
subpageIds: newSubpageIds,
});
},
[metas, onDrop, setPageMeta, shiftPageMeta]
);
return {
handleDrop,
handleAdd,
handleDelete,
};
};
export default usePivotHandler;

View File

@@ -0,0 +1,5 @@
export * from './hooks/usePivotData';
export * from './hooks/usePivotHandler';
export * from './PivotRender';
export * from './PivotsMenu/PivotsMenu';
export * from './types';

View File

@@ -0,0 +1,117 @@
import {
alpha,
displayFlex,
IconButton,
styled,
textEllipsis,
} from '@affine/component';
export const StyledCollapsedButton = styled('button')<{
collapse: boolean;
show?: boolean;
}>(({ collapse, show = true, theme }) => {
return {
width: '16px',
height: '16px',
fontSize: '16px',
position: 'absolute',
left: '0',
top: '0',
bottom: '0',
margin: 'auto',
color: theme.colors.iconColor,
opacity: '.6',
display: show ? 'block' : 'none',
svg: {
transform: `rotate(${collapse ? '0' : '-90'}deg)`,
},
};
});
export const StyledPivot = styled('div')<{
disable?: boolean;
active?: boolean;
isOver?: boolean;
}>(({ disable = false, active = false, theme, isOver }) => {
return {
width: '100%',
height: '32px',
borderRadius: '8px',
...displayFlex('flex-start', 'center'),
padding: '0 2px 0 16px',
position: 'relative',
color: disable
? theme.colors.disableColor
: active
? theme.colors.primaryColor
: theme.colors.textColor,
cursor: disable ? 'not-allowed' : 'pointer',
background: isOver ? alpha(theme.colors.primaryColor, 0.06) : '',
fontSize: theme.font.base,
span: {
flexGrow: '1',
textAlign: 'left',
...textEllipsis(1),
},
'> svg': {
fontSize: '20px',
marginRight: '8px',
flexShrink: '0',
color: active ? theme.colors.primaryColor : theme.colors.iconColor,
},
':hover': {
backgroundColor: disable ? '' : theme.colors.hoverBackground,
},
};
});
export const StyledOperationButton = styled(IconButton)<{ visible: boolean }>(
({ visible }) => {
return {
visibility: visible ? 'visible' : 'hidden',
};
}
);
export const StyledSearchContainer = styled('div')(({ theme }) => {
return {
width: 'calc(100% - 24px)',
margin: '0 auto',
...displayFlex('flex-start', 'center'),
borderBottom: `1px solid ${theme.colors.borderColor}`,
label: {
color: theme.colors.iconColor,
fontSize: '20px',
height: '20px',
},
};
});
export const StyledMenuContent = styled('div')(() => {
return {
height: '266px',
overflow: 'auto',
};
});
export const StyledMenuSubTitle = styled('div')(({ theme }) => {
return {
color: theme.colors.secondaryTextColor,
lineHeight: '36px',
padding: '0 12px',
};
});
export const StyledMenuFooter = styled('div')(({ theme }) => {
return {
width: 'calc(100% - 24px)',
margin: '0 auto',
borderTop: `1px solid ${theme.colors.borderColor}`,
padding: '6px 0',
p: {
paddingLeft: '44px',
color: theme.colors.secondaryTextColor,
fontSize: '14px',
},
};
});

View File

@@ -0,0 +1,18 @@
import type { Node } from '@affine/component';
import type { PageMeta } from '@blocksuite/store';
import type { MouseEvent } from 'react';
import type { BlockSuiteWorkspace } from '../../../shared';
export type RenderProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
onClick?: (e: MouseEvent<HTMLDivElement>, node: TreeNode) => void;
showOperationButton?: boolean;
};
export type NodeRenderProps = RenderProps & {
metas: PageMeta[];
currentMeta: PageMeta;
};
export type TreeNode = Node<NodeRenderProps>;

View File

@@ -1,8 +1,12 @@
import { styled } from '@affine/component';
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
import { Button } from '@affine/component';
import { Input } from '@affine/component';
import { MuiAvatar } from '@affine/component';
import {
Button,
Input,
Modal,
ModalCloseButton,
ModalWrapper,
MuiAvatar,
styled,
} from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { EmailIcon } from '@blocksuite/icons';
import type React from 'react';
@@ -87,7 +91,7 @@ export const InviteMemberModal = ({
setShowMemberPreview(false);
}, [])}
placeholder={t('Invite placeholder')}
></Input>
/>
{showMemberPreview && gmailReg.test(email) && (
<Suspense fallback="loading...">
<Result workspaceId={workspaceId} queryEmail={email} />

View File

@@ -20,10 +20,14 @@ import type { PageMeta } from '@blocksuite/store';
import type React from 'react';
import { useState } from 'react';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import { MoveTo } from '../../../affine/operation-menu-items';
export type OperationCellProps = {
pageMeta: PageMeta;
metas: PageMeta[];
blockSuiteWorkspace: BlockSuiteWorkspace;
onOpenPageInNewTab: (pageId: string) => void;
onToggleFavoritePage: (pageId: string) => void;
onToggleTrashPage: (pageId: string) => void;
@@ -31,6 +35,8 @@ export type OperationCellProps = {
export const OperationCell: React.FC<OperationCellProps> = ({
pageMeta,
metas,
blockSuiteWorkspace,
onOpenPageInNewTab,
onToggleFavoritePage,
onToggleTrashPage,
@@ -59,6 +65,11 @@ export const OperationCell: React.FC<OperationCellProps> = ({
>
{t('Open in new tab')}
</MenuItem>
<MoveTo
metas={metas}
currentMeta={pageMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
<MenuItem
data-testid="move-to-trash"
onClick={() => {

View File

@@ -1,11 +1,13 @@
import {
Content,
IconButton,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Tooltip,
} from '@affine/component';
import { Content, IconButton, Tooltip } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import {
EdgelessIcon,
@@ -223,6 +225,8 @@ export const PageList: React.FC<PageListProps> = ({
) : (
<OperationCell
pageMeta={pageMeta}
metas={pageList}
blockSuiteWorkspace={blockSuiteWorkspace}
onOpenPageInNewTab={pageId => {
onClickPage(pageId, true);
}}

View File

@@ -1,21 +1,16 @@
// fixme(himself65): refactor this file
import { Confirm, FlexWrapper, Menu, MenuItem } from '@affine/component';
import { IconButton } from '@affine/component';
import { FlexWrapper, IconButton, Menu, MenuItem } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import {
DeleteTemporarilyIcon,
ExportIcon,
ExportToHtmlIcon,
ExportToMarkdownIcon,
EdgelessIcon,
FavoritedIcon,
FavoriteIcon,
MoreVerticalIcon,
PageIcon,
} from '@blocksuite/icons';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useTheme } from '@mui/material';
import { useAtom } from 'jotai';
import { useState } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id';
@@ -25,6 +20,11 @@ import {
usePageMetaHelper,
} from '../../../../hooks/use-page-meta';
import { toast } from '../../../../utils';
import {
Export,
MoveTo,
MoveToTrash,
} from '../../../affine/operation-menu-items';
export const EditorOptionMenu = () => {
const { t } = useTranslation();
@@ -39,12 +39,12 @@ export const EditorOptionMenu = () => {
const pageMeta = usePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
const allMetas = usePageMeta(workspace?.blockSuiteWorkspace ?? null);
const [record, set] = useAtom(workspacePreferredModeAtom);
const mode = record[pageId] ?? 'page';
assertExists(pageMeta);
const { favorite, trash } = pageMeta;
const { favorite } = pageMeta;
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [open, setOpen] = useState(false);
const EditMenu = (
<>
@@ -56,7 +56,6 @@ export const EditorOptionMenu = () => {
favorite ? t('Removed from Favorites') : t('Added to Favorites')
);
}}
iconSize={[20, 20]}
icon={
favorite ? (
<FavoritedIcon style={{ color: theme.colors.primaryColor }} />
@@ -69,7 +68,6 @@ export const EditorOptionMenu = () => {
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
iconSize={[20, 20]}
data-testid="editor-option-menu-edgeless"
onClick={() => {
set(record => ({
@@ -81,48 +79,17 @@ export const EditorOptionMenu = () => {
{t('Convert to ')}
{mode === 'page' ? t('Edgeless') : t('Page')}
</MenuItem>
<Menu
width={248}
placement="left-start"
content={
<>
<MenuItem
onClick={() => {
// @ts-expect-error
globalThis.currentEditor.contentParser.onExportHtml();
}}
icon={<ExportToHtmlIcon />}
iconSize={[20, 20]}
>
{t('Export to HTML')}
</MenuItem>
<MenuItem
onClick={() => {
// @ts-expect-error
globalThis.currentEditor.contentParser.onExportMarkdown();
}}
icon={<ExportToMarkdownIcon />}
iconSize={[20, 20]}
>
{t('Export to Markdown')}
</MenuItem>
</>
}
>
<MenuItem icon={<ExportIcon />} iconSize={[20, 20]} isDir={true}>
{t('Export')}
</MenuItem>
</Menu>
<MenuItem
data-testid="editor-option-menu-delete"
onClick={() => {
setOpen(true);
}}
icon={<DeleteTemporarilyIcon />}
iconSize={[20, 20]}
>
{t('Delete')}
</MenuItem>
<Export />
<MoveTo
metas={allMetas}
currentMeta={pageMeta}
blockSuiteWorkspace={workspace?.blockSuiteWorkspace}
/>
<MoveToTrash
testId="editor-option-menu-delete"
currentMeta={pageMeta}
blockSuiteWorkspace={workspace?.blockSuiteWorkspace}
/>
</>
);
@@ -141,26 +108,6 @@ export const EditorOptionMenu = () => {
</IconButton>
</Menu>
</FlexWrapper>
<Confirm
title={t('Delete page?')}
content={t('will be moved to Trash', {
title: pageMeta.title || 'Untitled',
})}
confirmText={t('Delete')}
confirmType="danger"
open={open}
onConfirm={() => {
toast(t('Moved to Trash'));
setOpen(false);
setPageMeta(pageId, { trash: !trash, trashDate: +new Date() });
}}
onClose={() => {
setOpen(false);
}}
onCancel={() => {
setOpen(false);
}}
/>
</>
);
};

View File

@@ -0,0 +1,120 @@
import { MuiCollapse, TreeView } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { ArrowDownSmallIcon, PivotsIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import type { MouseEvent } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { RemWorkspace } from '../../../shared';
import type { TreeNode } from '../../affine/pivots';
import {
PivotRender,
usePivotData,
usePivotHandler,
} from '../../affine/pivots';
import EmptyItem from './favorite/empty-item';
import { StyledCollapseButton, StyledListItem } from './shared-styles';
export const PivotInternal = ({
currentWorkspace,
openPage,
allMetas,
}: {
currentWorkspace: RemWorkspace;
openPage: (pageId: string) => void;
allMetas: PageMeta[];
}) => {
const handlePivotClick = useCallback(
(e: MouseEvent<HTMLDivElement>, node: TreeNode) => {
openPage(node.id);
},
[openPage]
);
const onAdd = useCallback(
(id: string) => {
openPage(id);
},
[openPage]
);
const { data } = usePivotData({
metas: allMetas.filter(meta => !meta.trash),
pivotRender: PivotRender,
blockSuiteWorkspace: currentWorkspace.blockSuiteWorkspace,
onClick: handlePivotClick,
showOperationButton: true,
});
const { handleAdd, handleDelete, handleDrop } = usePivotHandler({
blockSuiteWorkspace: currentWorkspace.blockSuiteWorkspace,
metas: allMetas,
onAdd,
});
return (
<TreeView
data={data}
onAdd={handleAdd}
onDelete={handleDelete}
onDrop={handleDrop}
indent={16}
/>
);
};
export const Pivots = ({
currentWorkspace,
openPage,
allMetas,
}: {
currentWorkspace: RemWorkspace;
openPage: (pageId: string) => void;
allMetas: PageMeta[];
}) => {
const { t } = useTranslation();
const [showPivot, setShowPivot] = useState(true);
const isPivotEmpty = useMemo(
() => allMetas.filter(meta => !meta.trash).length === 0,
[allMetas]
);
return (
<>
<StyledListItem>
<StyledCollapseButton
onClick={useCallback(() => {
setShowPivot(!showPivot);
}, [showPivot])}
collapse={showPivot}
>
<ArrowDownSmallIcon />
</StyledCollapseButton>
<PivotsIcon />
{t('Pivots')}
</StyledListItem>
<MuiCollapse
in={showPivot}
style={{
maxHeight: 300,
paddingLeft: '16px',
overflowY: 'auto',
}}
>
{isPivotEmpty ? (
<EmptyItem />
) : (
<PivotInternal
currentWorkspace={currentWorkspace}
openPage={openPage}
allMetas={allMetas}
/>
)}
</MuiCollapse>
</>
);
};
export default Pivots;

View File

@@ -16,7 +16,7 @@ import { usePageMeta } from '../../../hooks/use-page-meta';
import type { RemWorkspace } from '../../../shared';
import { SidebarSwitch } from '../../affine/sidebar-switch';
import Favorite from './favorite';
import { Pivot } from './pivot';
import { Pivots } from './Pivots';
import { StyledListItem } from './shared-styles';
import {
StyledLink,
@@ -142,7 +142,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
currentWorkspace={currentWorkspace}
/>
{config.enableSubpage && !!currentWorkspace && (
<Pivot
<Pivots
currentWorkspace={currentWorkspace}
openPage={openPage}
allMetas={pageMeta}

View File

@@ -1,110 +0,0 @@
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,242 +0,0 @@
import { MuiCollapse, TreeView } from '@affine/component';
import { DebugLogger } from '@affine/debug';
import { useTranslation } from '@affine/i18n';
import { ArrowDownSmallIcon, PivotsIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import { useCallback, useMemo, useState } from 'react';
import { useBlockSuiteWorkspaceHelper } from '../../../../hooks/use-blocksuite-workspace-helper';
import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
import type { RemWorkspace } from '../../../../shared';
import EmptyItem from '../favorite/empty-item';
import { StyledCollapseButton, StyledListItem } from '../shared-styles';
import type { TreeNode } from './types';
import { flattenToTree } from './utils';
const logger = new DebugLogger('pivot');
export const PivotInternal = ({
currentWorkspace,
openPage,
allMetas,
}: {
currentWorkspace: RemWorkspace;
openPage: (pageId: string) => void;
allMetas: PageMeta[];
}) => {
const { createPage } = useBlockSuiteWorkspaceHelper(
currentWorkspace.blockSuiteWorkspace
);
const { getPageMeta, setPageMeta, shiftPageMeta } = usePageMetaHelper(
currentWorkspace.blockSuiteWorkspace
);
const treeData = useMemo(
() => flattenToTree(allMetas, { openPage }),
[allMetas, openPage]
);
const handleAdd = useCallback(
(node: TreeNode) => {
const id = nanoid();
createPage(id, node.id);
openPage(id);
},
[createPage, openPage]
);
const handleDelete = useCallback(
(node: TreeNode) => {
const removeToTrash = (pageMeta: PageMeta) => {
const { subpageIds = [] } = pageMeta;
setPageMeta(pageMeta.id, { trash: true, trashDate: +new Date() });
subpageIds.forEach(id => {
const subpageMeta = getPageMeta(id);
subpageMeta && removeToTrash(subpageMeta);
});
};
removeToTrash(node as PageMeta);
},
[getPageMeta, setPageMeta]
);
const handleDrop = useCallback(
(
dragNode: TreeNode,
dropNode: TreeNode,
position: {
topLine: boolean;
bottomLine: boolean;
internal: boolean;
}
) => {
const { topLine, bottomLine } = position;
logger.info('handleDrop', { dragNode, dropNode, bottomLine, allMetas });
const dragParentMeta = allMetas.find(meta =>
meta.subpageIds?.includes(dragNode.id)
);
if (bottomLine || topLine) {
const insertOffset = bottomLine ? 1 : 0;
const dropParentMeta = allMetas.find(m =>
m.subpageIds?.includes(dropNode.id)
);
if (!dropParentMeta) {
// drop into root
logger.info('drop into root and resort');
if (dragParentMeta) {
const newSubpageIds = [...(dragParentMeta.subpageIds ?? [])];
const deleteIndex = dragParentMeta.subpageIds?.findIndex(
id => id === dragNode.id
);
newSubpageIds.splice(deleteIndex, 1);
setPageMeta(dragParentMeta.id, {
subpageIds: newSubpageIds,
});
}
logger.info('resort root meta');
const insertIndex =
allMetas.findIndex(m => m.id === dropNode.id) + insertOffset;
shiftPageMeta(dragNode.id, insertIndex);
return;
}
if (
dragParentMeta &&
(dragParentMeta.id === dropNode.id ||
dragParentMeta.id === dropParentMeta!.id)
) {
logger.info('drop to resort');
// need to resort
const newSubpageIds = [...(dragParentMeta.subpageIds ?? [])];
const deleteIndex = newSubpageIds.findIndex(id => id === dragNode.id);
newSubpageIds.splice(deleteIndex, 1);
const insertIndex =
newSubpageIds.findIndex(id => id === dropNode.id) + insertOffset;
newSubpageIds.splice(insertIndex, 0, dragNode.id);
setPageMeta(dropParentMeta.id, {
subpageIds: newSubpageIds,
});
return;
}
logger.info('drop into drop node parent and resort');
if (dragParentMeta) {
const metaIndex = dragParentMeta.subpageIds.findIndex(
id => id === dragNode.id
);
const newSubpageIds = [...dragParentMeta.subpageIds];
newSubpageIds.splice(metaIndex, 1);
setPageMeta(dragParentMeta.id, {
subpageIds: newSubpageIds,
});
}
const newSubpageIds = [...(dropParentMeta!.subpageIds ?? [])];
const insertIndex =
newSubpageIds.findIndex(id => id === dropNode.id) + 1;
newSubpageIds.splice(insertIndex, 0, dragNode.id);
setPageMeta(dropParentMeta.id, {
subpageIds: newSubpageIds,
});
return;
}
logger.info('drop into the drop node');
// drop into the node
if (dragParentMeta && dragParentMeta.id === dropNode.id) {
return;
}
if (dragParentMeta) {
const metaIndex = dragParentMeta.subpageIds.findIndex(
id => id === dragNode.id
);
const newSubpageIds = [...dragParentMeta.subpageIds];
newSubpageIds.splice(metaIndex, 1);
setPageMeta(dragParentMeta.id, {
subpageIds: newSubpageIds,
});
}
const dropMeta = allMetas.find(meta => meta.id === dropNode.id)!;
const newSubpageIds = [dragNode.id, ...(dropMeta.subpageIds ?? [])];
setPageMeta(dropMeta.id, {
subpageIds: newSubpageIds,
});
},
[allMetas, setPageMeta, shiftPageMeta]
);
return (
<TreeView
data={treeData}
onAdd={handleAdd}
onDelete={handleDelete}
onDrop={handleDrop}
indent={16}
/>
);
};
export const Pivot = ({
currentWorkspace,
openPage,
allMetas,
}: {
currentWorkspace: RemWorkspace;
openPage: (pageId: string) => void;
allMetas: PageMeta[];
}) => {
const { t } = useTranslation();
const [showPivot, setShowPivot] = useState(true);
const isPivotEmpty = useMemo(
() => allMetas.filter(meta => !meta.trash).length === 0,
[allMetas]
);
return (
<>
<StyledListItem>
<StyledCollapseButton
onClick={useCallback(() => {
setShowPivot(!showPivot);
}, [showPivot])}
collapse={showPivot}
>
<ArrowDownSmallIcon />
</StyledCollapseButton>
<PivotsIcon />
{t('Pivots')}
</StyledListItem>
<MuiCollapse
in={showPivot}
style={{
maxHeight: 300,
paddingLeft: '16px',
overflowY: 'auto',
}}
>
{isPivotEmpty ? (
<EmptyItem />
) : (
<PivotInternal
currentWorkspace={currentWorkspace}
openPage={openPage}
allMetas={allMetas}
/>
)}
</MuiCollapse>
</>
);
};
export default Pivot;

View File

@@ -1,51 +0,0 @@
import { ArrowDownSmallIcon, EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import { workspacePreferredModeAtom } from '../../../../atoms';
import { StyledCollapseButton, StyledCollapseItem } from '../shared-styles';
import { OperationButton } from './OperationButton';
import type { TreeNode } from './types';
export const TreeNodeRender: TreeNode['render'] = (
node,
{ isOver, onAdd, onDelete, collapsed, setCollapsed },
extendProps
) => {
const { openPage, pageMeta } = extendProps as {
openPage: (pageId: string) => void;
pageMeta: PageMeta;
};
const record = useAtomValue(workspacePreferredModeAtom);
const router = useRouter();
const active = router.query.pageId === node.id;
return (
<StyledCollapseItem
onClick={() => {
if (active) {
return;
}
openPage(node.id);
}}
isOver={isOver}
active={active}
>
<StyledCollapseButton
collapse={collapsed}
show={!!node.children?.length}
onClick={e => {
e.stopPropagation();
setCollapsed(!collapsed);
}}
>
<ArrowDownSmallIcon />
</StyledCollapseButton>
{record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
<span>{node.title || 'Untitled'}</span>
<OperationButton onAdd={onAdd} onDelete={onDelete} />
</StyledCollapseItem>
);
};

View File

@@ -1,2 +0,0 @@
export * from './Pivot';
export * from './types';

View File

@@ -1,29 +0,0 @@
import { IconButton, styled } from '@affine/component';
export const StyledOperationButton = styled('button')(({ theme }) => {
return {
height: '20px',
width: '20px',
fontSize: '20px',
color: theme.colors.iconColor,
display: 'none',
':hover': {
background: theme.colors.hoverBackground,
},
};
});
export const StyledCollapsedButton = styled(IconButton, {
shouldForwardProp: prop => {
return !['show'].includes(prop as string);
},
})<{ show: boolean }>(({ show }) => {
return {
display: show ? 'block' : 'none',
position: 'absolute',
left: '0px',
top: '0px',
bottom: '0px',
margin: 'auto',
};
});

View File

@@ -1,4 +0,0 @@
import type { Node } from '@affine/component';
import type { PageMeta } from '@blocksuite/store';
export type TreeNode = Node<PageMeta>;

View File

@@ -11,6 +11,8 @@ declare module '@blocksuite/store' {
trashDate?: number;
// whether to create the page with the default template
init?: boolean;
// use for subpage
isPivots?: boolean;
}
}