From 6a7b5601aa67e08651bd7c2d5a119138ef1d47b8 Mon Sep 17 00:00:00 2001 From: Qi <474021214@qq.com> Date: Thu, 23 Mar 2023 13:47:07 +0800 Subject: [PATCH] feat: support subpage (#1663) --- apps/desktop/package.json | 6 +- apps/web/.env.local.template | 1 + apps/web/package.json | 6 +- apps/web/preset.config.mjs | 1 + .../block-suite-page-list/page-list/index.tsx | 35 ++- .../pure/workspace-slider-bar/index.tsx | 11 + .../pure/workspace-slider-bar/pivot/Pivot.tsx | 232 ++++++++++++++++++ .../pivot/TreeNodeRender.tsx | 69 ++++++ .../pure/workspace-slider-bar/pivot/index.tsx | 2 + .../pure/workspace-slider-bar/pivot/styles.ts | 47 ++++ .../pure/workspace-slider-bar/pivot/types.ts | 4 + .../pure/workspace-slider-bar/pivot/utils.ts | 43 ++++ .../hooks/use-blocksuite-workspace-helper.ts | 4 +- apps/web/src/hooks/use-feature-flag.ts | 1 - apps/web/src/hooks/use-page-meta.ts | 7 + packages/component/package.json | 11 +- packages/component/src/index.ts | 1 + .../component/src/ui/tree-view/TreeNode.tsx | 128 ++++++++++ .../component/src/ui/tree-view/TreeView.tsx | 16 ++ packages/component/src/ui/tree-view/index.ts | 3 + packages/component/src/ui/tree-view/styles.ts | 40 +++ packages/component/src/ui/tree-view/types.ts | 45 ++++ packages/data-center/package.json | 4 +- packages/env/package.json | 2 +- packages/env/src/index.ts | 1 + yarn.lock | 186 +++++++++----- 26 files changed, 824 insertions(+), 82 deletions(-) create mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/Pivot.tsx create mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/TreeNodeRender.tsx create mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/index.tsx create mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/styles.ts create mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/types.ts create mode 100644 apps/web/src/components/pure/workspace-slider-bar/pivot/utils.ts create mode 100644 packages/component/src/ui/tree-view/TreeNode.tsx create mode 100644 packages/component/src/ui/tree-view/TreeView.tsx create mode 100644 packages/component/src/ui/tree-view/index.ts create mode 100644 packages/component/src/ui/tree-view/styles.ts create mode 100644 packages/component/src/ui/tree-view/types.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 6080c26457..33d5a9143b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -14,10 +14,10 @@ "build:app": "tauri build" }, "dependencies": { - "@blocksuite/blocks": "0.5.0-20230320164115-e612d17", - "@blocksuite/editor": "0.5.0-20230320164115-e612d17", + "@blocksuite/blocks": "0.5.0-20230323032255-e75ed32", + "@blocksuite/editor": "0.5.0-20230323032255-e75ed32", "@blocksuite/icons": "2.0.23", - "@blocksuite/store": "0.5.0-20230320164115-e612d17", + "@blocksuite/store": "0.5.0-20230323032255-e75ed32", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@tauri-apps/api": "^1.2.0", diff --git a/apps/web/.env.local.template b/apps/web/.env.local.template index dc4f6101c2..24c1918690 100644 --- a/apps/web/.env.local.template +++ b/apps/web/.env.local.template @@ -15,3 +15,4 @@ PREFETCH_WORKSPACE=1 ENABLE_BC_PROVIDER=1 EXPOSE_INTERNAL=1 ENABLE_DEBUG_PAGE= +ENABLE_SUBPAGE= diff --git a/apps/web/package.json b/apps/web/package.json index 20ded3eae0..4c994e493b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,10 +15,10 @@ "@affine/env": "workspace:*", "@affine/i18n": "workspace:*", "@affine/templates": "workspace:*", - "@blocksuite/blocks": "0.5.0-20230320164115-e612d17", - "@blocksuite/editor": "0.5.0-20230320164115-e612d17", + "@blocksuite/blocks": "0.5.0-20230323032255-e75ed32", + "@blocksuite/editor": "0.5.0-20230323032255-e75ed32", "@blocksuite/icons": "2.0.23", - "@blocksuite/store": "0.5.0-20230320164115-e612d17", + "@blocksuite/store": "0.5.0-20230323032255-e75ed32", "@emotion/cache": "^11.10.5", "@emotion/react": "^11.10.6", "@emotion/server": "^11.10.0", diff --git a/apps/web/preset.config.mjs b/apps/web/preset.config.mjs index ec56a0369f..a2203168e8 100644 --- a/apps/web/preset.config.mjs +++ b/apps/web/preset.config.mjs @@ -10,5 +10,6 @@ const config = { enableDebugPage: Boolean( process.env.ENABLE_DEBUG_PAGE ?? process.env.NODE_ENV === 'development' ), + enableSubpage: Boolean(process.env.ENABLE_SUBPAGE), }; export default config; diff --git a/apps/web/src/components/blocksuite/block-suite-page-list/page-list/index.tsx b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/index.tsx index f377c84557..a06a4d55d7 100644 --- a/apps/web/src/components/blocksuite/block-suite-page-list/page-list/index.tsx +++ b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/index.tsx @@ -21,7 +21,7 @@ import { } from '@mui/material'; import { useAtomValue } from 'jotai'; import type React from 'react'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { workspacePreferredModeAtom } from '../../../../atoms'; import { @@ -87,9 +87,13 @@ type PageListProps = { }; const filter = { - all: (pageMeta: PageMeta) => !pageMeta.trash, - trash: (pageMeta: PageMeta) => pageMeta.trash, - favorite: (pageMeta: PageMeta) => pageMeta.favorite, + all: (pageMeta: PageMeta, allMetas: PageMeta[]) => !pageMeta.trash, + trash: (pageMeta: PageMeta, allMetas: PageMeta[]) => { + const parentMeta = allMetas.find(m => m.subpageIds?.includes(pageMeta.id)); + return !parentMeta?.trash && pageMeta.trash; + }, + favorite: (pageMeta: PageMeta, allMetas: PageMeta[]) => + pageMeta.favorite && !pageMeta.trash, }; export const PageList: React.FC = ({ @@ -106,9 +110,26 @@ export const PageList: React.FC = ({ const isTrash = listType === 'trash'; const record = useAtomValue(workspacePreferredModeAtom); const list = useMemo( - () => pageList.filter(filter[listType ?? 'all']), + () => + pageList.filter(pageMeta => + filter[listType ?? 'all'](pageMeta, pageList) + ), [pageList, listType] ); + const restorePage = useCallback( + (pageMeta: PageMeta, allMetas: PageMeta[]) => { + helper.setPageMeta(pageMeta.id, { + trash: false, + }); + + allMetas + .filter(m => pageMeta?.subpageIds.includes(m.id)) + .forEach(m => { + restorePage(m, allMetas); + }); + }, + [helper] + ); if (list.length === 0) { return ; } @@ -194,9 +215,7 @@ export const PageList: React.FC = ({ blockSuiteWorkspace.removePage(pageId); }} onRestorePage={() => { - helper.setPageMeta(pageMeta.id, { - trash: false, - }); + restorePage(pageMeta, pageList); }} onOpenPage={pageId => { onClickPage(pageId, false); 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 d1cd3a72f0..fbefb4a646 100644 --- a/apps/web/src/components/pure/workspace-slider-bar/index.tsx +++ b/apps/web/src/components/pure/workspace-slider-bar/index.tsx @@ -1,5 +1,6 @@ import { MuiCollapse } from '@affine/component'; import { IconButton } from '@affine/component'; +import { config } from '@affine/env'; import { useTranslation } from '@affine/i18n'; import { ArrowDownSmallIcon, @@ -20,6 +21,7 @@ import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status'; import { usePageMeta } from '../../../hooks/use-page-meta'; import type { RemWorkspace } from '../../../shared'; import { SidebarSwitch } from '../../affine/sidebar-switch'; +import { Pivot } from './pivot'; import { StyledLink, StyledListItem, @@ -168,6 +170,15 @@ export const WorkSpaceSliderBar: React.FC = ({ {t('All pages')} + + {config.enableSubpage && !!currentWorkspace && ( + + )} + 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 [showPivot, setShowPivot] = useState(true); + + return ( + <> + + + Pivot + { + setShowPivot(!showPivot); + }, [showPivot])} + > + + + + + + + + + ); +}; +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 new file mode 100644 index 0000000000..e640990a5d --- /dev/null +++ b/apps/web/src/components/pure/workspace-slider-bar/pivot/TreeNodeRender.tsx @@ -0,0 +1,69 @@ +import { IconButton } from '@affine/component'; +import { + ArrowDownSmallIcon, + DeleteTemporarilyIcon, + PlusIcon, +} from '@blocksuite/icons'; +import { useRouter } from 'next/router'; + +import { StyledCollapsedButton, StyledPivotItem } from './styles'; +import type { TreeNode } from './types'; + +export const TreeNodeRender: TreeNode['render'] = ( + node, + { onAdd, onDelete, collapsed, setCollapsed }, + extendProps +) => { + const { openPage } = extendProps as { openPage: (pageId: string) => void }; + + const router = useRouter(); + const active = router.query.pageId === node.id; + return ( + { + if (active) { + return; + } + openPage(node.id); + }} + active={active} + > + { + e.stopPropagation(); + setCollapsed(!collapsed); + }} + size="small" + > + + + {node.title || 'Untitled'} + { + e.stopPropagation(); + onAdd(); + }} + size="small" + className="pivot-item-button" + > + + + { + e.stopPropagation(); + + onDelete(); + }} + size="small" + className="pivot-item-button" + > + + + + ); +}; 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 new file mode 100644 index 0000000000..3e9528faf1 --- /dev/null +++ b/apps/web/src/components/pure/workspace-slider-bar/pivot/index.tsx @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000000..53db85af7a --- /dev/null +++ b/apps/web/src/components/pure/workspace-slider-bar/pivot/styles.ts @@ -0,0 +1,47 @@ +import { + displayFlex, + IconButton, + styled, + textEllipsis, +} from '@affine/component'; + +export const StyledPivotItem = styled('div')<{ active: boolean }>( + ({ active, theme }) => { + return { + width: '100%', + height: '32px', + ...displayFlex('flex-start', 'center'), + paddingLeft: '24px', + position: 'relative', + color: active ? theme.colors.primaryColor : theme.colors.textColor, + cursor: 'pointer', + span: { + flexGrow: '1', + ...textEllipsis(1), + }, + '.pivot-item-button': { + display: 'none', + }, + ':hover': { + '.pivot-item-button': { + display: 'flex', + }, + }, + }; + } +); + +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 new file mode 100644 index 0000000000..8166153f30 --- /dev/null +++ b/apps/web/src/components/pure/workspace-slider-bar/pivot/types.ts @@ -0,0 +1,4 @@ +import type { Node } from '@affine/component'; +import type { PageMeta } from '@blocksuite/store'; + +export type TreeNode = Node; diff --git a/apps/web/src/components/pure/workspace-slider-bar/pivot/utils.ts b/apps/web/src/components/pure/workspace-slider-bar/pivot/utils.ts new file mode 100644 index 0000000000..837e402bbd --- /dev/null +++ b/apps/web/src/components/pure/workspace-slider-bar/pivot/utils.ts @@ -0,0 +1,43 @@ +import type { PageMeta } from '@blocksuite/store'; + +import { TreeNodeRender } from './TreeNodeRender'; +import type { TreeNode } from './types'; +export const flattenToTree = ( + handleMetas: PageMeta[], + renderProps: { openPage: (pageId: string) => void } +): 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) { + return ( + metas.find(m => { + return m.subpageIds?.includes(meta.id); + }) === undefined + ); + } + return true; + }) + .filter(meta => !meta.trash); + + 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 + // @ts-ignore + const returnedMeta: TreeNode = { + ...internalMeta, + children: helper(childrenMetas), + render: (node, props) => TreeNodeRender!(node, props, renderProps), + }; + // @ts-ignore + returnedMetas.push(returnedMeta); + return returnedMetas; + }, []); + }; + return helper(rootMetas); +}; diff --git a/apps/web/src/hooks/use-blocksuite-workspace-helper.ts b/apps/web/src/hooks/use-blocksuite-workspace-helper.ts index c748c34aec..1c9646ac84 100644 --- a/apps/web/src/hooks/use-blocksuite-workspace-helper.ts +++ b/apps/web/src/hooks/use-blocksuite-workspace-helper.ts @@ -9,9 +9,9 @@ export function useBlockSuiteWorkspaceHelper( ) { return useMemo( () => ({ - createPage: (pageId: string, title?: string): Page => { + createPage: (pageId: string, parentId?: string): Page => { assertExists(blockSuiteWorkspace); - return blockSuiteWorkspace.createPage(pageId); + return blockSuiteWorkspace.createPage(pageId, parentId); }, }), [blockSuiteWorkspace] diff --git a/apps/web/src/hooks/use-feature-flag.ts b/apps/web/src/hooks/use-feature-flag.ts index b206e01cf8..8e81abff1f 100644 --- a/apps/web/src/hooks/use-feature-flag.ts +++ b/apps/web/src/hooks/use-feature-flag.ts @@ -22,7 +22,6 @@ declare global { callback: Set<() => void>; }; } - if (!globalThis.featureFlag) { globalThis.featureFlag = featureFlag; } diff --git a/apps/web/src/hooks/use-page-meta.ts b/apps/web/src/hooks/use-page-meta.ts index 8c21e7c2da..211767f600 100644 --- a/apps/web/src/hooks/use-page-meta.ts +++ b/apps/web/src/hooks/use-page-meta.ts @@ -6,6 +6,7 @@ import type { BlockSuiteWorkspace } from '../shared'; declare module '@blocksuite/store' { interface PageMeta { favorite?: boolean; + subpageIds: string[]; trash?: boolean; trashDate?: number; // whether to create the page with the default template @@ -45,6 +46,12 @@ export function usePageMetaHelper(blockSuiteWorkspace: BlockSuiteWorkspace) { setPageMeta: (pageId: string, pageMeta: Partial) => { blockSuiteWorkspace.meta.setPageMeta(pageId, pageMeta); }, + getPageMeta: (pageId: string) => { + return blockSuiteWorkspace.meta.getPageMeta(pageId); + }, + shiftPageMeta: (pageId: string, index: number) => { + return blockSuiteWorkspace.meta.shiftPageMeta(pageId, index); + }, }), [blockSuiteWorkspace] ); diff --git a/packages/component/package.json b/packages/component/package.json index 1df8e34bf8..1663a0fede 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -15,11 +15,11 @@ "dependencies": { "@affine/debug": "workspace:*", "@affine/i18n": "workspace:*", - "@blocksuite/blocks": "0.5.0-20230320164115-e612d17", - "@blocksuite/editor": "0.5.0-20230320164115-e612d17", - "@blocksuite/global": "0.5.0-20230320164115-e612d17", + "@blocksuite/blocks": "0.5.0-20230323032255-e75ed32", + "@blocksuite/editor": "0.5.0-20230323032255-e75ed32", + "@blocksuite/global": "0.5.0-20230323032255-e75ed32", "@blocksuite/icons": "2.0.23", - "@blocksuite/store": "0.5.0-20230320164115-e612d17", + "@blocksuite/store": "0.5.0-20230323032255-e75ed32", "@emotion/cache": "^11.10.5", "@emotion/react": "^11.10.6", "@emotion/server": "^11.10.0", @@ -29,6 +29,8 @@ "@mui/material": "^5.11.13", "lit": "^2.6.1", "react": "^18.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-is": "^18.2.0" }, @@ -44,6 +46,7 @@ "@storybook/test-runner": "0.10.0-next.11", "@storybook/testing-library": "0.0.14-next.1", "@types/react": "^18.0.28", + "@types/react-dnd": "^3.0.2", "@types/react-dom": "18.0.11", "@vitejs/plugin-react": "^3.1.0", "jest-mock": "^29.5.0", diff --git a/packages/component/src/index.ts b/packages/component/src/index.ts index 959d7b410b..cf000b13e3 100644 --- a/packages/component/src/index.ts +++ b/packages/component/src/index.ts @@ -15,6 +15,7 @@ export * from './ui/shared/Container'; export * from './ui/table'; export * from './ui/toast'; export * from './ui/tooltip'; +export * from './ui/tree-view'; declare module '@mui/material/styles' { interface Theme { diff --git a/packages/component/src/ui/tree-view/TreeNode.tsx b/packages/component/src/ui/tree-view/TreeNode.tsx new file mode 100644 index 0000000000..4b3cfb767f --- /dev/null +++ b/packages/component/src/ui/tree-view/TreeNode.tsx @@ -0,0 +1,128 @@ +import { useState } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; + +import { + StyledCollapse, + StyledNodeLine, + StyledTreeNodeContainer, + StyledTreeNodeItem, +} from './styles'; +import type { Node, NodeLIneProps, TreeNodeProps } from './types'; + +const NodeLine = ({ + node, + onDrop, + allowDrop = true, + isTop = false, +}: NodeLIneProps) => { + const [{ isOver }, drop] = useDrop( + () => ({ + accept: 'node', + drop: (item: Node, monitor) => { + const didDrop = monitor.didDrop(); + if (didDrop) { + return; + } + onDrop?.(item, node, { + internal: false, + topLine: isTop, + bottomLine: !isTop, + }); + }, + collect: monitor => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + }), + [onDrop] + ); + + return ; +}; + +export const TreeNode = ({ + node, + index, + allDrop = true, + ...otherProps +}: TreeNodeProps) => { + const { onAdd, onDelete, onDrop } = otherProps; + const [collapsed, setCollapsed] = useState(false); + + const [{ isDragging }, drag] = useDrag(() => ({ + type: 'node', + item: node, + collect: monitor => ({ + isDragging: monitor.isDragging(), + }), + })); + + const [{ canDrop, isOver }, drop] = useDrop( + () => ({ + accept: 'node', + drop: (item: Node, monitor) => { + const didDrop = monitor.didDrop(); + if (didDrop || item.id === node.id || !allDrop) { + return; + } + onDrop?.(item, node, { + internal: true, + topLine: false, + bottomLine: false, + }); + }, + collect: monitor => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + }), + [onDrop, allDrop] + ); + + return ( + + + {index === 0 && ( + + )} + {node.render?.(node, { + onAdd: () => onAdd?.(node), + onDelete: () => onDelete?.(node), + collapsed, + setCollapsed, + })} + {(!node.children?.length || collapsed) && ( + + )} + + + + {node.children && + node.children.map((childNode, index) => ( + + ))} + + + ); +}; + +export default TreeNode; diff --git a/packages/component/src/ui/tree-view/TreeView.tsx b/packages/component/src/ui/tree-view/TreeView.tsx new file mode 100644 index 0000000000..1d72433780 --- /dev/null +++ b/packages/component/src/ui/tree-view/TreeView.tsx @@ -0,0 +1,16 @@ +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) => { + return ( + + {data.map((node, index) => ( + + ))} + + ); +}; + +export default TreeView; diff --git a/packages/component/src/ui/tree-view/index.ts b/packages/component/src/ui/tree-view/index.ts new file mode 100644 index 0000000000..240ac00762 --- /dev/null +++ b/packages/component/src/ui/tree-view/index.ts @@ -0,0 +1,3 @@ +export * from './TreeNode'; +export * from './TreeView'; +export * from './types'; diff --git a/packages/component/src/ui/tree-view/styles.ts b/packages/component/src/ui/tree-view/styles.ts new file mode 100644 index 0000000000..448994d4a6 --- /dev/null +++ b/packages/component/src/ui/tree-view/styles.ts @@ -0,0 +1,40 @@ +import MuiCollapse from '@mui/material/Collapse'; + +import { styled } from '../../styles'; + +export const StyledCollapse = styled(MuiCollapse)(() => { + return { + paddingLeft: '12px', + }; +}); +export const StyledTreeNodeItem = styled('div')<{ + isOver?: boolean; + canDrop?: boolean; +}>(({ isOver, canDrop, theme }) => { + return { + background: isOver && canDrop ? theme.colors.hoverBackground : '', + position: 'relative', + }; +}); +export const StyledTreeNodeContainer = styled('div')<{ isDragging: boolean }>( + ({ isDragging, theme }) => { + return { + background: isDragging ? theme.colors.hoverBackground : '', + }; + } +); + +export const StyledNodeLine = styled('div')<{ show: boolean; isTop?: boolean }>( + ({ show, isTop = false, theme }) => { + return { + position: 'absolute', + left: '0', + ...(isTop ? { top: '0' } : { bottom: '0' }), + width: '100%', + paddingTop: '3px', + borderBottom: '3px solid', + borderColor: show ? theme.colors.primaryColor : 'transparent', + zIndex: 1, + }; + } +); diff --git a/packages/component/src/ui/tree-view/types.ts b/packages/component/src/ui/tree-view/types.ts new file mode 100644 index 0000000000..907a557ec1 --- /dev/null +++ b/packages/component/src/ui/tree-view/types.ts @@ -0,0 +1,45 @@ +import type { ReactNode } from 'react'; + +export type Node = { + id: string; + children?: Node[]; + render?: ( + node: Node, + eventsAndStatus: { + onAdd: () => void; + onDelete: () => void; + collapsed: boolean; + setCollapsed: (collapsed: boolean) => void; + }, + extendProps?: unknown + ) => ReactNode; +} & N; + +type CommonProps = { + 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; + index: number; + allDrop?: boolean; +} & CommonProps; + +export type TreeViewProps = { + data: Node[]; +} & CommonProps; + +export type NodeLIneProps = { + allowDrop: boolean; + isTop?: boolean; +} & Pick, 'node' | 'onDrop'>; diff --git a/packages/data-center/package.json b/packages/data-center/package.json index c2bb258bb1..bac12acf54 100644 --- a/packages/data-center/package.json +++ b/packages/data-center/package.json @@ -14,8 +14,8 @@ }, "dependencies": { "@affine/debug": "workspace:*", - "@blocksuite/blocks": "0.5.0-20230320164115-e612d17", - "@blocksuite/store": "0.5.0-20230320164115-e612d17", + "@blocksuite/blocks": "0.5.0-20230323032255-e75ed32", + "@blocksuite/store": "0.5.0-20230323032255-e75ed32", "@tauri-apps/api": "^1.2.0", "encoding": "^0.1.13", "firebase": "^9.18.0", diff --git a/packages/env/package.json b/packages/env/package.json index 0a6a81d676..2620538317 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -9,7 +9,7 @@ "zod": "^3.21.4" }, "dependencies": { - "@blocksuite/global": "0.5.0-20230320164115-e612d17", + "@blocksuite/global": "0.5.0-20230323032255-e75ed32", "lit": "^2.6.1" } } diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts index 3aae91f17e..a34066151e 100644 --- a/packages/env/src/index.ts +++ b/packages/env/src/index.ts @@ -112,6 +112,7 @@ export const publicRuntimeConfigSchema = z.object({ prefetchWorkspace: z.boolean(), // expose internal api to globalThis, **development only** exposeInternal: z.boolean(), + enableSubpage: z.boolean(), }); export type PublicRuntimeConfig = z.infer; diff --git a/yarn.lock b/yarn.lock index 5b31c75b01..786a0580ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,10 +22,10 @@ __metadata: "@affine/env": "workspace:*" "@affine/i18n": "workspace:*" "@affine/templates": "workspace:*" - "@blocksuite/blocks": 0.5.0-20230320164115-e612d17 - "@blocksuite/editor": 0.5.0-20230320164115-e612d17 + "@blocksuite/blocks": 0.5.0-20230323032255-e75ed32 + "@blocksuite/editor": 0.5.0-20230323032255-e75ed32 "@blocksuite/icons": 2.0.23 - "@blocksuite/store": 0.5.0-20230320164115-e612d17 + "@blocksuite/store": 0.5.0-20230323032255-e75ed32 "@emotion/cache": ^11.10.5 "@emotion/react": ^11.10.6 "@emotion/server": ^11.10.0 @@ -83,10 +83,10 @@ __metadata: version: 0.0.0-use.local resolution: "@affine/client-app@workspace:apps/desktop" dependencies: - "@blocksuite/blocks": 0.5.0-20230320164115-e612d17 - "@blocksuite/editor": 0.5.0-20230320164115-e612d17 + "@blocksuite/blocks": 0.5.0-20230323032255-e75ed32 + "@blocksuite/editor": 0.5.0-20230323032255-e75ed32 "@blocksuite/icons": 2.0.23 - "@blocksuite/store": 0.5.0-20230320164115-e612d17 + "@blocksuite/store": 0.5.0-20230323032255-e75ed32 "@emotion/react": ^11.10.6 "@emotion/styled": ^11.10.6 "@tauri-apps/api": ^1.2.0 @@ -118,11 +118,11 @@ __metadata: dependencies: "@affine/debug": "workspace:*" "@affine/i18n": "workspace:*" - "@blocksuite/blocks": 0.5.0-20230320164115-e612d17 - "@blocksuite/editor": 0.5.0-20230320164115-e612d17 - "@blocksuite/global": 0.5.0-20230320164115-e612d17 + "@blocksuite/blocks": 0.5.0-20230323032255-e75ed32 + "@blocksuite/editor": 0.5.0-20230323032255-e75ed32 + "@blocksuite/global": 0.5.0-20230323032255-e75ed32 "@blocksuite/icons": 2.0.23 - "@blocksuite/store": 0.5.0-20230320164115-e612d17 + "@blocksuite/store": 0.5.0-20230323032255-e75ed32 "@emotion/cache": ^11.10.5 "@emotion/react": ^11.10.6 "@emotion/server": ^11.10.0 @@ -141,11 +141,14 @@ __metadata: "@storybook/test-runner": 0.10.0-next.11 "@storybook/testing-library": 0.0.14-next.1 "@types/react": ^18.0.28 + "@types/react-dnd": ^3.0.2 "@types/react-dom": 18.0.11 "@vitejs/plugin-react": ^3.1.0 jest-mock: ^29.5.0 lit: ^2.6.1 react: ^18.2.0 + react-dnd: ^16.0.1 + react-dnd-html5-backend: ^16.0.1 react-dom: ^18.2.0 react-is: ^18.2.0 storybook: 7.0.0-rc.1 @@ -161,8 +164,8 @@ __metadata: resolution: "@affine/datacenter@workspace:packages/data-center" dependencies: "@affine/debug": "workspace:*" - "@blocksuite/blocks": 0.5.0-20230320164115-e612d17 - "@blocksuite/store": 0.5.0-20230320164115-e612d17 + "@blocksuite/blocks": 0.5.0-20230323032255-e75ed32 + "@blocksuite/store": 0.5.0-20230323032255-e75ed32 "@tauri-apps/api": ^1.2.0 encoding: ^0.1.13 fake-indexeddb: 4.0.1 @@ -215,7 +218,7 @@ __metadata: version: 0.0.0-use.local resolution: "@affine/env@workspace:packages/env" dependencies: - "@blocksuite/global": 0.5.0-20230320164115-e612d17 + "@blocksuite/global": 0.5.0-20230323032255-e75ed32 lit: ^2.6.1 next: =13.2.3 react: ^18.2.0 @@ -1752,15 +1755,14 @@ __metadata: languageName: node linkType: hard -"@blocksuite/blocks@npm:0.5.0-20230320164115-e612d17": - version: 0.5.0-20230320164115-e612d17 - resolution: "@blocksuite/blocks@npm:0.5.0-20230320164115-e612d17" +"@blocksuite/blocks@npm:0.5.0-20230323032255-e75ed32": + version: 0.5.0-20230323032255-e75ed32 + resolution: "@blocksuite/blocks@npm:0.5.0-20230323032255-e75ed32" dependencies: - "@blocksuite/global": 0.5.0-20230320164115-e612d17 - "@blocksuite/phasor": 0.5.0-20230320164115-e612d17 - "@blocksuite/virgo": 0.5.0-20230320164115-e612d17 + "@blocksuite/global": 0.5.0-20230323032255-e75ed32 + "@blocksuite/phasor": 0.5.0-20230323032255-e75ed32 + "@blocksuite/virgo": 0.5.0-20230323032255-e75ed32 "@popperjs/core": ^2.11.6 - highlight.js: ^11.7.0 hotkeys-js: ^3.10.1 lit: ^2.6.1 marked: ^4.2.12 @@ -1768,29 +1770,29 @@ __metadata: turndown: ^7.1.1 zod: ^3.21.4 peerDependencies: - "@blocksuite/store": 0.5.0-20230320164115-e612d17 - checksum: ebbe156f0e0320ecac28e744bde121671d7c2eedfb222a89c454c98662d27a59dc362f9b1d00c236ac5e8ffd5af900e70b4fe0e8d2c6e5acea3260ea96bd9ce7 + "@blocksuite/store": 0.5.0-20230323032255-e75ed32 + checksum: 6b850678b983ca7a0e85abf32a32dba2b52b95935d15ae831f23505933bc98b651972417a986a5d43aabe26e7497773d9e12a263dcfafb690665d0f88bc6224b languageName: node linkType: hard -"@blocksuite/editor@npm:0.5.0-20230320164115-e612d17": - version: 0.5.0-20230320164115-e612d17 - resolution: "@blocksuite/editor@npm:0.5.0-20230320164115-e612d17" +"@blocksuite/editor@npm:0.5.0-20230323032255-e75ed32": + version: 0.5.0-20230323032255-e75ed32 + resolution: "@blocksuite/editor@npm:0.5.0-20230323032255-e75ed32" dependencies: - "@blocksuite/global": 0.5.0-20230320164115-e612d17 + "@blocksuite/global": 0.5.0-20230323032255-e75ed32 lit: ^2.6.1 marked: ^4.2.12 turndown: ^7.1.1 peerDependencies: - "@blocksuite/blocks": 0.5.0-20230320164115-e612d17 - "@blocksuite/store": 0.5.0-20230320164115-e612d17 - checksum: 8e797059cd2df4ae3f17d9c39ae3b8493d40cb26303a0aee53113a6dc5d60943fc7bea008f8928543267e2883fb729230aedb90a142b5c01c330d044befbb0a3 + "@blocksuite/blocks": 0.5.0-20230323032255-e75ed32 + "@blocksuite/store": 0.5.0-20230323032255-e75ed32 + checksum: dec95740eacca9294a53547503480fa5681c10ca914c7b214e8a367fe52e8fe5e0dc1de4cf63439b49acb69b95220b1d9f70cd1d3ca55775a70087ef7922d900 languageName: node linkType: hard -"@blocksuite/global@npm:0.5.0-20230320164115-e612d17": - version: 0.5.0-20230320164115-e612d17 - resolution: "@blocksuite/global@npm:0.5.0-20230320164115-e612d17" +"@blocksuite/global@npm:0.5.0-20230323032255-e75ed32": + version: 0.5.0-20230323032255-e75ed32 + resolution: "@blocksuite/global@npm:0.5.0-20230323032255-e75ed32" dependencies: ansi-colors: ^4.1.3 zod: ^3.21.4 @@ -1799,7 +1801,7 @@ __metadata: peerDependenciesMeta: lit: optional: true - checksum: 78f4b473a7d0fa1caa9c4fccb020064f56ca8d9d7585fdd706d8a6bd88e5abeda899596bce56cb7ef19309535144ec817267b032a8567f179ea6e580e1b57526 + checksum: 2a72cb1407bf4a8438a385b7c7421b6c7c053641b20a20d7a2c43230933d75460331ff8d810c239dafa7769e13723a4caeb56fb1067ccda77011654ea936c533 languageName: node linkType: hard @@ -1813,26 +1815,26 @@ __metadata: languageName: node linkType: hard -"@blocksuite/phasor@npm:0.5.0-20230320164115-e612d17": - version: 0.5.0-20230320164115-e612d17 - resolution: "@blocksuite/phasor@npm:0.5.0-20230320164115-e612d17" +"@blocksuite/phasor@npm:0.5.0-20230323032255-e75ed32": + version: 0.5.0-20230323032255-e75ed32 + resolution: "@blocksuite/phasor@npm:0.5.0-20230323032255-e75ed32" dependencies: - "@blocksuite/global": 0.5.0-20230320164115-e612d17 + "@blocksuite/global": 0.5.0-20230323032255-e75ed32 fractional-indexing: ^3.2.0 perfect-freehand: ^1.2.0 peerDependencies: nanoid: ^4 yjs: ^13 - checksum: 06fb6d531aa84838cbcaf0b5ddb0e1da40a98da165e570ac70348e76e459a6a9c93ee71bd4b536b05e4dd51e89bf243d96eb6add755ffda20d2dcd6e497bd2fa + checksum: 083f5932edf4366274406b979be90af54898c7c788d8243ffc0013124e67e520ffb35ad3ae8a0120d2399a12c85659454afc743f3a73498dabbc72ed6164d122 languageName: node linkType: hard -"@blocksuite/store@npm:0.5.0-20230320164115-e612d17": - version: 0.5.0-20230320164115-e612d17 - resolution: "@blocksuite/store@npm:0.5.0-20230320164115-e612d17" +"@blocksuite/store@npm:0.5.0-20230323032255-e75ed32": + version: 0.5.0-20230323032255-e75ed32 + resolution: "@blocksuite/store@npm:0.5.0-20230323032255-e75ed32" dependencies: - "@blocksuite/global": 0.5.0-20230320164115-e612d17 - "@blocksuite/virgo": 0.5.0-20230320164115-e612d17 + "@blocksuite/global": 0.5.0-20230323032255-e75ed32 + "@blocksuite/virgo": 0.5.0-20230323032255-e75ed32 "@types/flexsearch": ^0.7.3 buffer: ^6.0.3 flexsearch: 0.7.21 @@ -1846,20 +1848,20 @@ __metadata: zod: ^3.21.4 peerDependencies: yjs: ^13 - checksum: 5e8f0f6d37a2ae66bd3df1cca67fb21322a78220cbc4114ba19e6486c6e9edab9b24bd14e99045822eae4af9c0116c86de463e87ad45503268d71a96dbc126b0 + checksum: ff5ec05f74a397fe29302b61e87b13e26029e0cf03356029058cc2cc1f791f3bd0643ed9b29950718c58c67e374922610ab91b2546feeee8e26828d7e99089d7 languageName: node linkType: hard -"@blocksuite/virgo@npm:0.5.0-20230320164115-e612d17": - version: 0.5.0-20230320164115-e612d17 - resolution: "@blocksuite/virgo@npm:0.5.0-20230320164115-e612d17" +"@blocksuite/virgo@npm:0.5.0-20230323032255-e75ed32": + version: 0.5.0-20230323032255-e75ed32 + resolution: "@blocksuite/virgo@npm:0.5.0-20230323032255-e75ed32" dependencies: - "@blocksuite/global": 0.5.0-20230320164115-e612d17 + "@blocksuite/global": 0.5.0-20230323032255-e75ed32 zod: ^3.21.4 peerDependencies: lit: ^2 yjs: ^13 - checksum: 3e9154f4abe501f5a880a4c2fed45077451b55bbf6386cb1cd8fb7efd4e621818961b9cdcadcd1f00d1b97760b6b509557b866addd9b18fab0f8410f7ac9139c + checksum: 9407ea1707ca586926f4cf417adcba89928255840f645865443d791d988cb2cb8d2bfb4cad5ce1eaead1fbdb75f6a5f9cf41c7e5f7d181380854b9cc9b85d3d2 languageName: node linkType: hard @@ -4598,6 +4600,27 @@ __metadata: languageName: node linkType: hard +"@react-dnd/asap@npm:^5.0.1": + version: 5.0.2 + resolution: "@react-dnd/asap@npm:5.0.2" + checksum: 18f040e53512983f11c542ef21e6e4cac605d585a10cd764b13bc1b2f3ac7490e0fa40503adc348d8387aa45bc8e7eebe9cb33003b960a30bb5fde666ff2adde + languageName: node + linkType: hard + +"@react-dnd/invariant@npm:^4.0.1": + version: 4.0.2 + resolution: "@react-dnd/invariant@npm:4.0.2" + checksum: 594f6d78896c19bb8f023e101334fd91a9fdff686117bd8e830ba53737ec0a6042dab66971d3d63c7afbc622103909aff7a64c5c6767e0aa8d9561fd42705016 + languageName: node + linkType: hard + +"@react-dnd/shallowequal@npm:^4.0.1": + version: 4.0.2 + resolution: "@react-dnd/shallowequal@npm:4.0.2" + checksum: 7f21d691bddbfd4d2830948cbeefecca1600b2b46bcb1934926795f07ae8a1fa60a3dfd3a2112be5ef682c3820c80a99711e9fa15843f7e300acb25a4ecb70ab + languageName: node + linkType: hard + "@redux-devtools/extension@npm:^3.2.5": version: 3.2.5 resolution: "@redux-devtools/extension@npm:3.2.5" @@ -6846,6 +6869,15 @@ __metadata: languageName: node linkType: hard +"@types/react-dnd@npm:^3.0.2": + version: 3.0.2 + resolution: "@types/react-dnd@npm:3.0.2" + dependencies: + react-dnd: "*" + checksum: 3013caff425f1b3c0cd2b79e7b7763f88f100a6ef5aa5e5cec7636119a71976827ee7bd316d7724f3a179e1f076b1fa1726fefcb9743b780d4bec4f0cd8aacfb + languageName: node + linkType: hard + "@types/react-dom@npm:18.0.11, @types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.0.11": version: 18.0.11 resolution: "@types/react-dom@npm:18.0.11" @@ -9707,6 +9739,17 @@ __metadata: languageName: node linkType: hard +"dnd-core@npm:^16.0.1": + version: 16.0.1 + resolution: "dnd-core@npm:16.0.1" + dependencies: + "@react-dnd/asap": ^5.0.1 + "@react-dnd/invariant": ^4.0.1 + redux: ^4.2.0 + checksum: b7d3ef4664f433af796f440ddd27ad9d7fef0205f26c4b7c0af6ebf612ffa9b33e64d095d3e79190c4baaed34aa36570f321ebe0d2cc8ff1031ff158a0907b3f + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -12493,14 +12536,7 @@ __metadata: languageName: node linkType: hard -"highlight.js@npm:^11.7.0": - version: 11.7.0 - resolution: "highlight.js@npm:11.7.0" - checksum: 19e3fb8b56f4b361b057a8523b989dfeb6479bbd1e29cec3fac6fa5c78d09927d5fa61b7dba6631fdb57cfdca9b3084aa4da49405ceaf4a67f67beae2ed5b77d - languageName: node - linkType: hard - -"hoist-non-react-statics@npm:^3.3.1": +"hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -17053,6 +17089,40 @@ __metadata: languageName: node linkType: hard +"react-dnd-html5-backend@npm:^16.0.1": + version: 16.0.1 + resolution: "react-dnd-html5-backend@npm:16.0.1" + dependencies: + dnd-core: ^16.0.1 + checksum: e2368bf85d5632a5cd867b743feb54c9052d909ea5331608860fa455edf3c633ac791f5b338e3db29b19ea8670c0ba5fb43c9c1c2510760bea030811d726cdfa + languageName: node + linkType: hard + +"react-dnd@npm:*, react-dnd@npm:^16.0.1": + version: 16.0.1 + resolution: "react-dnd@npm:16.0.1" + dependencies: + "@react-dnd/invariant": ^4.0.1 + "@react-dnd/shallowequal": ^4.0.1 + dnd-core: ^16.0.1 + fast-deep-equal: ^3.1.3 + hoist-non-react-statics: ^3.3.2 + peerDependencies: + "@types/hoist-non-react-statics": ">= 3.3.1" + "@types/node": ">= 12" + "@types/react": ">= 16" + react: ">= 16.14" + peerDependenciesMeta: + "@types/hoist-non-react-statics": + optional: true + "@types/node": + optional: true + "@types/react": + optional: true + checksum: e8da2186aaafcd5bb41c090a995c963a7c3c73c20991667a2cfc0c800d7f7f73913414b2e61c437cdb6221bb2151bd5174088b8b42c17056a896fc4d1da5729f + languageName: node + linkType: hard + "react-docgen-typescript@npm:^2.2.2": version: 2.2.2 resolution: "react-docgen-typescript@npm:2.2.2" @@ -17401,7 +17471,7 @@ __metadata: languageName: node linkType: hard -"redux@npm:^4.2.1": +"redux@npm:^4.2.0, redux@npm:^4.2.1": version: 4.2.1 resolution: "redux@npm:4.2.1" dependencies: