From e50bf9fbfe700394dad16e033ba9395885178061 Mon Sep 17 00:00:00 2001 From: Qi <474021214@qq.com> Date: Sat, 8 Apr 2023 05:55:59 +0800 Subject: [PATCH] feat: add root pinboard & rename pivots to pinboard (#1843) --- .../components/__tests__/PinBoard.spec.tsx | 147 ++++----- .../affine/operation-menu-items/CopyLink.tsx | 23 +- .../affine/operation-menu-items/MoveTo.tsx | 13 +- .../src/components/affine/pinboard/index.ts | 2 + .../pinboard-menu}/SearchContent.tsx | 8 +- .../pinboard-menu/index.tsx} | 58 ++-- .../pinboard/pinboard-render/EmptyItem.tsx | 14 + .../pinboard-render}/OperationButton.tsx | 86 +++--- .../affine/pinboard/pinboard-render/index.tsx | 122 ++++++++ .../affine/{pivots => pinboard}/styles.ts | 4 +- .../affine/pivots/PivotsMenu/EmptyItem.tsx | 10 - .../affine/pivots/PivotsMenu/Pivots.tsx | 70 ----- .../affine/pivots/hooks/usePivotData.ts | 74 ----- .../web/src/components/affine/pivots/index.ts | 5 - .../pivots/pivot-render/PivotRender.tsx | 93 ------ .../web/src/components/affine/pivots/types.ts | 18 -- .../page-list/OperationCell.tsx | 35 ++- .../header-right-items/EditorOptionMenu.tsx | 26 +- .../pure/workspace-slider-bar/Pinboard.tsx | 65 ++++ .../pure/workspace-slider-bar/Pivots.tsx | 114 ------- .../workspace-slider-bar/changeLog/index.tsx | 7 +- .../pure/workspace-slider-bar/index.tsx | 4 +- .../workspace-slider-bar/shared-styles.ts | 4 +- .../web/src/hooks/affine/use-pinboard-data.ts | 79 +++++ .../affine/use-pinboard-handler.ts} | 117 +++----- apps/web/src/hooks/use-page-meta.ts | 12 +- .../src/pages/workspace/[workspaceId]/all.tsx | 5 + apps/web/src/utils/blocksuite.ts | 41 ++- .../component/src/ui/tree-view/TreeNode.tsx | 19 +- packages/component/src/ui/tree-view/types.ts | 2 + tests/libs/utils.ts | 7 + tests/parallels/pin-board.spec.ts | 281 ++++++++++++++---- 32 files changed, 836 insertions(+), 729 deletions(-) create mode 100644 apps/web/src/components/affine/pinboard/index.ts rename apps/web/src/components/affine/{pivots/PivotsMenu => pinboard/pinboard-menu}/SearchContent.tsx (89%) rename apps/web/src/components/affine/{pivots/PivotsMenu/PivotsMenu.tsx => pinboard/pinboard-menu/index.tsx} (70%) create mode 100644 apps/web/src/components/affine/pinboard/pinboard-render/EmptyItem.tsx rename apps/web/src/components/affine/{pivots/pivot-render => pinboard/pinboard-render}/OperationButton.tsx (69%) create mode 100644 apps/web/src/components/affine/pinboard/pinboard-render/index.tsx rename apps/web/src/components/affine/{pivots => pinboard}/styles.ts (97%) delete mode 100644 apps/web/src/components/affine/pivots/PivotsMenu/EmptyItem.tsx delete mode 100644 apps/web/src/components/affine/pivots/PivotsMenu/Pivots.tsx delete mode 100644 apps/web/src/components/affine/pivots/hooks/usePivotData.ts delete mode 100644 apps/web/src/components/affine/pivots/index.ts delete mode 100644 apps/web/src/components/affine/pivots/pivot-render/PivotRender.tsx delete mode 100644 apps/web/src/components/affine/pivots/types.ts create mode 100644 apps/web/src/components/pure/workspace-slider-bar/Pinboard.tsx delete mode 100644 apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx create mode 100644 apps/web/src/hooks/affine/use-pinboard-data.ts rename apps/web/src/{components/affine/pivots/hooks/usePivotHandler.ts => hooks/affine/use-pinboard-handler.ts} (60%) diff --git a/apps/web/src/components/__tests__/PinBoard.spec.tsx b/apps/web/src/components/__tests__/PinBoard.spec.tsx index c216ff346a..dfd73a22e4 100644 --- a/apps/web/src/components/__tests__/PinBoard.spec.tsx +++ b/apps/web/src/components/__tests__/PinBoard.spec.tsx @@ -19,8 +19,8 @@ import { import { useWorkspacesHelper } from '../../hooks/use-workspaces'; import { ThemeProvider } from '../../providers/ThemeProvider'; import type { BlockSuiteWorkspace } from '../../shared'; -import type { PivotsProps } from '../pure/workspace-slider-bar/Pivots'; -import Pivots from '../pure/workspace-slider-bar/Pivots'; +import type { PinboardProps } from '../pure/workspace-slider-bar/Pinboard'; +import Pinboard from '../pure/workspace-slider-bar/Pinboard'; expect.extend(matchers); @@ -35,17 +35,18 @@ const ProviderWrapper: FC = ({ children }) => { }; const initPinBoard = async () => { - // create one workspace with 2 root pages and 2 pivot pages - // - hasPivotPage - // - pivot1 - // - pivot2 - // - noPivotPage + // create one workspace with 2 root pages and 2 pinboard pages + // - hasPinboardPage + // - hasPinboardPage + // - pinboard1 + // - pinboard2 + // - noPinboardPage const mutationHook = renderHook(() => useWorkspacesHelper(), { wrapper: ProviderWrapper, }); - const rootPageIds = ['hasPivotPage', 'noPivotPage']; - const pivotPageIds = ['pivot1', 'pivot2']; + const rootPageIds = ['hasPinboardPage', 'noPinboardPage']; + const pinboardPageIds = ['pinboard1', 'pinboard2']; const id = await mutationHook.result.current.createLocalWorkspace('test0'); await store.get(workspacesAtom); mutationHook.rerender(); @@ -59,25 +60,32 @@ const initPinBoard = async () => { const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace as BlockSuiteWorkspace; + // create root pinboard + mutationHook.result.current.createWorkspacePage(id, 'rootPinboard'); + blockSuiteWorkspace.meta.setPageMeta('rootPinboard', { + isRootPinboard: true, + subpageIds: rootPageIds, + }); + // create parent rootPageIds.forEach(rootPageId => { mutationHook.result.current.createWorkspacePage(id, rootPageId); blockSuiteWorkspace.meta.setPageMeta(rootPageId, { - isPivots: true, - subpageIds: rootPageId === rootPageIds[0] ? pivotPageIds : [], + subpageIds: rootPageId === rootPageIds[0] ? pinboardPageIds : [], }); }); - pivotPageIds.forEach(pivotId => { - mutationHook.result.current.createWorkspacePage(id, pivotId); - blockSuiteWorkspace.meta.setPageMeta(pivotId, { - title: pivotId, + // create children to firs parent + pinboardPageIds.forEach(pinboardId => { + mutationHook.result.current.createWorkspacePage(id, pinboardId); + blockSuiteWorkspace.meta.setPageMeta(pinboardId, { + title: pinboardId, }); }); - const App = (props: PivotsProps) => { + const App = (props: PinboardProps) => { return ( - + ); @@ -93,53 +101,51 @@ const initPinBoard = async () => { return { rootPageIds, - pivotPageIds, + pinboardPageIds, app, blockSuiteWorkspace, }; }; const openOperationMenu = async (app: RenderResult, pageId: string) => { - const rootPivot = await app.findByTestId(`pivot-${pageId}`); - const operationBtn = (await rootPivot.querySelector( - '[data-testid="pivot-operation-button"]' + const rootPinboard = await app.findByTestId(`pinboard-${pageId}`); + const operationBtn = (await rootPinboard.querySelector( + '[data-testid="pinboard-operation-button"]' )) as HTMLElement; await operationBtn.click(); - const menu = await app.findByTestId('pivot-operation-menu'); + const menu = await app.findByTestId('pinboard-operation-menu'); expect(menu).toBeInTheDocument(); }; describe('PinBoard', () => { - test('add pivot', async () => { - const { app, blockSuiteWorkspace, rootPageIds, pivotPageIds } = - await initPinBoard(); - const [hasPivotPageId] = rootPageIds; - await openOperationMenu(app, hasPivotPageId); + test('add pinboard', async () => { + const { app, blockSuiteWorkspace, rootPageIds } = await initPinBoard(); + const [hasChildrenPageId] = rootPageIds; + await openOperationMenu(app, hasChildrenPageId); - const addBtn = await app.findByTestId('pivot-operation-add'); + const addBtn = await app.findByTestId('pinboard-operation-add'); await addBtn.click(); - const metas = blockSuiteWorkspace.meta.pageMetas ?? []; - const rootPageMeta = blockSuiteWorkspace.meta.getPageMeta(hasPivotPageId); - const addedPageMeta = metas.find( - meta => !pivotPageIds.includes(meta.id) && !rootPageIds.includes(meta.id) - ) as PageMeta; + const hasChildrenPageMeta = + blockSuiteWorkspace.meta.getPageMeta(hasChildrenPageId); // Page meta have been added - expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(5); + expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(6); // New page meta is added in initial page meta - expect(rootPageMeta?.subpageIds.includes(addedPageMeta.id)).toBe(true); + expect(hasChildrenPageMeta?.subpageIds.length).toBe(3); app.unmount(); }); - test('delete pivot', async () => { + test('delete pinboard', async () => { const { app, blockSuiteWorkspace, - rootPageIds: [hasPivotPageId], + rootPageIds: [hasChildrenPageId], } = await initPinBoard(); - await openOperationMenu(app, hasPivotPageId); + await openOperationMenu(app, hasChildrenPageId); - const deleteBtn = await app.findByTestId('pivot-operation-move-to-trash'); + const deleteBtn = await app.findByTestId( + 'pinboard-operation-move-to-trash' + ); await deleteBtn.click(); const confirmBtn = await app.findByTestId('move-to-trash-confirm'); @@ -153,77 +159,78 @@ describe('PinBoard', () => { app.unmount(); }); - test('rename pivot', async () => { + test('rename pinboard', async () => { const { app, - rootPageIds: [hasPivotPageId], + rootPageIds: [hasChildrenPageId], } = await initPinBoard(); - await openOperationMenu(app, hasPivotPageId); + await openOperationMenu(app, hasChildrenPageId); - const renameBtn = await app.findByTestId('pivot-operation-rename'); + const renameBtn = await app.findByTestId('pinboard-operation-rename'); await renameBtn.click(); - const input = await app.findByTestId(`pivot-input-${hasPivotPageId}`); + const input = await app.findByTestId(`pinboard-input-${hasChildrenPageId}`); expect(input).toBeInTheDocument(); // TODO: Fix this test // fireEvent.change(input, { target: { value: 'tteesstt' } }); - // // expect( - // blockSuiteWorkspace.meta.getPageMeta(rootPageId)?.name + // blockSuiteWorkspace.meta.getPageMeta(hasChildrenPageId)?.name // ).toBe('tteesstt'); app.unmount(); }); - test('move pivot', async () => { + test('move pinboard', async () => { const { app, blockSuiteWorkspace, - rootPageIds: [hasPivotPageId], - pivotPageIds: [pivotId1, pivotId2], + rootPageIds: [hasChildrenPageId], + pinboardPageIds: [pinboardId1, pinboardId2], } = await initPinBoard(); - await openOperationMenu(app, pivotId1); + await openOperationMenu(app, pinboardId1); - const moveToBtn = await app.findByTestId('pivot-operation-move-to'); + const moveToBtn = await app.findByTestId('pinboard-operation-move-to'); await moveToBtn.click(); - const pivotsMenu = await app.findByTestId('pivots-menu'); - expect(pivotsMenu).toBeInTheDocument(); + const pinboardMenu = await app.findByTestId('pinboard-menu'); + expect(pinboardMenu).toBeInTheDocument(); await ( - pivotsMenu.querySelector( - `[data-testid="pivot-${pivotId2}"]` + pinboardMenu.querySelector( + `[data-testid="pinboard-${pinboardId2}"]` ) as HTMLElement ).click(); - const rootPageMeta = blockSuiteWorkspace.meta.getPageMeta(hasPivotPageId); + const hasChildrenPageMeta = + blockSuiteWorkspace.meta.getPageMeta(hasChildrenPageId); - expect(rootPageMeta?.subpageIds.includes(pivotId1)).toBe(false); - expect(rootPageMeta?.subpageIds.includes(pivotId2)).toBe(true); + expect(hasChildrenPageMeta?.subpageIds.includes(pinboardId1)).toBe(false); + expect(hasChildrenPageMeta?.subpageIds.includes(pinboardId2)).toBe(true); app.unmount(); }); - test('remove from pivots', async () => { + test('remove from pinboard', async () => { const { app, blockSuiteWorkspace, - rootPageIds: [hasPivotPageId], - pivotPageIds: [pivotId1], + rootPageIds: [hasChildrenPageId], + pinboardPageIds: [pinboardId1], } = await initPinBoard(); - await openOperationMenu(app, pivotId1); + await openOperationMenu(app, pinboardId1); - const moveToBtn = await app.findByTestId('pivot-operation-move-to'); + const moveToBtn = await app.findByTestId('pinboard-operation-move-to'); await moveToBtn.click(); - const removeFromPivotsBtn = await app.findByTestId( - 'remove-from-pivots-button' + const removeFromPinboardBtn = await app.findByTestId( + 'remove-from-pinboard-button' ); - removeFromPivotsBtn.click(); + removeFromPinboardBtn.click(); - const hasPivotsPageMeta = - blockSuiteWorkspace.meta.getPageMeta(hasPivotPageId); + const hasPinboardPageMeta = + blockSuiteWorkspace.meta.getPageMeta(hasChildrenPageId); - expect(hasPivotsPageMeta?.subpageIds.length).toBe(1); - expect(hasPivotsPageMeta?.subpageIds.includes(pivotId1)).toBe(false); + expect(hasPinboardPageMeta?.subpageIds.length).toBe(1); + expect(hasPinboardPageMeta?.subpageIds.includes(pinboardId1)).toBe(false); + app.unmount(); }); }); diff --git a/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx b/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx index 260bbf1319..7410736398 100644 --- a/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx +++ b/apps/web/src/components/affine/operation-menu-items/CopyLink.tsx @@ -16,17 +16,16 @@ export const CopyLink = ({ onItemClick, onSelect }: CommonMenuItemProps) => { }, [t]); return ( - <> - { - copyUrl(); - onItemClick?.(); - onSelect?.(); - }} - icon={} - > - {t('Copy Link')} - - + { + copyUrl(); + onItemClick?.(); + onSelect?.(); + }} + icon={} + > + {t('Copy Link')} + ); }; diff --git a/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx b/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx index 5e31978c17..f31314b309 100644 --- a/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx +++ b/apps/web/src/components/affine/operation-menu-items/MoveTo.tsx @@ -2,10 +2,10 @@ 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 { useMemo, useRef, useState } from 'react'; import type { BlockSuiteWorkspace } from '../../../shared'; -import { PivotsMenu } from '../pivots'; +import { PinboardMenu } from '../pinboard'; import type { CommonMenuItemProps } from './types'; export type MoveToProps = CommonMenuItemProps<{ @@ -43,14 +43,17 @@ export const MoveTo = ({ > {t('Move to')} - !meta.trash)} + metas={useMemo( + () => metas.filter(m => !m.trash && m.id !== currentMeta.id), + [metas, currentMeta] + )} currentMeta={currentMeta} blockSuiteWorkspace={blockSuiteWorkspace} - onPivotClick={onSelect} + onPinboardClick={onSelect} /> ); diff --git a/apps/web/src/components/affine/pinboard/index.ts b/apps/web/src/components/affine/pinboard/index.ts new file mode 100644 index 0000000000..d37c3b63fb --- /dev/null +++ b/apps/web/src/components/affine/pinboard/index.ts @@ -0,0 +1,2 @@ +export * from './pinboard-menu'; +export * from './pinboard-render/'; diff --git a/apps/web/src/components/affine/pivots/PivotsMenu/SearchContent.tsx b/apps/web/src/components/affine/pinboard/pinboard-menu/SearchContent.tsx similarity index 89% rename from apps/web/src/components/affine/pivots/PivotsMenu/SearchContent.tsx rename to apps/web/src/components/affine/pinboard/pinboard-menu/SearchContent.tsx index 924dc67275..6141ba32f3 100644 --- a/apps/web/src/components/affine/pivots/PivotsMenu/SearchContent.tsx +++ b/apps/web/src/components/affine/pinboard/pinboard-menu/SearchContent.tsx @@ -7,7 +7,7 @@ import Image from 'next/legacy/image'; import React from 'react'; import { workspacePreferredModeAtom } from '../../../../atoms'; -import { StyledMenuSubTitle, StyledPivot } from '../styles'; +import { StyledMenuSubTitle, StyledPinboard } from '../styles'; export const SearchContent = ({ results, @@ -27,16 +27,16 @@ export const SearchContent = ({ {results.map(meta => { return ( - { onClick?.(meta.id); }} - data-testid="pivot-search-result" + data-testid="pinboard-search-result" > {record[meta.id] === 'edgeless' ? : } {meta.title} - + ); })} diff --git a/apps/web/src/components/affine/pivots/PivotsMenu/PivotsMenu.tsx b/apps/web/src/components/affine/pinboard/pinboard-menu/index.tsx similarity index 70% rename from apps/web/src/components/affine/pivots/PivotsMenu/PivotsMenu.tsx rename to apps/web/src/components/affine/pinboard/pinboard-menu/index.tsx index 34f5351a16..25766968c8 100644 --- a/apps/web/src/components/affine/pivots/PivotsMenu/PivotsMenu.tsx +++ b/apps/web/src/components/affine/pinboard/pinboard-menu/index.tsx @@ -1,42 +1,41 @@ import type { PureMenuProps } from '@affine/component'; -import { Input, PureMenu } from '@affine/component'; +import { Input, PureMenu, TreeView } from '@affine/component'; import { useTranslation } from '@affine/i18n'; import { RemoveIcon, SearchIcon } from '@blocksuite/icons'; import type { PageMeta } from '@blocksuite/store'; import React, { useCallback, useState } from 'react'; +import { usePinboardData } from '../../../../hooks/affine/use-pinboard-data'; +import { usePinboardHandler } from '../../../../hooks/affine/use-pinboard-handler'; import { usePageMetaHelper } from '../../../../hooks/use-page-meta'; import type { BlockSuiteWorkspace } from '../../../../shared'; import { toast } from '../../../../utils'; -import { usePivotData } from '../hooks/usePivotData'; -import { usePivotHandler } from '../hooks/usePivotHandler'; -import { PivotRender } from '../pivot-render/PivotRender'; +import { PinboardRender } from '../pinboard-render/'; import { StyledMenuContent, StyledMenuFooter, StyledMenuSubTitle, - StyledPivot, + StyledPinboard, StyledSearchContainer, } from '../styles'; -import { Pivots } from './Pivots'; import { SearchContent } from './SearchContent'; -export type PivotsMenuProps = { +export type PinboardMenuProps = { metas: PageMeta[]; currentMeta: PageMeta; blockSuiteWorkspace: BlockSuiteWorkspace; - showRemovePivots?: boolean; - onPivotClick?: (p: { dragId: string; dropId: string }) => void; + showRemovePinboard?: boolean; + onPinboardClick?: (p: { dragId: string; dropId: string }) => void; } & PureMenuProps; -export const PivotsMenu = ({ +export const PinboardMenu = ({ metas, currentMeta, blockSuiteWorkspace, - showRemovePivots = false, - onPivotClick, + showRemovePinboard = false, + onPinboardClick, ...pureMenuProps -}: PivotsMenuProps) => { +}: PinboardMenuProps) => { const { t } = useTranslation(); const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); const [query, setQuery] = useState(''); @@ -46,7 +45,7 @@ export const PivotsMenu = ({ meta => !meta.trash && meta.title.includes(query) ); - const { handleDrop } = usePivotHandler({ + const { handleDrop } = usePinboardHandler({ blockSuiteWorkspace, metas, }); @@ -60,15 +59,15 @@ export const PivotsMenu = ({ topLine: false, internal: true, }); - onPivotClick?.({ dragId: currentMeta.id, dropId }); + onPinboardClick?.({ dragId: currentMeta.id, dropId }); toast(`Moved "${currentMeta.title}" to "${targetTitle}"`); }, - [currentMeta.id, currentMeta.title, handleDrop, metas, onPivotClick] + [currentMeta.id, currentMeta.title, handleDrop, metas, onPinboardClick] ); - const { data } = usePivotData({ + const { data } = usePinboardData({ metas, - pivotRender: PivotRender, + pinboardRender: PinboardRender, blockSuiteWorkspace, onClick: (e, node) => { handleClick(node.id); @@ -80,7 +79,7 @@ export const PivotsMenu = ({ width={320} height={480} {...pureMenuProps} - data-testid="pivots-menu" + data-testid="pinboard-menu" > @@ -104,21 +103,16 @@ export const PivotsMenu = ({ {!isSearching && ( <> Suggested - + )} - {showRemovePivots && ( + {showRemovePinboard && ( - { - setPageMeta(currentMeta.id, { isPivots: false }); const parentMeta = metas.find(m => m.subpageIds.includes(currentMeta.id) ); @@ -132,11 +126,13 @@ export const PivotsMenu = ({ }} > - {t('Remove from Pivots')} - + {t('Remove from Pinboard')} +

{t('RFP')}

)} ); }; + +export default PinboardMenu; diff --git a/apps/web/src/components/affine/pinboard/pinboard-render/EmptyItem.tsx b/apps/web/src/components/affine/pinboard/pinboard-render/EmptyItem.tsx new file mode 100644 index 0000000000..6d8e45ab85 --- /dev/null +++ b/apps/web/src/components/affine/pinboard/pinboard-render/EmptyItem.tsx @@ -0,0 +1,14 @@ +import { useTranslation } from '@affine/i18n'; + +import { StyledPinboard } from '../styles'; + +export const EmptyItem = () => { + const { t } = useTranslation(); + return ( + + {t('No item')} + + ); +}; + +export default EmptyItem; diff --git a/apps/web/src/components/affine/pivots/pivot-render/OperationButton.tsx b/apps/web/src/components/affine/pinboard/pinboard-render/OperationButton.tsx similarity index 69% rename from apps/web/src/components/affine/pivots/pivot-render/OperationButton.tsx rename to apps/web/src/components/affine/pinboard/pinboard-render/OperationButton.tsx index d013ffb0b1..91fcfaa598 100644 --- a/apps/web/src/components/affine/pivots/pivot-render/OperationButton.tsx +++ b/apps/web/src/components/affine/pinboard/pinboard-render/OperationButton.tsx @@ -14,10 +14,11 @@ import { usePageMetaHelper } from '../../../../hooks/use-page-meta'; import type { BlockSuiteWorkspace } from '../../../../shared'; import { toast } from '../../../../utils'; import { CopyLink, MoveToTrash } from '../../operation-menu-items'; -import { PivotsMenu } from '../PivotsMenu/PivotsMenu'; +import { PinboardMenu } from '../pinboard-menu/'; import { StyledOperationButton } from '../styles'; export type OperationButtonProps = { + isRoot: boolean; onAdd: () => void; onDelete: () => void; metas: PageMeta[]; @@ -28,6 +29,7 @@ export type OperationButtonProps = { onMenuClose?: () => void; }; export const OperationButton = ({ + isRoot, onAdd, onDelete, metas, @@ -44,7 +46,7 @@ export const OperationButton = ({ const [anchorEl, setAnchorEl] = useState(null); const [operationMenuOpen, setOperationMenuOpen] = useState(false); - const [pivotsMenuOpen, setPivotsMenuOpen] = useState(false); + const [pinboardMenuOpen, setPinboardMenuOpen] = useState(false); const [confirmModalOpen, setConfirmModalOpen] = useState(false); const menuIndex = useMemo(() => modalIndex + 1, [modalIndex]); const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); @@ -53,7 +55,7 @@ export const OperationButton = ({ { setOperationMenuOpen(false); - setPivotsMenuOpen(false); + setPinboardMenuOpen(false); }} >
{ setOperationMenuOpen(false); - setPivotsMenuOpen(false); + setPinboardMenuOpen(false); }} > setAnchorEl(ref)} size="small" onClick={() => { @@ -78,7 +80,7 @@ export const OperationButton = ({ { onAdd(); setOperationMenuOpen(false); @@ -96,47 +98,53 @@ export const OperationButton = ({ > {t('Add a subpage inside')} - { - setOperationMenuOpen(false); - setPivotsMenuOpen(true); - }} - icon={} - > - {t('Move to')} - - { - onRename?.(); - setOperationMenuOpen(false); - onMenuClose?.(); - }} - icon={} - > - {t('Rename')} - - { - setOperationMenuOpen(false); - setConfirmModalOpen(true); - onMenuClose?.(); - }} - /> + {!isRoot && ( + { + setOperationMenuOpen(false); + setPinboardMenuOpen(true); + }} + icon={} + > + {t('Move to')} + + )} + {!isRoot && ( + { + onRename?.(); + setOperationMenuOpen(false); + onMenuClose?.(); + }} + icon={} + > + {t('Rename')} + + )} + {!isRoot && ( + { + setOperationMenuOpen(false); + setConfirmModalOpen(true); + onMenuClose?.(); + }} + /> + )} - { + switch (type) { + case 'root': + return ; + case 'edgeless': + return ; + default: + return ; + } +}; + +export const PinboardRender: PinboardNode['render'] = ( + node, + { isOver, onAdd, onDelete, collapsed, setCollapsed, isSelected }, + renderProps +) => { + const { + onClick, + showOperationButton = false, + currentMeta, + metas = [], + blockSuiteWorkspace, + } = renderProps!; + const record = useAtomValue(workspacePreferredModeAtom); + const { setPageTitle } = usePageMetaHelper(blockSuiteWorkspace); + + const router = useRouter(); + + const [isHover, setIsHover] = useState(false); + const [showRename, setShowRename] = useState(false); + + const active = router.query.pageId === node.id; + const isRoot = !!currentMeta.isRootPinboard; + return ( + <> + { + onClick?.(e, node); + }} + onMouseEnter={() => setIsHover(true)} + onMouseLeave={() => setIsHover(false)} + isOver={isOver || isSelected} + active={active} + > + { + e.stopPropagation(); + setCollapsed(node.id, !collapsed); + }} + > + + + {getIcon(isRoot ? 'root' : record[node.id])} + + {showRename ? ( + e.stopPropagation()} + height={32} + onBlur={() => { + setShowRename(false); + }} + onChange={value => { + // FIXME: setPageTitle would make input blur, and can't input the Chinese character + setPageTitle(node.id, value); + }} + /> + ) : ( + {isRoot ? 'Pinboard' : currentMeta.title || 'Untitled'} + )} + + {showOperationButton && ( + setIsHover(false)} + onRename={() => { + setShowRename(true); + setIsHover(false); + }} + /> + )} + + + {useMemo( + () => + isRoot && + !metas.find(m => (currentMeta.subpageIds ?? []).includes(m.id)), + [currentMeta.subpageIds, isRoot, metas] + ) && } + + ); +}; +export default PinboardRender; diff --git a/apps/web/src/components/affine/pivots/styles.ts b/apps/web/src/components/affine/pinboard/styles.ts similarity index 97% rename from apps/web/src/components/affine/pivots/styles.ts rename to apps/web/src/components/affine/pinboard/styles.ts index caf9af3e6e..90d3473b83 100644 --- a/apps/web/src/components/affine/pivots/styles.ts +++ b/apps/web/src/components/affine/pinboard/styles.ts @@ -21,14 +21,14 @@ export const StyledCollapsedButton = styled('button')<{ margin: 'auto', color: theme.colors.iconColor, opacity: '.6', - display: show ? 'block' : 'none', + display: show ? 'flex' : 'none', svg: { transform: `rotate(${collapse ? '0' : '-90'}deg)`, }, }; }); -export const StyledPivot = styled('div')<{ +export const StyledPinboard = styled('div')<{ disable?: boolean; active?: boolean; isOver?: boolean; diff --git a/apps/web/src/components/affine/pivots/PivotsMenu/EmptyItem.tsx b/apps/web/src/components/affine/pivots/PivotsMenu/EmptyItem.tsx deleted file mode 100644 index a53472c995..0000000000 --- a/apps/web/src/components/affine/pivots/PivotsMenu/EmptyItem.tsx +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index f6e6b9ceab..0000000000 --- a/apps/web/src/components/affine/pivots/PivotsMenu/Pivots.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import type { TreeViewProps } from '@affine/component'; -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 { toast } from '../../../../utils'; -import { StyledCollapsedButton, StyledPivot } from '../styles'; -import type { NodeRenderProps } from '../types'; -import EmptyItem from './EmptyItem'; -import type { PivotsMenuProps } from './PivotsMenu'; - -export type PivotsProps = { - data: TreeViewProps['data']; -} & Pick; -export const Pivots = ({ - data, - blockSuiteWorkspace, - currentMeta, -}: PivotsProps) => { - const { t } = useTranslation(); - const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); - const [showPivot, setShowPivot] = useState(true); - const isPivotEmpty = useMemo(() => data.length === 0, [data]); - return ( - <> - { - setPageMeta(currentMeta.id, { isPivots: true }); - toast(`Moved "${currentMeta.title}" to Pivots`); - }} - > - ) => { - e.stopPropagation(); - setShowPivot(!showPivot); - }, - [showPivot] - )} - collapse={showPivot} - > - - - - {t('Pivots')} - - - - {isPivotEmpty ? ( - - ) : ( - - )} - - - ); -}; -export default Pivots; diff --git a/apps/web/src/components/affine/pivots/hooks/usePivotData.ts b/apps/web/src/components/affine/pivots/hooks/usePivotData.ts deleted file mode 100644 index 01d2fea1aa..0000000000 --- a/apps/web/src/components/affine/pivots/hooks/usePivotData.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { PageMeta } from '@blocksuite/store'; -import { useMemo } from 'react'; - -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 rootMetas = metas - .filter(meta => { - if (meta.subpageIds) { - return ( - metas.find(m => { - return m.subpageIds?.includes(meta.id); - }) === undefined - ); - } - return true; - }) - .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(m => m); - // @ts-ignore - const returnedMeta: TreeNode = { - ...internalMeta, - children: helper(childrenMetas), - render: (node, props) => - pivotRender(node, props, { - ...renderProps, - currentMeta: internalMeta, - metas, - }), - }; - 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/index.ts b/apps/web/src/components/affine/pivots/index.ts deleted file mode 100644 index 4f5691a9ed..0000000000 --- a/apps/web/src/components/affine/pivots/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './hooks/usePivotData'; -export * from './hooks/usePivotHandler'; -export * from './pivot-render/PivotRender'; -export * from './PivotsMenu/PivotsMenu'; -export * from './types'; diff --git a/apps/web/src/components/affine/pivots/pivot-render/PivotRender.tsx b/apps/web/src/components/affine/pivots/pivot-render/PivotRender.tsx deleted file mode 100644 index 2714fa2446..0000000000 --- a/apps/web/src/components/affine/pivots/pivot-render/PivotRender.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Input } from '@affine/component'; -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 { usePageMetaHelper } from '../../../../hooks/use-page-meta'; -import { StyledCollapsedButton, StyledPivot } from '../styles'; -import type { TreeNode } from '../types'; -import { OperationButton } from './OperationButton'; - -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 { setPageTitle } = usePageMetaHelper(blockSuiteWorkspace); - - const router = useRouter(); - - const [isHover, setIsHover] = useState(false); - const [showRename, setShowRename] = 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' ? : } - {showRename ? ( - e.stopPropagation()} - height={32} - onBlur={() => { - setShowRename(false); - }} - onChange={value => { - // FIXME: setPageTitle would make input blur, and can't input the Chinese character - setPageTitle(node.id, value); - }} - /> - ) : ( - {currentMeta?.title || 'Untitled'} - )} - - {showOperationButton && ( - setIsHover(false)} - onRename={() => { - setShowRename(true); - setIsHover(false); - }} - /> - )} - - ); -}; diff --git a/apps/web/src/components/affine/pivots/types.ts b/apps/web/src/components/affine/pivots/types.ts deleted file mode 100644 index 388b1064a4..0000000000 --- a/apps/web/src/components/affine/pivots/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -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/blocksuite/block-suite-page-list/page-list/OperationCell.tsx b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/OperationCell.tsx index 0fec338b62..19d673a89a 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 @@ -44,6 +44,7 @@ export const OperationCell: React.FC = ({ const { id, favorite } = pageMeta; const { t } = useTranslation(); const [open, setOpen] = useState(false); + const OperationMenu = ( <> = ({ > {t('Open in new tab')} - - { - setOpen(true); - }} - icon={} - > - {t('Move to Trash')} - + {!pageMeta.isRootPinboard && ( + + )} + {!pageMeta.isRootPinboard && ( + { + setOpen(true); + }} + icon={} + > + {t('Move to Trash')} + + )} ); return ( @@ -90,7 +95,7 @@ export const OperationCell: React.FC = ({ disablePortal={true} trigger="click" > - + diff --git a/apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx b/apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx index 8b8a335639..ce506f8ca2 100644 --- a/apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx +++ b/apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx @@ -82,17 +82,21 @@ export const EditorOptionMenu = () => { {mode === 'page' ? t('Edgeless') : t('Page')} - - { - setOpenConfirm(true); - }} - /> + {!pageMeta.isRootPinboard && ( + + )} + {!pageMeta.isRootPinboard && ( + { + setOpenConfirm(true); + }} + /> + )} ); diff --git a/apps/web/src/components/pure/workspace-slider-bar/Pinboard.tsx b/apps/web/src/components/pure/workspace-slider-bar/Pinboard.tsx new file mode 100644 index 0000000000..9a97abd1c5 --- /dev/null +++ b/apps/web/src/components/pure/workspace-slider-bar/Pinboard.tsx @@ -0,0 +1,65 @@ +import { TreeView } from '@affine/component'; +import type { PageMeta } from '@blocksuite/store'; +import type { MouseEvent } from 'react'; +import { useCallback } from 'react'; + +import type { PinboardNode } from '../../../hooks/affine/use-pinboard-data'; +import { usePinboardData } from '../../../hooks/affine/use-pinboard-data'; +import { usePinboardHandler } from '../../../hooks/affine/use-pinboard-handler'; +import type { BlockSuiteWorkspace } from '../../../shared'; +import { PinboardRender } from '../../affine/pinboard'; + +export type PinboardProps = { + blockSuiteWorkspace: BlockSuiteWorkspace; + openPage: (pageId: string) => void; + allMetas: PageMeta[]; +}; + +export const Pinboard = ({ + blockSuiteWorkspace, + openPage, + allMetas, +}: PinboardProps) => { + const handlePinboardClick = useCallback( + (e: MouseEvent, node: PinboardNode) => { + openPage(node.id); + }, + [openPage] + ); + const onAdd = useCallback( + (id: string) => { + openPage(id); + }, + [openPage] + ); + + const { data } = usePinboardData({ + metas: allMetas.filter(meta => !meta.trash), + pinboardRender: PinboardRender, + blockSuiteWorkspace: blockSuiteWorkspace, + onClick: handlePinboardClick, + showOperationButton: true, + }); + + const { handleAdd, handleDelete, handleDrop } = usePinboardHandler({ + blockSuiteWorkspace: blockSuiteWorkspace, + metas: allMetas, + onAdd, + }); + + if (!data.length) { + return null; + } + return ( +
+ +
+ ); +}; +export default Pinboard; diff --git a/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx b/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx deleted file mode 100644 index 8bc2225015..0000000000 --- a/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx +++ /dev/null @@ -1,114 +0,0 @@ -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 { BlockSuiteWorkspace } 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 = ({ - blockSuiteWorkspace, - openPage, - allMetas, -}: { - blockSuiteWorkspace: BlockSuiteWorkspace; - 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: blockSuiteWorkspace, - onClick: handlePivotClick, - showOperationButton: true, - }); - - const { handleAdd, handleDelete, handleDrop } = usePivotHandler({ - blockSuiteWorkspace: blockSuiteWorkspace, - - metas: allMetas, - onAdd, - }); - - return ( - - ); -}; - -export type PivotsProps = { - blockSuiteWorkspace: BlockSuiteWorkspace; - openPage: (pageId: string) => void; - allMetas: PageMeta[]; -}; - -export const Pivots = ({ - blockSuiteWorkspace, - openPage, - allMetas, -}: PivotsProps) => { - const { t } = useTranslation(); - - const [showPivot, setShowPivot] = useState(true); - const metas = useMemo(() => allMetas.filter(meta => !meta.trash), [allMetas]); - const isPivotEmpty = useMemo( - () => metas.filter(meta => meta.isPivots === true).length === 0, - [metas] - ); - - return ( -
- { - setShowPivot(!showPivot); - }, [showPivot])} - > - - - - - {t('Pivots')} - - - - {isPivotEmpty ? ( - - ) : ( - - )} - -
- ); -}; -export default Pivots; diff --git a/apps/web/src/components/pure/workspace-slider-bar/changeLog/index.tsx b/apps/web/src/components/pure/workspace-slider-bar/changeLog/index.tsx index 9c1dff20b6..9dcaaeff2f 100644 --- a/apps/web/src/components/pure/workspace-slider-bar/changeLog/index.tsx +++ b/apps/web/src/components/pure/workspace-slider-bar/changeLog/index.tsx @@ -7,8 +7,9 @@ import { useGuideHidden, useGuideHiddenUntilNextUpdate, } from '../../../../hooks/affine/use-is-first-load'; -import { StyledChangeLog, StyledChangeLogWarper } from '../shared-styles'; +import { StyledChangeLog, StyledChangeLogWrapper } from '../shared-styles'; import { StyledLink } from '../style'; + export const ChangeLog = () => { const [guideHidden, setGuideHidden] = useGuideHidden(); const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] = @@ -33,7 +34,7 @@ export const ChangeLog = () => { return <>; } return ( - + @@ -49,7 +50,7 @@ export const ChangeLog = () => {
- + ); }; 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 b1430c317f..1d9c2f7242 100644 --- a/apps/web/src/components/pure/workspace-slider-bar/index.tsx +++ b/apps/web/src/components/pure/workspace-slider-bar/index.tsx @@ -22,7 +22,7 @@ import type { AllWorkspace } from '../../../shared'; import { SidebarSwitch } from '../../affine/sidebar-switch'; import { ChangeLog } from './changeLog'; import Favorite from './favorite'; -import { Pivots } from './Pivots'; +import { Pinboard } from './Pinboard'; import { StyledListItem } from './shared-styles'; import { StyledLink, @@ -188,7 +188,7 @@ export const WorkSpaceSliderBar: React.FC = ({ currentWorkspace={currentWorkspace} /> {!!blockSuiteWorkspace && ( - (({ isClose }) => { return { diff --git a/apps/web/src/hooks/affine/use-pinboard-data.ts b/apps/web/src/hooks/affine/use-pinboard-data.ts new file mode 100644 index 0000000000..06f80986f7 --- /dev/null +++ b/apps/web/src/hooks/affine/use-pinboard-data.ts @@ -0,0 +1,79 @@ +import type { Node } from '@affine/component'; +import type { PageMeta } from '@blocksuite/store'; +import type { MouseEvent } from 'react'; +import { useMemo } from 'react'; + +import type { BlockSuiteWorkspace } from '../../shared'; + +export type RenderProps = { + blockSuiteWorkspace: BlockSuiteWorkspace; + onClick?: (e: MouseEvent, node: PinboardNode) => void; + showOperationButton?: boolean; +}; + +export type NodeRenderProps = RenderProps & { + metas: PageMeta[]; + currentMeta: PageMeta; +}; + +export type PinboardNode = Node; + +function flattenToTree( + metas: PageMeta[], + pinboardRender: PinboardNode['render'], + renderProps: RenderProps +): PinboardNode[] { + const rootMeta = metas.find(meta => meta.isRootPinboard); + const helper = (internalMetas: PageMeta[]): PinboardNode[] => { + return internalMetas.reduce( + (returnedMetas, internalMeta) => { + const { subpageIds = [] } = internalMeta; + const childrenMetas = subpageIds + .map(id => metas.find(m => m.id === id)!) + .filter(m => m); + // @ts-ignore + const returnedMeta: PinboardNode = { + ...internalMeta, + children: helper(childrenMetas), + render: (node, props) => + pinboardRender(node, props, { + ...renderProps, + currentMeta: internalMeta, + metas, + }), + }; + returnedMetas.push(returnedMeta); + return returnedMetas; + }, + [] + ); + }; + return helper(rootMeta ? [{ ...rootMeta, renderTopLine: false }] : []); +} + +export function usePinboardData({ + metas, + pinboardRender, + blockSuiteWorkspace, + onClick, + showOperationButton, +}: { + metas: PageMeta[]; + pinboardRender: PinboardNode['render']; +} & RenderProps) { + const data = useMemo( + () => + flattenToTree(metas, pinboardRender, { + blockSuiteWorkspace, + onClick, + showOperationButton, + }), + [blockSuiteWorkspace, metas, onClick, pinboardRender, showOperationButton] + ); + + return { + data, + }; +} + +export default usePinboardData; diff --git a/apps/web/src/components/affine/pivots/hooks/usePivotHandler.ts b/apps/web/src/hooks/affine/use-pinboard-handler.ts similarity index 60% rename from apps/web/src/components/affine/pivots/hooks/usePivotHandler.ts rename to apps/web/src/hooks/affine/use-pinboard-handler.ts index e60220856e..7a30a193b6 100644 --- a/apps/web/src/components/affine/pivots/hooks/usePivotHandler.ts +++ b/apps/web/src/hooks/affine/use-pinboard-handler.ts @@ -4,21 +4,21 @@ 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'; +import type { BlockSuiteWorkspace } from '../../shared'; +import { useBlockSuiteWorkspaceHelper } from '../use-blocksuite-workspace-helper'; +import { usePageMetaHelper } from '../use-page-meta'; +import type { NodeRenderProps, PinboardNode } from './use-pinboard-data'; -const logger = new DebugLogger('pivot'); +const logger = new DebugLogger('pinboard'); -const findRootIds = (metas: PageMeta[], id: string): string[] => { +function 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 = ({ +} +export function usePinboardHandler({ blockSuiteWorkspace, metas, onAdd, @@ -30,13 +30,12 @@ export const usePivotHandler = ({ onAdd?: (addedId: string, parentId: string) => void; onDelete?: TreeViewProps['onDelete']; onDrop?: TreeViewProps['onDrop']; -}) => { +}) { const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace); - const { getPageMeta, setPageMeta, shiftPageMeta } = - usePageMetaHelper(blockSuiteWorkspace); + const { getPageMeta, setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); const handleAdd = useCallback( - (node: TreeNode) => { + (node: PinboardNode) => { const id = nanoid(); createPage(id, node.id); onAdd?.(id, node.id); @@ -45,7 +44,7 @@ export const usePivotHandler = ({ ); const handleDelete = useCallback( - (node: TreeNode) => { + (node: PinboardNode) => { const removeToTrash = (currentMeta: PageMeta) => { const { subpageIds = [] } = currentMeta; setPageMeta(currentMeta.id, { @@ -80,91 +79,57 @@ export const usePivotHandler = ({ if (dropRootIds.includes(dragId)) { return; } - - const { topLine, bottomLine } = position; logger.info('handleDrop', { dragId, dropId, - bottomLine, + position, metas, }); + const { topLine, bottomLine } = position; + 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 ?? [])]; - + if (dropParentMeta?.id === dragParentMeta?.id) { + // same parent + 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, - }); - + dragParentMeta && + setPageMeta(dragParentMeta.id, { + subpageIds: newSubpageIds, + }); return onDrop?.(dragId, dropId, position); } + const newDragParentSubpageIds = [...(dragParentMeta?.subpageIds ?? [])]; + const deleteIndex = newDragParentSubpageIds.findIndex( + id => id === dragId + ); + newDragParentSubpageIds.splice(deleteIndex, 1); - 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); + const newDropParentSubpageIds = [...(dropParentMeta?.subpageIds ?? [])]; + const insertIndex = + newDropParentSubpageIds.findIndex(id => id === dropId) + insertOffset; + newDropParentSubpageIds.splice(insertIndex, 0, dragId); + dragParentMeta && setPageMeta(dragParentMeta.id, { - subpageIds: newSubpageIds, + subpageIds: newDragParentSubpageIds, + }); + dropParentMeta && + setPageMeta(dropParentMeta.id, { + subpageIds: newDropParentSubpageIds, }); - } - 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; @@ -185,7 +150,7 @@ export const usePivotHandler = ({ subpageIds: newSubpageIds, }); }, - [metas, onDrop, setPageMeta, shiftPageMeta] + [metas, onDrop, setPageMeta] ); return { @@ -193,6 +158,6 @@ export const usePivotHandler = ({ handleAdd, handleDelete, }; -}; +} -export default usePivotHandler; +export default usePinboardHandler; diff --git a/apps/web/src/hooks/use-page-meta.ts b/apps/web/src/hooks/use-page-meta.ts index 06387435d0..e8bb348be0 100644 --- a/apps/web/src/hooks/use-page-meta.ts +++ b/apps/web/src/hooks/use-page-meta.ts @@ -13,8 +13,7 @@ declare module '@blocksuite/store' { trashDate?: number; // whether to create the page with the default template init?: boolean; - // use for subpage - isPivots?: boolean; + isRootPinboard?: boolean; } } @@ -44,10 +43,13 @@ export function usePageMeta( return pageMeta; } -export function usePageMetaHelper(blockSuiteWorkspace: BlockSuiteWorkspace) { +export function usePageMetaHelper( + blockSuiteWorkspace: BlockSuiteWorkspace | null +) { return useMemo( () => ({ setPageTitle: (pageId: string, newTitle: string) => { + assertExists(blockSuiteWorkspace); const page = blockSuiteWorkspace.getPage(pageId); assertExists(page); const pageBlock = page @@ -58,15 +60,19 @@ export function usePageMetaHelper(blockSuiteWorkspace: BlockSuiteWorkspace) { pageBlock.title.delete(0, pageBlock.title.length); pageBlock.title.insert(newTitle, 0); }); + assertExists(blockSuiteWorkspace); blockSuiteWorkspace.meta.setPageMeta(pageId, { title: newTitle }); }, setPageMeta: (pageId: string, pageMeta: Partial) => { + assertExists(blockSuiteWorkspace); blockSuiteWorkspace.meta.setPageMeta(pageId, pageMeta); }, getPageMeta: (pageId: string) => { + assertExists(blockSuiteWorkspace); return blockSuiteWorkspace.meta.getPageMeta(pageId); }, shiftPageMeta: (pageId: string, index: number) => { + assertExists(blockSuiteWorkspace); return blockSuiteWorkspace.meta.shiftPageMeta(pageId, index); }, }), diff --git a/apps/web/src/pages/workspace/[workspaceId]/all.tsx b/apps/web/src/pages/workspace/[workspaceId]/all.tsx index 25c7236679..f505d94c47 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/all.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/all.tsx @@ -19,6 +19,7 @@ import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-route import { WorkspaceLayout } from '../../../layouts'; import { WorkspacePlugins } from '../../../plugins'; import type { NextPageWithLayout } from '../../../shared'; +import { ensureRootPinboard } from '../../../utils'; const AllPage: NextPageWithLayout = () => { const router = useRouter(); @@ -35,6 +36,8 @@ const AllPage: NextPageWithLayout = () => { } if (currentWorkspace.flavour !== WorkspaceFlavour.LOCAL) { // only create a new page for local workspace + // just ensure the root pinboard exists + ensureRootPinboard(currentWorkspace.blockSuiteWorkspace); return; } const localProvider = currentWorkspace.providers.find( @@ -53,6 +56,8 @@ const AllPage: NextPageWithLayout = () => { }); jumpToPage(currentWorkspace.id, pageId); } + // no matter workspace is empty, ensure the root pinboard exists + ensureRootPinboard(currentWorkspace.blockSuiteWorkspace); }; provider.callbacks.add(callback); return () => { diff --git a/apps/web/src/utils/blocksuite.ts b/apps/web/src/utils/blocksuite.ts index 85a674ae25..d2ae4f2079 100644 --- a/apps/web/src/utils/blocksuite.ts +++ b/apps/web/src/utils/blocksuite.ts @@ -1,8 +1,10 @@ import { DebugLogger } from '@affine/debug'; import markdown from '@affine/templates/Welcome-to-AFFiNE.md'; import { ContentParser } from '@blocksuite/blocks/content-parser'; -import type { EditorContainer } from '@blocksuite/editor'; import type { Page } from '@blocksuite/store'; +import { nanoid } from '@blocksuite/store'; + +import type { BlockSuiteWorkspace } from '../shared'; const demoTitle = markdown .split('\n') @@ -15,7 +17,7 @@ const demoText = markdown.split('\n').slice(1).join('\n'); const logger = new DebugLogger('init-page'); -export function initPage(page: Page, editor: Readonly): void { +export function initPage(page: Page): void { logger.debug('initEmptyPage', page.id); // Add page block and surface block at root level const isFirstPage = page.meta.init === true; @@ -23,26 +25,23 @@ export function initPage(page: Page, editor: Readonly): void { page.workspace.setPageMeta(page.id, { init: false, }); - _initPageWithDemoMarkdown(page, editor); + _initPageWithDemoMarkdown(page); } else { - _initEmptyPage(page, editor); + _initEmptyPage(page); } page.resetHistory(); } -export function _initEmptyPage(page: Page, _: Readonly) { +export function _initEmptyPage(page: Page, title?: string): void { const pageBlockId = page.addBlock('affine:page', { - title: new page.Text(''), + title: new page.Text(title ?? ''), }); page.addBlock('affine:surface', {}, null); const frameId = page.addBlock('affine:frame', {}, pageBlockId); page.addBlock('affine:paragraph', {}, frameId); } -export function _initPageWithDemoMarkdown( - page: Page, - editor: Readonly -): void { +export function _initPageWithDemoMarkdown(page: Page): void { logger.debug('initPageWithDefaultMarkdown', page.id); const pageBlockId = page.addBlock('affine:page', { title: new page.Text(demoTitle), @@ -63,3 +62,25 @@ export function _initPageWithDemoMarkdown( }); page.workspace.setPageMeta(page.id, { demoTitle }); } + +export function ensureRootPinboard(blockSuiteWorkspace: BlockSuiteWorkspace) { + const metas = blockSuiteWorkspace.meta.pageMetas; + const rootMeta = metas.find(m => m.isRootPinboard); + + if (rootMeta) { + return rootMeta.id; + } + + const rootPinboardPage = blockSuiteWorkspace.createPage(nanoid()); + + const title = `${blockSuiteWorkspace.meta.name}'s Pinboard`; + + _initEmptyPage(rootPinboardPage, title); + + blockSuiteWorkspace.meta.setPageMeta(rootPinboardPage.id, { + isRootPinboard: true, + title, + }); + + return rootPinboardPage.id; +} diff --git a/packages/component/src/ui/tree-view/TreeNode.tsx b/packages/component/src/ui/tree-view/TreeNode.tsx index e37a6b6334..de70cd3b0a 100644 --- a/packages/component/src/ui/tree-view/TreeNode.tsx +++ b/packages/component/src/ui/tree-view/TreeNode.tsx @@ -144,11 +144,12 @@ export const TreeNode = ({ }: TreeNodeProps) => { const { indent, enableDnd, collapsedIds } = otherProps; const collapsed = collapsedIds.includes(node.id); + const { renderTopLine = true, renderBottomLine = true } = node; return ( - {enableDnd && index === 0 && ( + {enableDnd && renderTopLine && index === 0 && ( ({ /> )} - {enableDnd && (!node.children?.length || collapsed) && ( - - )} + {enableDnd && + renderBottomLine && + (!node.children?.length || collapsed) && ( + + )} {node.children && diff --git a/packages/component/src/ui/tree-view/types.ts b/packages/component/src/ui/tree-view/types.ts index 8b1af1687c..b63a87a997 100644 --- a/packages/component/src/ui/tree-view/types.ts +++ b/packages/component/src/ui/tree-view/types.ts @@ -26,6 +26,8 @@ export type Node = { }, renderProps?: RenderProps ) => ReactNode; + renderTopLine?: boolean; + renderBottomLine?: boolean; }; type CommonProps = { diff --git a/tests/libs/utils.ts b/tests/libs/utils.ts index 10641bdb26..4454598449 100644 --- a/tests/libs/utils.ts +++ b/tests/libs/utils.ts @@ -1,3 +1,4 @@ +import type { PageMeta } from '@blocksuite/store'; import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; @@ -123,3 +124,9 @@ export async function loginUser( export async function openHomePage(page: Page) { return page.goto('http://localhost:8080'); } + +export async function getMetas(page: Page): Promise { + return page.evaluate( + () => globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas ?? [] + ); +} diff --git a/tests/parallels/pin-board.spec.ts b/tests/parallels/pin-board.spec.ts index 7eaf41f33c..163ab814fb 100644 --- a/tests/parallels/pin-board.spec.ts +++ b/tests/parallels/pin-board.spec.ts @@ -4,8 +4,9 @@ import { expect } from '@playwright/test'; import { openHomePage } from '../libs/load-page'; import { clickPageMoreActions, newPage } from '../libs/page-logic'; import { test } from '../libs/playwright'; +import { getMetas } from '../libs/utils'; -async function createRootPage(page: Page, title: string) { +async function createPinboardPage(page: Page, parentId: string, title: string) { await newPage(page); await page.focus('.affine-default-page-block-title'); await page.type('.affine-default-page-block-title', title, { @@ -13,73 +14,249 @@ async function createRootPage(page: Page, title: string) { }); await clickPageMoreActions(page); await page.getByTestId('move-to-menu-item').click(); - await page.getByTestId('root-pivot-button-in-pivots-menu').click(); + await page + .getByTestId('pinboard-menu') + .getByTestId(`pinboard-${parentId}`) + .click(); } -async function createPivotPage(page: Page, title: string, parentTitle: string) { - await newPage(page); - await page.focus('.affine-default-page-block-title'); - await page.type('.affine-default-page-block-title', title, { - delay: 100, - }); - await clickPageMoreActions(page); - await page.getByTestId('move-to-menu-item').click(); - await page.getByTestId('pivots-menu').getByText(parentTitle).click(); -} -export async function initPinBoard(page: Page) { + +async function initHomePageWithPinboard(page: Page) { await openHomePage(page); - await createRootPage(page, 'parent1'); - await createRootPage(page, 'parent2'); - await createPivotPage(page, 'child1', 'parent1'); - await createPivotPage(page, 'child2', 'parent1'); + await page.waitForSelector('[data-testid="sidebar-pinboard-container"]'); + return (await getMetas(page)).find(m => m.isRootPinboard); } + +async function openPinboardPageOperationMenu(page: Page, id: string) { + const node = await page + .getByTestId('sidebar-pinboard-container') + .getByTestId(`pinboard-${id}`); + await node.hover(); + await node.getByTestId('pinboard-operation-button').click(); +} + test.describe('PinBoard interaction', () => { - test('Add pivot', async ({ page }) => { - await initPinBoard(page); + test('Have initial root pinboard page when first in', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + expect(rootPinboardMeta).not.toBeUndefined(); }); - test('Remove pivots', async ({ page }) => { - await initPinBoard(page); - const child2Meta = await page.evaluate(() => { - return globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas.find( - m => m.title === 'child2' - ); - }); - - const child2 = await page - .getByTestId('sidebar-pivots-container') - .getByTestId(`pivot-${child2Meta?.id}`); - await child2.hover(); - await child2.getByTestId('pivot-operation-button').click(); - await page.getByTestId('pivot-operation-move-to').click(); - await page.getByTestId('remove-from-pivots-button').click(); - await page.waitForTimeout(1000); + test('Root pinboard page have no operation of "trash" ,"rename" and "move to"', async ({ + page, + }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? ''); expect( - await page.locator(`[data-testid="pivot-${child2Meta.id}"]`).count() + await page + .locator(`[data-testid="pinboard-operation-move-to-trash"]`) + .count() + ).toEqual(0); + expect( + await page.locator(`[data-testid="pinboard-operation-rename"]`).count() + ).toEqual(0); + expect( + await page.locator(`[data-testid="pinboard-operation-move-to"]`).count() ).toEqual(0); }); - test('search pivot', async ({ page }) => { - await initPinBoard(page); + test('Click Pinboard in sidebar should open root pinboard page', async ({ + page, + }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await page.getByTestId(`pinboard-${rootPinboardMeta?.id}`).click(); + await page.waitForTimeout(200); + expect(await page.locator('affine-editor-container')).not.toBeNull(); + }); - const child2Meta = await page.evaluate(() => { - return globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas.find( - m => m.title === 'child2' - ); - }); + test('Add pinboard by header operation menu', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); + const meta = (await getMetas(page)).find(m => m.title === 'test1'); + expect(meta).not.toBeUndefined(); + expect( + await page + .getByTestId('[data-testid="sidebar-pinboard-container"]') + .getByTestId(`pinboard-${meta?.id}`) + ).not.toBeNull(); + }); - const child2 = await page - .getByTestId('sidebar-pivots-container') - .getByTestId(`pivot-${child2Meta?.id}`); - await child2.hover(); - await child2.getByTestId('pivot-operation-button').click(); - await page.getByTestId('pivot-operation-move-to').click(); + test('Add pinboard by sidebar operation menu', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); - await page.fill('[data-testid="pivots-menu-search"]', '111'); + await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? ''); + await page.getByTestId('pinboard-operation-add').click(); + const newPageMeta = (await getMetas(page)).find( + m => m.id !== rootPinboardMeta?.id + ); + expect( + await page + .getByTestId('sidebar-pinboard-container') + .getByTestId(`pinboard-${newPageMeta?.id}`) + ).not.toBeNull(); + }); + + test('Move pinboard to another in sidebar', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2'); + const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); + const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2'); + await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); + await page.getByTestId('pinboard-operation-move-to').click(); + await page + .getByTestId('pinboard-menu') + .getByTestId(`pinboard-${childMeta2?.id}`) + .click(); + expect( + (await getMetas(page)).find(m => m.title === 'test2')?.subpageIds.length + ).toBe(1); + }); + + test('Should no show pinboard self in move to menu', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2'); + const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); + const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2'); + + await page.getByTestId('all-pages').click(); + await page + .getByTestId(`page-list-item-${childMeta?.id}`) + .getByTestId('page-list-operation-button') + .click(); + await page.getByTestId('move-to-menu-item').click(); + + expect( + await page + .getByTestId('pinboard-menu') + .locator(`[data-testid="pinboard-${childMeta?.id}"]`) + .count() + ).toEqual(0); + }); + test('Move pinboard to another in page list', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2'); + const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); + const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2'); + + await page.getByTestId('all-pages').click(); + await page + .getByTestId(`page-list-item-${childMeta?.id}`) + .getByTestId('page-list-operation-button') + .click(); + await page.getByTestId('move-to-menu-item').click(); + await page + .getByTestId('pinboard-menu') + .getByTestId(`pinboard-${childMeta2?.id}`) + .click(); + expect( + (await getMetas(page)).find(m => m.title === 'test2')?.subpageIds.length + ).toBe(1); + }); + + test('Remove from pinboard', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); + const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); + + await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); + + await page.getByTestId('pinboard-operation-move-to').click(); + await page.getByTestId('remove-from-pinboard-button').click(); + await page.waitForTimeout(1000); + expect( + await page.locator(`[data-testid="pinboard-${childMeta?.id}"]`).count() + ).toEqual(0); + }); + + test('search pinboard', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); + const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); + + await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); + + await page.getByTestId('pinboard-operation-move-to').click(); + + await page.fill('[data-testid="pinboard-menu-search"]', '111'); expect(await page.locator('[alt="no result"]').count()).toEqual(1); - await page.fill('[data-testid="pivots-menu-search"]', 'parent2'); + await page.fill('[data-testid="pinboard-menu-search"]', 'test1'); expect( - await page.locator('[data-testid="pivot-search-result"]').count() + await page.locator('[data-testid="pinboard-search-result"]').count() ).toEqual(1); }); + + test('Rename pinboard', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); + const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); + + await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); + + await page.getByTestId('pinboard-operation-rename').click(); + await page.fill(`[data-testid="pinboard-input-${childMeta?.id}"]`, 'test2'); + + const title = (await page + .locator('.affine-default-page-block-title') + .textContent()) as string; + + expect(title).toEqual('test2'); + }); + + test('Move pinboard to trash', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); + const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); + await createPinboardPage(page, childMeta?.id ?? '', 'test2'); + const grandChildMeta = (await getMetas(page)).find( + m => m.title === 'test2' + ); + + await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); + + await page.getByTestId('pinboard-operation-move-to-trash').click(); + ( + await page.waitForSelector('[data-testid="move-to-trash-confirm"]') + ).click(); + await page.waitForTimeout(1000); + + expect( + await page + .getByTestId('sidebar-pinboard-container') + .locator(`[data-testid="pinboard-${childMeta?.id}"]`) + .count() + ).toEqual(0); + + expect( + await page + .getByTestId('sidebar-pinboard-container') + .locator(`[data-testid="pinboard-${grandChildMeta?.id}"]`) + .count() + ).toEqual(0); + }); + + // FIXME + test.skip('Copy link', async ({ page }) => { + const rootPinboardMeta = await initHomePageWithPinboard(page); + await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1'); + const childMeta = (await getMetas(page)).find(m => m.title === 'test1'); + + await openPinboardPageOperationMenu(page, childMeta?.id ?? ''); + + await page.getByTestId('copy-link').click(); + + await page.evaluate(() => { + const input = document.createElement('input'); + input.id = 'paste-input'; + document.body.appendChild(input); + input.focus(); + }); + await page.keyboard.press(`Meta+v`, { delay: 50 }); + await page.keyboard.press(`Control+v`, { delay: 50 }); + const copiedValue = await page + .locator('#paste-input') + .evaluate((input: HTMLInputElement) => input.value); + expect(copiedValue).toEqual(page.url()); + }); });