From b6ded3077099a63770d96d3b155a64e396b6154e Mon Sep 17 00:00:00 2001
From: Qi <474021214@qq.com>
Date: Thu, 30 Mar 2023 17:37:41 +0800
Subject: [PATCH] feat: support pivots menu (#1755)
---
.../affine/operation-menu-items/CopyLink.tsx | 24 ++
.../affine/operation-menu-items/Export.tsx | 50 ++++
.../affine/operation-menu-items/MoveTo.tsx | 46 ++++
.../operation-menu-items/MoveToTrash.tsx | 60 +++++
.../affine/operation-menu-items/index.ts | 4 +
.../affine/pivots/OperationButton.tsx | 103 ++++++++
.../affine/pivots/OperationMenu.tsx | 74 ++++++
.../components/affine/pivots/PivotRender.tsx | 65 +++++
.../affine/pivots/PivotsMenu/EmptyItem.tsx | 10 +
.../affine/pivots/PivotsMenu/Pivots.tsx | 85 ++++++
.../affine/pivots/PivotsMenu/PivotsMenu.tsx | 109 ++++++++
.../pivots/hooks/usePivotData.ts} | 51 +++-
.../affine/pivots/hooks/usePivotHandler.ts | 198 ++++++++++++++
.../web/src/components/affine/pivots/index.ts | 5 +
.../src/components/affine/pivots/styles.ts | 117 +++++++++
.../web/src/components/affine/pivots/types.ts | 18 ++
.../invite-member-modal/index.tsx | 16 +-
.../page-list/OperationCell.tsx | 11 +
.../block-suite-page-list/page-list/index.tsx | 6 +-
.../header-right-items/EditorOptionMenu.tsx | 95 ++-----
.../pure/workspace-slider-bar/Pivots.tsx | 120 +++++++++
.../pure/workspace-slider-bar/index.tsx | 4 +-
.../pivot/OperationButton.tsx | 110 --------
.../pure/workspace-slider-bar/pivot/Pivot.tsx | 242 ------------------
.../pivot/TreeNodeRender.tsx | 51 ----
.../pure/workspace-slider-bar/pivot/index.tsx | 2 -
.../pure/workspace-slider-bar/pivot/styles.ts | 29 ---
.../pure/workspace-slider-bar/pivot/types.ts | 4 -
apps/web/src/hooks/use-page-meta.ts | 2 +
packages/component/src/ui/input/Input.tsx | 15 +-
packages/component/src/ui/input/style.ts | 21 +-
packages/component/src/ui/menu/MenuItem.tsx | 29 +--
packages/component/src/ui/menu/PureMenu.tsx | 7 +-
packages/component/src/ui/menu/styles.ts | 36 ++-
.../component/src/ui/tree-view/TreeNode.tsx | 142 ++++++----
.../component/src/ui/tree-view/TreeView.tsx | 105 +++++++-
packages/component/src/ui/tree-view/styles.ts | 4 +-
packages/component/src/ui/tree-view/types.ts | 85 +++---
packages/component/src/ui/tree-view/utils.ts | 18 ++
packages/i18n/src/resources/en.json | 5 +-
40 files changed, 1513 insertions(+), 665 deletions(-)
create mode 100644 apps/web/src/components/affine/operation-menu-items/CopyLink.tsx
create mode 100644 apps/web/src/components/affine/operation-menu-items/Export.tsx
create mode 100644 apps/web/src/components/affine/operation-menu-items/MoveTo.tsx
create mode 100644 apps/web/src/components/affine/operation-menu-items/MoveToTrash.tsx
create mode 100644 apps/web/src/components/affine/operation-menu-items/index.ts
create mode 100644 apps/web/src/components/affine/pivots/OperationButton.tsx
create mode 100644 apps/web/src/components/affine/pivots/OperationMenu.tsx
create mode 100644 apps/web/src/components/affine/pivots/PivotRender.tsx
create mode 100644 apps/web/src/components/affine/pivots/PivotsMenu/EmptyItem.tsx
create mode 100644 apps/web/src/components/affine/pivots/PivotsMenu/Pivots.tsx
create mode 100644 apps/web/src/components/affine/pivots/PivotsMenu/PivotsMenu.tsx
rename apps/web/src/components/{pure/workspace-slider-bar/pivot/utils.ts => affine/pivots/hooks/usePivotData.ts} (53%)
create mode 100644 apps/web/src/components/affine/pivots/hooks/usePivotHandler.ts
create mode 100644 apps/web/src/components/affine/pivots/index.ts
create mode 100644 apps/web/src/components/affine/pivots/styles.ts
create mode 100644 apps/web/src/components/affine/pivots/types.ts
create mode 100644 apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx
delete mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/OperationButton.tsx
delete mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/Pivot.tsx
delete mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/TreeNodeRender.tsx
delete mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/index.tsx
delete mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/styles.ts
delete mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/types.ts
create mode 100644 packages/component/src/ui/tree-view/utils.ts
diff --git a/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx b/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx
new file mode 100644
index 0000000000..b8008bb58f
--- /dev/null
+++ b/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx
@@ -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 (
+ <>
+
+ >
+ );
+};
diff --git a/apps/web/src/components/affine/operation-menu-items/Export.tsx b/apps/web/src/components/affine/operation-menu-items/Export.tsx
new file mode 100644
index 0000000000..6c2a876717
--- /dev/null
+++ b/apps/web/src/components/affine/operation-menu-items/Export.tsx
@@ -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 (
+
+ );
+};
diff --git a/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx b/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx
new file mode 100644
index 0000000000..deba4def80
--- /dev/null
+++ b/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx
@@ -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(null);
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = anchorEl !== null;
+ return (
+ <>
+
+ !meta.trash)}
+ currentMeta={currentMeta}
+ blockSuiteWorkspace={blockSuiteWorkspace}
+ />
+ >
+ );
+};
diff --git a/apps/web/src/components/affine/operation-menu-items/MoveToTrash.tsx b/apps/web/src/components/affine/operation-menu-items/MoveToTrash.tsx
new file mode 100644
index 0000000000..3ea05448fc
--- /dev/null
+++ b/apps/web/src/components/affine/operation-menu-items/MoveToTrash.tsx
@@ -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 (
+ <>
+
+ {
+ toast(t('Moved to Trash'));
+ setOpen(false);
+ setPageMeta(currentMeta.id, {
+ trash: true,
+ trashDate: +new Date(),
+ });
+ }}
+ onClose={() => {
+ setOpen(false);
+ }}
+ onCancel={() => {
+ setOpen(false);
+ }}
+ />
+ >
+ );
+};
diff --git a/apps/web/src/components/affine/operation-menu-items/index.ts b/apps/web/src/components/affine/operation-menu-items/index.ts
new file mode 100644
index 0000000000..612fefc606
--- /dev/null
+++ b/apps/web/src/components/affine/operation-menu-items/index.ts
@@ -0,0 +1,4 @@
+export * from './CopyLink';
+export * from './Export';
+export * from './MoveTo';
+export * from './MoveToTrash';
diff --git a/apps/web/src/components/affine/pivots/OperationButton.tsx b/apps/web/src/components/affine/pivots/OperationButton.tsx
new file mode 100644
index 0000000000..e949aafeb8
--- /dev/null
+++ b/apps/web/src/components/affine/pivots/OperationButton.tsx
@@ -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);
+ const [operationOpen, setOperationOpen] = useState(false);
+ const [pivotsMenuOpen, setPivotsMenuOpen] = useState(false);
+
+ const menuIndex = useMemo(() => modalIndex + 1, [modalIndex]);
+
+ return (
+ {
+ setOperationOpen(false);
+ setPivotsMenuOpen(false);
+ }}
+ >
+ {
+ e.stopPropagation();
+ }}
+ onMouseLeave={() => {
+ setOperationOpen(false);
+ setPivotsMenuOpen(false);
+ }}
+ >
+
setAnchorEl(ref)}
+ size="small"
+ onClick={() => {
+ setOperationOpen(!operationOpen);
+ }}
+ visible={isHover}
+ >
+
+
+
{
+ switch (type) {
+ case 'add':
+ onAdd();
+ break;
+ case 'move':
+ setPivotsMenuOpen(true);
+ break;
+ case 'delete':
+ onDelete();
+ break;
+ }
+ setOperationOpen(false);
+ onMenuClose?.();
+ }}
+ currentMeta={currentMeta}
+ blockSuiteWorkspace={blockSuiteWorkspace}
+ />
+
+
+
+
+ );
+};
diff --git a/apps/web/src/components/affine/pivots/OperationMenu.tsx b/apps/web/src/components/affine/pivots/OperationMenu.tsx
new file mode 100644
index 0000000000..137d75fedf
--- /dev/null
+++ b/apps/web/src/components/affine/pivots/OperationMenu.tsx
@@ -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: ,
+ type: 'add',
+ },
+ {
+ label: 'Move to',
+ icon: ,
+ type: 'move',
+ },
+ {
+ label: 'Rename',
+ icon: ,
+ type: 'rename',
+ disabled: true,
+ },
+];
+
+export const OperationMenu = ({
+ onSelect,
+ blockSuiteWorkspace,
+ currentMeta,
+ ...menuProps
+}: OperationMenuProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+ {menuItems.map((item, index) => {
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/apps/web/src/components/affine/pivots/PivotRender.tsx b/apps/web/src/components/affine/pivots/PivotRender.tsx
new file mode 100644
index 0000000000..8073ccdd03
--- /dev/null
+++ b/apps/web/src/components/affine/pivots/PivotRender.tsx
@@ -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 (
+ {
+ onClick?.(e, node);
+ }}
+ onMouseEnter={() => setIsHover(true)}
+ onMouseLeave={() => setIsHover(false)}
+ isOver={isOver || isSelected}
+ active={active}
+ >
+ {
+ e.stopPropagation();
+ setCollapsed(node.id, !collapsed);
+ }}
+ >
+
+
+ {record[node.id] === 'edgeless' ? : }
+ {currentMeta?.title || 'Untitled'}
+ {showOperationButton && (
+ setIsHover(false)}
+ />
+ )}
+
+ );
+};
diff --git a/apps/web/src/components/affine/pivots/PivotsMenu/EmptyItem.tsx b/apps/web/src/components/affine/pivots/PivotsMenu/EmptyItem.tsx
new file mode 100644
index 0000000000..a53472c995
--- /dev/null
+++ b/apps/web/src/components/affine/pivots/PivotsMenu/EmptyItem.tsx
@@ -0,0 +1,10 @@
+import { useTranslation } from '@affine/i18n';
+
+import { StyledPivot } from '../styles';
+
+export const EmptyItem = () => {
+ const { t } = useTranslation();
+ return {t('No item')};
+};
+
+export default EmptyItem;
diff --git a/apps/web/src/components/affine/pivots/PivotsMenu/Pivots.tsx b/apps/web/src/components/affine/pivots/PivotsMenu/Pivots.tsx
new file mode 100644
index 0000000000..9ac93ba7ab
--- /dev/null
+++ b/apps/web/src/components/affine/pivots/PivotsMenu/Pivots.tsx
@@ -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) => {
+ 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 (
+ <>
+ {
+ setPageMeta(currentMeta.id, { isPivots: true });
+ }}
+ >
+ ) => {
+ e.stopPropagation();
+ setShowPivot(!showPivot);
+ },
+ [showPivot]
+ )}
+ collapse={showPivot}
+ >
+
+
+
+ {t('Pivots')}
+
+
+
+ {isPivotEmpty ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
+export default Pivots;
diff --git a/apps/web/src/components/affine/pivots/PivotsMenu/PivotsMenu.tsx b/apps/web/src/components/affine/pivots/PivotsMenu/PivotsMenu.tsx
new file mode 100644
index 0000000000..4153a47ad0
--- /dev/null
+++ b/apps/web/src/components/affine/pivots/PivotsMenu/PivotsMenu.tsx
@@ -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 (
+
+
+
+ e.stopPropagation()}
+ />
+
+
+
+ {isSearching && (
+ <>
+
+ {searchResult.length
+ ? t('Find results', { number: searchResult.length })
+ : t('Find 0 result')}
+
+ {searchResult.map(meta => {
+ return {meta.title};
+ })}
+ >
+ )}
+
+ {!isSearching && (
+ <>
+ Suggested
+
+ >
+ )}
+
+
+ {showRemovePivots && (
+
+ {
+ 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 });
+ }}
+ >
+
+ {t('Remove from Pivots')}
+
+ {t('RFP')}
+
+ )}
+
+ );
+};
diff --git a/apps/web/src/components/pure/workspace-slider-bar/pivot/utils.ts b/apps/web/src/components/affine/pivots/hooks/usePivotData.ts
similarity index 53%
rename from apps/web/src/components/pure/workspace-slider-bar/pivot/utils.ts
rename to apps/web/src/components/affine/pivots/hooks/usePivotData.ts
index be785620f8..01d2fea1aa 100644
--- a/apps/web/src/components/pure/workspace-slider-bar/pivot/utils.ts
+++ b/apps/web/src/components/affine/pivots/hooks/usePivotData.ts
@@ -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((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;
diff --git a/apps/web/src/components/affine/pivots/hooks/usePivotHandler.ts b/apps/web/src/components/affine/pivots/hooks/usePivotHandler.ts
new file mode 100644
index 0000000000..e60220856e
--- /dev/null
+++ b/apps/web/src/components/affine/pivots/hooks/usePivotHandler.ts
@@ -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['onDelete'];
+ onDrop?: TreeViewProps['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;
diff --git a/apps/web/src/components/affine/pivots/index.ts b/apps/web/src/components/affine/pivots/index.ts
new file mode 100644
index 0000000000..eb887647ac
--- /dev/null
+++ b/apps/web/src/components/affine/pivots/index.ts
@@ -0,0 +1,5 @@
+export * from './hooks/usePivotData';
+export * from './hooks/usePivotHandler';
+export * from './PivotRender';
+export * from './PivotsMenu/PivotsMenu';
+export * from './types';
diff --git a/apps/web/src/components/affine/pivots/styles.ts b/apps/web/src/components/affine/pivots/styles.ts
new file mode 100644
index 0000000000..c824a2ac2f
--- /dev/null
+++ b/apps/web/src/components/affine/pivots/styles.ts
@@ -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',
+ },
+ };
+});
diff --git a/apps/web/src/components/affine/pivots/types.ts b/apps/web/src/components/affine/pivots/types.ts
new file mode 100644
index 0000000000..388b1064a4
--- /dev/null
+++ b/apps/web/src/components/affine/pivots/types.ts
@@ -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, node: TreeNode) => void;
+ showOperationButton?: boolean;
+};
+
+export type NodeRenderProps = RenderProps & {
+ metas: PageMeta[];
+ currentMeta: PageMeta;
+};
+
+export type TreeNode = Node;
diff --git a/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx
index 566b25eef4..4c39c1a646 100644
--- a/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx
+++ b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx
@@ -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')}
- >
+ />
{showMemberPreview && gmailReg.test(email) && (
diff --git a/apps/web/src/components/blocksuite/block-suite-page-list/page-list/OperationCell.tsx b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/OperationCell.tsx
index e5abfd7029..0fec338b62 100644
--- a/apps/web/src/components/blocksuite/block-suite-page-list/page-list/OperationCell.tsx
+++ b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/OperationCell.tsx
@@ -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 = ({
pageMeta,
+ metas,
+ blockSuiteWorkspace,
onOpenPageInNewTab,
onToggleFavoritePage,
onToggleTrashPage,
@@ -59,6 +65,11 @@ export const OperationCell: React.FC = ({
>
{t('Open in new tab')}
+
: }
- 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')}
-
-
+
+
+
>
);
@@ -141,26 +108,6 @@ export const EditorOptionMenu = () => {
- {
- toast(t('Moved to Trash'));
- setOpen(false);
- setPageMeta(pageId, { trash: !trash, trashDate: +new Date() });
- }}
- onClose={() => {
- setOpen(false);
- }}
- onCancel={() => {
- setOpen(false);
- }}
- />
>
);
};
diff --git a/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx b/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx
new file mode 100644
index 0000000000..07a020e35e
--- /dev/null
+++ b/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx
@@ -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, 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 (
+
+ );
+};
+
+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 (
+ <>
+
+ {
+ setShowPivot(!showPivot);
+ }, [showPivot])}
+ collapse={showPivot}
+ >
+
+
+
+ {t('Pivots')}
+
+
+
+ {isPivotEmpty ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
+export default Pivots;
diff --git a/apps/web/src/components/pure/workspace-slider-bar/index.tsx b/apps/web/src/components/pure/workspace-slider-bar/index.tsx
index a337fd9c95..c842d5f45f 100644
--- a/apps/web/src/components/pure/workspace-slider-bar/index.tsx
+++ b/apps/web/src/components/pure/workspace-slider-bar/index.tsx
@@ -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 = ({
currentWorkspace={currentWorkspace}
/>
{config.enableSubpage && !!currentWorkspace && (
- void;
- onDelete: () => void;
-}) => {
- const { t } = useTranslation();
- const router = useRouter();
-
- const [anchorEl, setAnchorEl] = useState(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 (
- {
- setOpen(false);
- }}
- >
- {
- e.stopPropagation();
- }}
- onMouseLeave={() => {
- setOpen(false);
- }}
- >
-
setAnchorEl(ref)}
- size="small"
- className="operation-button"
- onClick={event => {
- event.stopPropagation();
- setOpen(!open);
- }}
- >
-
-
-
- }
- onClick={() => {
- onAdd();
- setOpen(false);
- }}
- >
- {t('Add a subpage inside')}
-
- } disabled={true}>
- {t('Move to')}
-
- } disabled={true}>
- {t('Rename')}
-
- }
- onClick={() => {
- onDelete();
- setOpen(false);
- }}
- >
- {t('Move to Trash')}
-
- }
- disabled={true}
- // onClick={() => {
- // const workspaceId = router.query.workspaceId;
- // navigator.clipboard.writeText(window.location.href);
- // toast(t('Copied link to clipboard'));
- // }}
- >
- {t('Copy Link')}
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/pure/workspace-slider-bar/pivot/Pivot.tsx b/apps/web/src/components/pure/workspace-slider-bar/pivot/Pivot.tsx
deleted file mode 100644
index d40b92f5a5..0000000000
--- a/apps/web/src/components/pure/workspace-slider-bar/pivot/Pivot.tsx
+++ /dev/null
@@ -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 (
-
- );
-};
-
-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 (
- <>
-
- {
- setShowPivot(!showPivot);
- }, [showPivot])}
- collapse={showPivot}
- >
-
-
-
- {t('Pivots')}
-
-
-
- {isPivotEmpty ? (
-
- ) : (
-
- )}
-
- >
- );
-};
-export default Pivot;
diff --git a/apps/web/src/components/pure/workspace-slider-bar/pivot/TreeNodeRender.tsx b/apps/web/src/components/pure/workspace-slider-bar/pivot/TreeNodeRender.tsx
deleted file mode 100644
index 68a1271241..0000000000
--- a/apps/web/src/components/pure/workspace-slider-bar/pivot/TreeNodeRender.tsx
+++ /dev/null
@@ -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 (
- {
- if (active) {
- return;
- }
- openPage(node.id);
- }}
- isOver={isOver}
- active={active}
- >
- {
- e.stopPropagation();
- setCollapsed(!collapsed);
- }}
- >
-
-
- {record[pageMeta.id] === 'edgeless' ? : }
- {node.title || 'Untitled'}
-
-
- );
-};
diff --git a/apps/web/src/components/pure/workspace-slider-bar/pivot/index.tsx b/apps/web/src/components/pure/workspace-slider-bar/pivot/index.tsx
deleted file mode 100644
index 3e9528faf1..0000000000
--- a/apps/web/src/components/pure/workspace-slider-bar/pivot/index.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './Pivot';
-export * from './types';
diff --git a/apps/web/src/components/pure/workspace-slider-bar/pivot/styles.ts b/apps/web/src/components/pure/workspace-slider-bar/pivot/styles.ts
deleted file mode 100644
index cbaabc9cba..0000000000
--- a/apps/web/src/components/pure/workspace-slider-bar/pivot/styles.ts
+++ /dev/null
@@ -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',
- };
-});
diff --git a/apps/web/src/components/pure/workspace-slider-bar/pivot/types.ts b/apps/web/src/components/pure/workspace-slider-bar/pivot/types.ts
deleted file mode 100644
index 8166153f30..0000000000
--- a/apps/web/src/components/pure/workspace-slider-bar/pivot/types.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import type { Node } from '@affine/component';
-import type { PageMeta } from '@blocksuite/store';
-
-export type TreeNode = Node;
diff --git a/apps/web/src/hooks/use-page-meta.ts b/apps/web/src/hooks/use-page-meta.ts
index 211767f600..2172f49fb8 100644
--- a/apps/web/src/hooks/use-page-meta.ts
+++ b/apps/web/src/hooks/use-page-meta.ts
@@ -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;
}
}
diff --git a/packages/component/src/ui/input/Input.tsx b/packages/component/src/ui/input/Input.tsx
index 99376c2726..43e8e2d15e 100644
--- a/packages/component/src/ui/input/Input.tsx
+++ b/packages/component/src/ui/input/Input.tsx
@@ -1,12 +1,12 @@
import type {
+ CSSProperties,
FocusEventHandler,
ForwardedRef,
HTMLAttributes,
InputHTMLAttributes,
KeyboardEventHandler,
} from 'react';
-import { forwardRef } from 'react';
-import { useEffect, useState } from 'react';
+import { forwardRef, useEffect, useState } from 'react';
import { StyledInput } from './style';
@@ -14,13 +14,14 @@ type inputProps = {
value?: string;
placeholder?: string;
disabled?: boolean;
- width?: number;
- height?: number;
+ width?: CSSProperties['width'];
+ height?: CSSProperties['height'];
maxLength?: number;
minLength?: number;
onChange?: (value: string) => void;
onBlur?: FocusEventHandler;
onKeyDown?: KeyboardEventHandler;
+ noBorder?: boolean;
} & Omit, 'onChange'>;
export const Input = forwardRef(function Input(
@@ -31,10 +32,11 @@ export const Input = forwardRef(function Input(
maxLength,
minLength,
height,
- width = 260,
+ width,
onChange,
onBlur,
onKeyDown,
+ noBorder = false,
...otherProps
}: inputProps,
ref: ForwardedRef
@@ -69,7 +71,8 @@ export const Input = forwardRef(function Input(
onBlur={handleBlur}
onKeyDown={handleKeyDown}
height={height}
+ noBorder={noBorder}
{...otherProps}
- >
+ />
);
});
diff --git a/packages/component/src/ui/input/style.ts b/packages/component/src/ui/input/style.ts
index eb5d4b663d..11f9d92be9 100644
--- a/packages/component/src/ui/input/style.ts
+++ b/packages/component/src/ui/input/style.ts
@@ -1,28 +1,25 @@
+import type { CSSProperties } from 'react';
+
import { styled } from '../../styles';
export const StyledInput = styled('input')<{
disabled?: boolean;
value?: string;
- width: number;
- height?: number;
-}>(({ theme, width, disabled, height }) => {
- const fontWeight = 400;
- const fontSize = '16px';
+ width?: CSSProperties['width'];
+ height?: CSSProperties['height'];
+ noBorder?: boolean;
+}>(({ theme, width, disabled, height, noBorder }) => {
return {
- width: `${width}px`,
+ width: width || '100%',
+ height,
lineHeight: '22px',
padding: '8px 12px',
- fontWeight,
- fontSize,
- height: height ? `${height}px` : 'auto',
color: disabled ? theme.colors.disableColor : theme.colors.textColor,
- border: `1px solid`,
+ border: noBorder ? 'unset' : `1px solid`,
borderColor: theme.colors.borderColor, // TODO: check out disableColor,
backgroundColor: theme.colors.popoverBackground,
borderRadius: '10px',
'&::placeholder': {
- fontWeight,
- fontSize,
color: theme.colors.placeHolderColor,
},
'&:focus': {
diff --git a/packages/component/src/ui/menu/MenuItem.tsx b/packages/component/src/ui/menu/MenuItem.tsx
index b23bf60481..6a73686ad0 100644
--- a/packages/component/src/ui/menu/MenuItem.tsx
+++ b/packages/component/src/ui/menu/MenuItem.tsx
@@ -1,31 +1,28 @@
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
-import { cloneElement, forwardRef } from 'react';
+import { forwardRef } from 'react';
+
+import {
+ StyledContent,
+ StyledEndIconWrapper,
+ StyledMenuItem,
+ StyledStartIconWrapper,
+} from './styles';
-import { StyledArrow, StyledMenuItem } from './styles';
export type IconMenuProps = PropsWithChildren<{
- isDir?: boolean;
icon?: ReactElement;
+ endIcon?: ReactElement;
iconSize?: [number, number];
disabled?: boolean;
}> &
HTMLAttributes;
export const MenuItem = forwardRef(
- ({ isDir = false, icon, iconSize, children, ...props }, ref) => {
- const [iconWidth, iconHeight] = iconSize || [20, 20];
+ ({ endIcon, icon, iconSize, children, ...props }, ref) => {
return (
- {icon &&
- cloneElement(icon, {
- width: iconWidth,
- height: iconHeight,
- style: {
- marginRight: 12,
- ...icon.props?.style,
- },
- })}
- {children}
- {isDir ? : null}
+ {icon && {icon}}
+ {children}
+ {endIcon && {endIcon}}
);
}
diff --git a/packages/component/src/ui/menu/PureMenu.tsx b/packages/component/src/ui/menu/PureMenu.tsx
index 1f21aa932a..b72ca03152 100644
--- a/packages/component/src/ui/menu/PureMenu.tsx
+++ b/packages/component/src/ui/menu/PureMenu.tsx
@@ -4,12 +4,17 @@ import type { PurePopperProps } from '../popper';
import { PurePopper } from '../popper';
import { StyledMenuWrapper } from './styles';
+export type PureMenuProps = PurePopperProps & {
+ width?: CSSProperties['width'];
+ height?: CSSProperties['height'];
+};
export const PureMenu = ({
children,
placement,
width,
+ height,
...otherProps
-}: PurePopperProps & { width?: CSSProperties['width'] }) => {
+}: PureMenuProps) => {
return (
diff --git a/packages/component/src/ui/menu/styles.ts b/packages/component/src/ui/menu/styles.ts
index 48e6336e0f..b8ee3cddc7 100644
--- a/packages/component/src/ui/menu/styles.ts
+++ b/packages/component/src/ui/menu/styles.ts
@@ -1,14 +1,15 @@
-import { ArrowRightSmallIcon } from '@blocksuite/icons';
import type { CSSProperties } from 'react';
-import { displayFlex, styled } from '../../styles';
+import { displayFlex, styled, textEllipsis } from '../../styles';
import StyledPopperContainer from '../shared/Container';
export const StyledMenuWrapper = styled(StyledPopperContainer)<{
width?: CSSProperties['width'];
-}>(({ theme, width }) => {
+ height?: CSSProperties['height'];
+}>(({ theme, width, height }) => {
return {
width,
+ height,
background: theme.colors.popoverBackground,
padding: '8px 4px',
fontSize: '14px',
@@ -17,13 +18,28 @@ export const StyledMenuWrapper = styled(StyledPopperContainer)<{
};
});
-export const StyledArrow = styled(ArrowRightSmallIcon)({
- position: 'absolute',
- right: '12px',
- top: 0,
- bottom: 0,
- margin: 'auto',
- fontSize: '20px',
+export const StyledStartIconWrapper = styled('div')(({ theme }) => {
+ return {
+ marginRight: '12px',
+ fontSize: '20px',
+ color: theme.colors.iconColor,
+ };
+});
+export const StyledEndIconWrapper = styled('div')(({ theme }) => {
+ return {
+ marginLeft: '12px',
+ fontSize: '20px',
+ color: theme.colors.iconColor,
+ };
+});
+
+export const StyledContent = styled('div')(({ theme }) => {
+ return {
+ textAlign: 'left',
+ flexGrow: 1,
+ fontSize: theme.font.base,
+ ...textEllipsis(1),
+ };
});
export const StyledMenuItem = styled('button')<{
diff --git a/packages/component/src/ui/tree-view/TreeNode.tsx b/packages/component/src/ui/tree-view/TreeNode.tsx
index 1355acf582..e37a6b6334 100644
--- a/packages/component/src/ui/tree-view/TreeNode.tsx
+++ b/packages/component/src/ui/tree-view/TreeNode.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useEffect } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import {
@@ -14,21 +14,21 @@ import type {
TreeNodeProps,
} from './types';
-const NodeLine = ({
+const NodeLine = ({
node,
onDrop,
allowDrop = true,
isTop = false,
-}: NodeLIneProps) => {
+}: NodeLIneProps) => {
const [{ isOver }, drop] = useDrop(
() => ({
accept: 'node',
- drop: (item: Node, monitor) => {
+ drop: (item: Node, monitor) => {
const didDrop = monitor.didDrop();
if (didDrop) {
return;
}
- onDrop?.(item, node, {
+ onDrop?.(item.id, node.id, {
internal: false,
topLine: isTop,
bottomLine: !isTop,
@@ -44,24 +44,23 @@ const NodeLine = ({
return ;
};
-const TreeNodeItem = ({
+const TreeNodeItemWithDnd = ({
node,
allowDrop,
- collapsed,
setCollapsed,
...otherProps
-}: TreeNodeItemProps) => {
+}: TreeNodeItemProps) => {
const { onAdd, onDelete, onDrop } = otherProps;
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept: 'node',
- drop: (item: Node, monitor) => {
+ drop: (item: Node, monitor) => {
const didDrop = monitor.didDrop();
if (didDrop || item.id === node.id || !allowDrop) {
return;
}
- onDrop?.(item, node, {
+ onDrop?.(item.id, node.id, {
internal: true,
topLine: false,
bottomLine: false,
@@ -77,44 +76,79 @@ const TreeNodeItem = ({
useEffect(() => {
if (isOver && canDrop) {
- setCollapsed(false);
+ setCollapsed(node.id, false);
}
}, [isOver, canDrop]);
return (
-
+
+ );
+};
+
+const TreeNodeItem =
({
+ node,
+ collapsed,
+ setCollapsed,
+ selectedId,
+ isOver = false,
+ canDrop = false,
+ onAdd,
+ onDelete,
+ dropRef,
+}: TreeNodeItemProps) => {
+ return (
+
{node.render?.(node, {
- isOver: !!(isOver && canDrop),
+ isOver: isOver && canDrop,
onAdd: () => onAdd?.(node),
onDelete: () => onDelete?.(node),
collapsed,
setCollapsed,
+ isSelected: selectedId === node.id,
})}
);
};
-export const TreeNode = ({
- node,
- index,
- allowDrop = true,
- ...otherProps
-}: TreeNodeProps) => {
- const { indent } = otherProps;
- const [collapsed, setCollapsed] = useState(false);
-
+export const TreeNodeWithDnd = (
+ props: TreeNodeProps
+) => {
const [{ isDragging }, drag] = useDrag(() => ({
type: 'node',
- item: node,
+ item: props.node,
collect: monitor => ({
isDragging: monitor.isDragging(),
}),
}));
+ return ;
+};
+
+export const TreeNode = ({
+ node,
+ index,
+ isDragging = false,
+ allowDrop = true,
+ dragRef,
+ ...otherProps
+}: TreeNodeProps) => {
+ const { indent, enableDnd, collapsedIds } = otherProps;
+ const collapsed = collapsedIds.includes(node.id);
+
return (
-
+
- {index === 0 && (
+ {enableDnd && index === 0 && (
({
isTop={true}
/>
)}
-
- {(!node.children?.length || collapsed) && (
+ {enableDnd ? (
+
+ ) : (
+
+ )}
+
+ {enableDnd && (!node.children?.length || collapsed) && (
({
{node.children &&
- node.children.map((childNode, index) => (
-
- ))}
+ node.children.map((childNode, index) =>
+ enableDnd ? (
+
+ ) : (
+
+ )
+ )}
);
};
-
-export default TreeNode;
diff --git a/packages/component/src/ui/tree-view/TreeView.tsx b/packages/component/src/ui/tree-view/TreeView.tsx
index 1d72433780..61c87c780e 100644
--- a/packages/component/src/ui/tree-view/TreeView.tsx
+++ b/packages/component/src/ui/tree-view/TreeView.tsx
@@ -1,15 +1,108 @@
+import { useEffect, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
-import { TreeNode } from './TreeNode';
-import type { TreeViewProps } from './types';
-export const TreeView = ({ data, ...otherProps }: TreeViewProps) => {
+import { TreeNode, TreeNodeWithDnd } from './TreeNode';
+import type { TreeNodeProps, TreeViewProps } from './types';
+import { flattenIds } from './utils';
+
+export const TreeView = ({
+ data,
+ enableKeyboardSelection,
+ onSelect,
+ enableDnd = true,
+ initialCollapsedIds = [],
+ ...otherProps
+}: TreeViewProps) => {
+ const [selectedId, setSelectedId] = useState();
+ // TODO: should record collapsedIds in localStorage
+ const [collapsedIds, setCollapsedIds] =
+ useState(initialCollapsedIds);
+
+ useEffect(() => {
+ if (!enableKeyboardSelection) {
+ return;
+ }
+
+ const flattenedIds = flattenIds(data);
+
+ const handleDirectionKeyDown = (e: KeyboardEvent) => {
+ if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') {
+ return;
+ }
+ if (selectedId === undefined) {
+ setSelectedId(flattenedIds[0]);
+ return;
+ }
+ let selectedIndex = flattenedIds.indexOf(selectedId);
+ if (e.key === 'ArrowDown') {
+ selectedIndex < flattenedIds.length - 1 && selectedIndex++;
+ }
+ if (e.key === 'ArrowUp') {
+ selectedIndex > 0 && selectedIndex--;
+ }
+
+ setSelectedId(flattenedIds[selectedIndex]);
+ };
+
+ const handleEnterKeyDown = (e: KeyboardEvent) => {
+ if (e.key !== 'Enter') {
+ return;
+ }
+ selectedId && onSelect?.(selectedId);
+ };
+
+ document.addEventListener('keydown', handleDirectionKeyDown);
+ document.addEventListener('keydown', handleEnterKeyDown);
+
+ return () => {
+ document.removeEventListener('keydown', handleDirectionKeyDown);
+ document.removeEventListener('keydown', handleEnterKeyDown);
+ };
+ }, [data, selectedId]);
+
+ const setCollapsed: TreeNodeProps['setCollapsed'] = (id, collapsed) => {
+ if (collapsed) {
+ setCollapsedIds(ids => [...ids, id]);
+ } else {
+ setCollapsedIds(ids => ids.filter(i => i !== id));
+ }
+ };
+
+ if (enableDnd) {
+ return (
+
+ {data.map((node, index) => (
+
+ ))}
+
+ );
+ }
+
return (
-
+ <>
{data.map((node, index) => (
-
+
))}
-
+ >
);
};
diff --git a/packages/component/src/ui/tree-view/styles.ts b/packages/component/src/ui/tree-view/styles.ts
index 5255543a95..9b5f6305d6 100644
--- a/packages/component/src/ui/tree-view/styles.ts
+++ b/packages/component/src/ui/tree-view/styles.ts
@@ -15,8 +15,8 @@ export const StyledTreeNodeWrapper = styled('div')(() => {
position: 'relative',
};
});
-export const StyledTreeNodeContainer = styled('div')<{ isDragging: boolean }>(
- ({ isDragging, theme }) => {
+export const StyledTreeNodeContainer = styled('div')<{ isDragging?: boolean }>(
+ ({ isDragging = false, theme }) => {
return {
background: isDragging ? theme.colors.hoverBackground : '',
// opacity: isDragging ? 0.4 : 1,
diff --git a/packages/component/src/ui/tree-view/types.ts b/packages/component/src/ui/tree-view/types.ts
index 494b481db3..8b1af1687c 100644
--- a/packages/component/src/ui/tree-view/types.ts
+++ b/packages/component/src/ui/tree-view/types.ts
@@ -1,52 +1,71 @@
-import type { CSSProperties, ReactNode } from 'react';
+import type { CSSProperties, ReactNode, Ref } from 'react';
-export type Node = {
+export type DropPosition = {
+ topLine: boolean;
+ bottomLine: boolean;
+ internal: boolean;
+};
+export type OnDrop = (
+ dragId: string,
+ dropId: string,
+ position: DropPosition
+) => void;
+
+export type Node = {
id: string;
- children?: Node[];
- render?: (
- node: Node,
+ children?: Node[];
+ render: (
+ node: Node,
eventsAndStatus: {
isOver: boolean;
onAdd: () => void;
onDelete: () => void;
collapsed: boolean;
- setCollapsed: (collapsed: boolean) => void;
+ setCollapsed: (id: string, collapsed: boolean) => void;
+ isSelected: boolean;
},
- extendProps?: unknown
+ renderProps?: RenderProps
) => ReactNode;
-} & N;
-
-type CommonProps = {
- indent?: CSSProperties['paddingLeft'];
- onAdd?: (node: Node) => void;
- onDelete?: (node: Node) => void;
- onDrop?: (
- dragNode: Node,
- dropNode: Node,
- position: {
- topLine: boolean;
- bottomLine: boolean;
- internal: boolean;
- }
- ) => void;
};
-export type TreeNodeProps = {
- node: Node;
+type CommonProps = {
+ enableDnd?: boolean;
+ enableKeyboardSelection?: boolean;
+ indent?: CSSProperties['paddingLeft'];
+ onAdd?: (node: Node) => void;
+ onDelete?: (node: Node) => void;
+ onDrop?: OnDrop;
+ // Only trigger when the enableKeyboardSelection is true
+ onSelect?: (id: string) => void;
+};
+
+export type TreeNodeProps = {
+ node: Node;
index: number;
+ collapsedIds: string[];
+ setCollapsed: (id: string, collapsed: boolean) => void;
allowDrop?: boolean;
-} & CommonProps;
+ selectedId?: string;
+ isDragging?: boolean;
+ dragRef?: Ref;
+} & CommonProps;
-export type TreeNodeItemProps = {
+export type TreeNodeItemProps = {
collapsed: boolean;
- setCollapsed: (collapsed: boolean) => void;
-} & TreeNodeProps;
+ setCollapsed: (id: string, collapsed: boolean) => void;
-export type TreeViewProps = {
- data: Node[];
-} & CommonProps;
+ isOver?: boolean;
+ canDrop?: boolean;
-export type NodeLIneProps = {
+ dropRef?: Ref;
+} & TreeNodeProps;
+
+export type TreeViewProps = {
+ data: Node[];
+ initialCollapsedIds?: string[];
+} & CommonProps;
+
+export type NodeLIneProps = {
allowDrop: boolean;
isTop?: boolean;
-} & Pick, 'node' | 'onDrop'>;
+} & Pick, 'node' | 'onDrop'>;
diff --git a/packages/component/src/ui/tree-view/utils.ts b/packages/component/src/ui/tree-view/utils.ts
new file mode 100644
index 0000000000..869ad1aea3
--- /dev/null
+++ b/packages/component/src/ui/tree-view/utils.ts
@@ -0,0 +1,18 @@
+import type { Node } from '@affine/component';
+
+export function flattenIds(arr: Node[]): string[] {
+ const result: string[] = [];
+
+ function flatten(arr: Node[]) {
+ for (let i = 0, len = arr.length; i < len; i++) {
+ const item = arr[i];
+ result.push(item.id);
+ if (Array.isArray(item.children)) {
+ flatten(item.children);
+ }
+ }
+ }
+
+ flatten(arr);
+ return result;
+}
diff --git a/packages/i18n/src/resources/en.json b/packages/i18n/src/resources/en.json
index c904129e23..534e33d59a 100644
--- a/packages/i18n/src/resources/en.json
+++ b/packages/i18n/src/resources/en.json
@@ -197,5 +197,8 @@
"Pivots": "Pivots",
"Add a subpage inside": "Add a subpage inside",
"Rename": "Rename",
- "Move to": "Move to"
+ "Move to": "Move to",
+ "Move page to...": "Move page to...",
+ "Remove from Pivots": "Remove from Pivots",
+ "RFP": "Pages can be freely added/removed from pivots, remaining accessible from \"All Pages\"."
}