feat: add root pinboard & rename pivots to pinboard (#1843)

This commit is contained in:
Qi
2023-04-08 05:55:59 +08:00
committed by GitHub
parent d4b2b9ab44
commit e50bf9fbfe
32 changed files with 836 additions and 729 deletions

View File

@@ -19,8 +19,8 @@ import {
import { useWorkspacesHelper } from '../../hooks/use-workspaces'; import { useWorkspacesHelper } from '../../hooks/use-workspaces';
import { ThemeProvider } from '../../providers/ThemeProvider'; import { ThemeProvider } from '../../providers/ThemeProvider';
import type { BlockSuiteWorkspace } from '../../shared'; import type { BlockSuiteWorkspace } from '../../shared';
import type { PivotsProps } from '../pure/workspace-slider-bar/Pivots'; import type { PinboardProps } from '../pure/workspace-slider-bar/Pinboard';
import Pivots from '../pure/workspace-slider-bar/Pivots'; import Pinboard from '../pure/workspace-slider-bar/Pinboard';
expect.extend(matchers); expect.extend(matchers);
@@ -35,17 +35,18 @@ const ProviderWrapper: FC<PropsWithChildren> = ({ children }) => {
}; };
const initPinBoard = async () => { const initPinBoard = async () => {
// create one workspace with 2 root pages and 2 pivot pages // create one workspace with 2 root pages and 2 pinboard pages
// - hasPivotPage // - hasPinboardPage
// - pivot1 // - hasPinboardPage
// - pivot2 // - pinboard1
// - noPivotPage // - pinboard2
// - noPinboardPage
const mutationHook = renderHook(() => useWorkspacesHelper(), { const mutationHook = renderHook(() => useWorkspacesHelper(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
const rootPageIds = ['hasPivotPage', 'noPivotPage']; const rootPageIds = ['hasPinboardPage', 'noPinboardPage'];
const pivotPageIds = ['pivot1', 'pivot2']; const pinboardPageIds = ['pinboard1', 'pinboard2'];
const id = await mutationHook.result.current.createLocalWorkspace('test0'); const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(workspacesAtom); await store.get(workspacesAtom);
mutationHook.rerender(); mutationHook.rerender();
@@ -59,25 +60,32 @@ const initPinBoard = async () => {
const blockSuiteWorkspace = const blockSuiteWorkspace =
currentWorkspace?.blockSuiteWorkspace as 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 => { rootPageIds.forEach(rootPageId => {
mutationHook.result.current.createWorkspacePage(id, rootPageId); mutationHook.result.current.createWorkspacePage(id, rootPageId);
blockSuiteWorkspace.meta.setPageMeta(rootPageId, { blockSuiteWorkspace.meta.setPageMeta(rootPageId, {
isPivots: true, subpageIds: rootPageId === rootPageIds[0] ? pinboardPageIds : [],
subpageIds: rootPageId === rootPageIds[0] ? pivotPageIds : [],
}); });
}); });
pivotPageIds.forEach(pivotId => { // create children to firs parent
mutationHook.result.current.createWorkspacePage(id, pivotId); pinboardPageIds.forEach(pinboardId => {
blockSuiteWorkspace.meta.setPageMeta(pivotId, { mutationHook.result.current.createWorkspacePage(id, pinboardId);
title: pivotId, blockSuiteWorkspace.meta.setPageMeta(pinboardId, {
title: pinboardId,
}); });
}); });
const App = (props: PivotsProps) => { const App = (props: PinboardProps) => {
return ( return (
<ThemeProvider> <ThemeProvider>
<ProviderWrapper> <ProviderWrapper>
<Pivots {...props} /> <Pinboard {...props} />
</ProviderWrapper> </ProviderWrapper>
</ThemeProvider> </ThemeProvider>
); );
@@ -93,53 +101,51 @@ const initPinBoard = async () => {
return { return {
rootPageIds, rootPageIds,
pivotPageIds, pinboardPageIds,
app, app,
blockSuiteWorkspace, blockSuiteWorkspace,
}; };
}; };
const openOperationMenu = async (app: RenderResult, pageId: string) => { const openOperationMenu = async (app: RenderResult, pageId: string) => {
const rootPivot = await app.findByTestId(`pivot-${pageId}`); const rootPinboard = await app.findByTestId(`pinboard-${pageId}`);
const operationBtn = (await rootPivot.querySelector( const operationBtn = (await rootPinboard.querySelector(
'[data-testid="pivot-operation-button"]' '[data-testid="pinboard-operation-button"]'
)) as HTMLElement; )) as HTMLElement;
await operationBtn.click(); await operationBtn.click();
const menu = await app.findByTestId('pivot-operation-menu'); const menu = await app.findByTestId('pinboard-operation-menu');
expect(menu).toBeInTheDocument(); expect(menu).toBeInTheDocument();
}; };
describe('PinBoard', () => { describe('PinBoard', () => {
test('add pivot', async () => { test('add pinboard', async () => {
const { app, blockSuiteWorkspace, rootPageIds, pivotPageIds } = const { app, blockSuiteWorkspace, rootPageIds } = await initPinBoard();
await initPinBoard(); const [hasChildrenPageId] = rootPageIds;
const [hasPivotPageId] = rootPageIds; await openOperationMenu(app, hasChildrenPageId);
await openOperationMenu(app, hasPivotPageId);
const addBtn = await app.findByTestId('pivot-operation-add'); const addBtn = await app.findByTestId('pinboard-operation-add');
await addBtn.click(); await addBtn.click();
const metas = blockSuiteWorkspace.meta.pageMetas ?? []; const hasChildrenPageMeta =
const rootPageMeta = blockSuiteWorkspace.meta.getPageMeta(hasPivotPageId); blockSuiteWorkspace.meta.getPageMeta(hasChildrenPageId);
const addedPageMeta = metas.find(
meta => !pivotPageIds.includes(meta.id) && !rootPageIds.includes(meta.id)
) as PageMeta;
// Page meta have been added // 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 // 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(); app.unmount();
}); });
test('delete pivot', async () => { test('delete pinboard', async () => {
const { const {
app, app,
blockSuiteWorkspace, blockSuiteWorkspace,
rootPageIds: [hasPivotPageId], rootPageIds: [hasChildrenPageId],
} = await initPinBoard(); } = 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(); await deleteBtn.click();
const confirmBtn = await app.findByTestId('move-to-trash-confirm'); const confirmBtn = await app.findByTestId('move-to-trash-confirm');
@@ -153,77 +159,78 @@ describe('PinBoard', () => {
app.unmount(); app.unmount();
}); });
test('rename pivot', async () => { test('rename pinboard', async () => {
const { const {
app, app,
rootPageIds: [hasPivotPageId], rootPageIds: [hasChildrenPageId],
} = await initPinBoard(); } = 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(); await renameBtn.click();
const input = await app.findByTestId(`pivot-input-${hasPivotPageId}`); const input = await app.findByTestId(`pinboard-input-${hasChildrenPageId}`);
expect(input).toBeInTheDocument(); expect(input).toBeInTheDocument();
// TODO: Fix this test // TODO: Fix this test
// fireEvent.change(input, { target: { value: 'tteesstt' } }); // fireEvent.change(input, { target: { value: 'tteesstt' } });
//
// expect( // expect(
// blockSuiteWorkspace.meta.getPageMeta(rootPageId)?.name // blockSuiteWorkspace.meta.getPageMeta(hasChildrenPageId)?.name
// ).toBe('tteesstt'); // ).toBe('tteesstt');
app.unmount(); app.unmount();
}); });
test('move pivot', async () => { test('move pinboard', async () => {
const { const {
app, app,
blockSuiteWorkspace, blockSuiteWorkspace,
rootPageIds: [hasPivotPageId], rootPageIds: [hasChildrenPageId],
pivotPageIds: [pivotId1, pivotId2], pinboardPageIds: [pinboardId1, pinboardId2],
} = await initPinBoard(); } = 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(); await moveToBtn.click();
const pivotsMenu = await app.findByTestId('pivots-menu'); const pinboardMenu = await app.findByTestId('pinboard-menu');
expect(pivotsMenu).toBeInTheDocument(); expect(pinboardMenu).toBeInTheDocument();
await ( await (
pivotsMenu.querySelector( pinboardMenu.querySelector(
`[data-testid="pivot-${pivotId2}"]` `[data-testid="pinboard-${pinboardId2}"]`
) as HTMLElement ) as HTMLElement
).click(); ).click();
const rootPageMeta = blockSuiteWorkspace.meta.getPageMeta(hasPivotPageId); const hasChildrenPageMeta =
blockSuiteWorkspace.meta.getPageMeta(hasChildrenPageId);
expect(rootPageMeta?.subpageIds.includes(pivotId1)).toBe(false); expect(hasChildrenPageMeta?.subpageIds.includes(pinboardId1)).toBe(false);
expect(rootPageMeta?.subpageIds.includes(pivotId2)).toBe(true); expect(hasChildrenPageMeta?.subpageIds.includes(pinboardId2)).toBe(true);
app.unmount(); app.unmount();
}); });
test('remove from pivots', async () => { test('remove from pinboard', async () => {
const { const {
app, app,
blockSuiteWorkspace, blockSuiteWorkspace,
rootPageIds: [hasPivotPageId], rootPageIds: [hasChildrenPageId],
pivotPageIds: [pivotId1], pinboardPageIds: [pinboardId1],
} = await initPinBoard(); } = 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(); await moveToBtn.click();
const removeFromPivotsBtn = await app.findByTestId( const removeFromPinboardBtn = await app.findByTestId(
'remove-from-pivots-button' 'remove-from-pinboard-button'
); );
removeFromPivotsBtn.click(); removeFromPinboardBtn.click();
const hasPivotsPageMeta = const hasPinboardPageMeta =
blockSuiteWorkspace.meta.getPageMeta(hasPivotPageId); blockSuiteWorkspace.meta.getPageMeta(hasChildrenPageId);
expect(hasPivotsPageMeta?.subpageIds.length).toBe(1); expect(hasPinboardPageMeta?.subpageIds.length).toBe(1);
expect(hasPivotsPageMeta?.subpageIds.includes(pivotId1)).toBe(false); expect(hasPinboardPageMeta?.subpageIds.includes(pinboardId1)).toBe(false);
app.unmount();
}); });
}); });

View File

@@ -16,17 +16,16 @@ export const CopyLink = ({ onItemClick, onSelect }: CommonMenuItemProps) => {
}, [t]); }, [t]);
return ( return (
<> <MenuItem
<MenuItem data-testid="copy-link"
onClick={() => { onClick={() => {
copyUrl(); copyUrl();
onItemClick?.(); onItemClick?.();
onSelect?.(); onSelect?.();
}} }}
icon={<CopyIcon />} icon={<CopyIcon />}
> >
{t('Copy Link')} {t('Copy Link')}
</MenuItem> </MenuItem>
</>
); );
}; };

View File

@@ -2,10 +2,10 @@ import { MenuItem } from '@affine/component';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { ArrowRightSmallIcon, MoveToIcon } from '@blocksuite/icons'; import { ArrowRightSmallIcon, MoveToIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { useRef, useState } from 'react'; import { useMemo, useRef, useState } from 'react';
import type { BlockSuiteWorkspace } from '../../../shared'; import type { BlockSuiteWorkspace } from '../../../shared';
import { PivotsMenu } from '../pivots'; import { PinboardMenu } from '../pinboard';
import type { CommonMenuItemProps } from './types'; import type { CommonMenuItemProps } from './types';
export type MoveToProps = CommonMenuItemProps<{ export type MoveToProps = CommonMenuItemProps<{
@@ -43,14 +43,17 @@ export const MoveTo = ({
> >
{t('Move to')} {t('Move to')}
</MenuItem> </MenuItem>
<PivotsMenu <PinboardMenu
anchorEl={anchorEl} anchorEl={anchorEl}
open={open} open={open}
placement="left-start" placement="left-start"
metas={metas.filter(meta => !meta.trash)} metas={useMemo(
() => metas.filter(m => !m.trash && m.id !== currentMeta.id),
[metas, currentMeta]
)}
currentMeta={currentMeta} currentMeta={currentMeta}
blockSuiteWorkspace={blockSuiteWorkspace} blockSuiteWorkspace={blockSuiteWorkspace}
onPivotClick={onSelect} onPinboardClick={onSelect}
/> />
</> </>
); );

View File

@@ -0,0 +1,2 @@
export * from './pinboard-menu';
export * from './pinboard-render/';

View File

@@ -7,7 +7,7 @@ import Image from 'next/legacy/image';
import React from 'react'; import React from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms'; import { workspacePreferredModeAtom } from '../../../../atoms';
import { StyledMenuSubTitle, StyledPivot } from '../styles'; import { StyledMenuSubTitle, StyledPinboard } from '../styles';
export const SearchContent = ({ export const SearchContent = ({
results, results,
@@ -27,16 +27,16 @@ export const SearchContent = ({
</StyledMenuSubTitle> </StyledMenuSubTitle>
{results.map(meta => { {results.map(meta => {
return ( return (
<StyledPivot <StyledPinboard
key={meta.id} key={meta.id}
onClick={() => { onClick={() => {
onClick?.(meta.id); onClick?.(meta.id);
}} }}
data-testid="pivot-search-result" data-testid="pinboard-search-result"
> >
{record[meta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />} {record[meta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
{meta.title} {meta.title}
</StyledPivot> </StyledPinboard>
); );
})} })}
</> </>

View File

@@ -1,42 +1,41 @@
import type { PureMenuProps } from '@affine/component'; 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 { useTranslation } from '@affine/i18n';
import { RemoveIcon, SearchIcon } from '@blocksuite/icons'; import { RemoveIcon, SearchIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import React, { useCallback, useState } from 'react'; 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 { usePageMetaHelper } from '../../../../hooks/use-page-meta';
import type { BlockSuiteWorkspace } from '../../../../shared'; import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils'; import { toast } from '../../../../utils';
import { usePivotData } from '../hooks/usePivotData'; import { PinboardRender } from '../pinboard-render/';
import { usePivotHandler } from '../hooks/usePivotHandler';
import { PivotRender } from '../pivot-render/PivotRender';
import { import {
StyledMenuContent, StyledMenuContent,
StyledMenuFooter, StyledMenuFooter,
StyledMenuSubTitle, StyledMenuSubTitle,
StyledPivot, StyledPinboard,
StyledSearchContainer, StyledSearchContainer,
} from '../styles'; } from '../styles';
import { Pivots } from './Pivots';
import { SearchContent } from './SearchContent'; import { SearchContent } from './SearchContent';
export type PivotsMenuProps = { export type PinboardMenuProps = {
metas: PageMeta[]; metas: PageMeta[];
currentMeta: PageMeta; currentMeta: PageMeta;
blockSuiteWorkspace: BlockSuiteWorkspace; blockSuiteWorkspace: BlockSuiteWorkspace;
showRemovePivots?: boolean; showRemovePinboard?: boolean;
onPivotClick?: (p: { dragId: string; dropId: string }) => void; onPinboardClick?: (p: { dragId: string; dropId: string }) => void;
} & PureMenuProps; } & PureMenuProps;
export const PivotsMenu = ({ export const PinboardMenu = ({
metas, metas,
currentMeta, currentMeta,
blockSuiteWorkspace, blockSuiteWorkspace,
showRemovePivots = false, showRemovePinboard = false,
onPivotClick, onPinboardClick,
...pureMenuProps ...pureMenuProps
}: PivotsMenuProps) => { }: PinboardMenuProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
@@ -46,7 +45,7 @@ export const PivotsMenu = ({
meta => !meta.trash && meta.title.includes(query) meta => !meta.trash && meta.title.includes(query)
); );
const { handleDrop } = usePivotHandler({ const { handleDrop } = usePinboardHandler({
blockSuiteWorkspace, blockSuiteWorkspace,
metas, metas,
}); });
@@ -60,15 +59,15 @@ export const PivotsMenu = ({
topLine: false, topLine: false,
internal: true, internal: true,
}); });
onPivotClick?.({ dragId: currentMeta.id, dropId }); onPinboardClick?.({ dragId: currentMeta.id, dropId });
toast(`Moved "${currentMeta.title}" to "${targetTitle}"`); 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, metas,
pivotRender: PivotRender, pinboardRender: PinboardRender,
blockSuiteWorkspace, blockSuiteWorkspace,
onClick: (e, node) => { onClick: (e, node) => {
handleClick(node.id); handleClick(node.id);
@@ -80,7 +79,7 @@ export const PivotsMenu = ({
width={320} width={320}
height={480} height={480}
{...pureMenuProps} {...pureMenuProps}
data-testid="pivots-menu" data-testid="pinboard-menu"
> >
<StyledSearchContainer> <StyledSearchContainer>
<label> <label>
@@ -93,7 +92,7 @@ export const PivotsMenu = ({
height={32} height={32}
noBorder={true} noBorder={true}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
data-testid="pivots-menu-search" data-testid="pinboard-menu-search"
/> />
</StyledSearchContainer> </StyledSearchContainer>
@@ -104,21 +103,16 @@ export const PivotsMenu = ({
{!isSearching && ( {!isSearching && (
<> <>
<StyledMenuSubTitle>Suggested</StyledMenuSubTitle> <StyledMenuSubTitle>Suggested</StyledMenuSubTitle>
<Pivots <TreeView data={data} indent={16} enableDnd={false} />
data={data}
blockSuiteWorkspace={blockSuiteWorkspace}
currentMeta={currentMeta}
/>
</> </>
)} )}
</StyledMenuContent> </StyledMenuContent>
{showRemovePivots && ( {showRemovePinboard && (
<StyledMenuFooter> <StyledMenuFooter>
<StyledPivot <StyledPinboard
data-testid={'remove-from-pivots-button'} data-testid={'remove-from-pinboard-button'}
onClick={() => { onClick={() => {
setPageMeta(currentMeta.id, { isPivots: false });
const parentMeta = metas.find(m => const parentMeta = metas.find(m =>
m.subpageIds.includes(currentMeta.id) m.subpageIds.includes(currentMeta.id)
); );
@@ -132,11 +126,13 @@ export const PivotsMenu = ({
}} }}
> >
<RemoveIcon /> <RemoveIcon />
{t('Remove from Pivots')} {t('Remove from Pinboard')}
</StyledPivot> </StyledPinboard>
<p>{t('RFP')}</p> <p>{t('RFP')}</p>
</StyledMenuFooter> </StyledMenuFooter>
)} )}
</PureMenu> </PureMenu>
); );
}; };
export default PinboardMenu;

View File

@@ -0,0 +1,14 @@
import { useTranslation } from '@affine/i18n';
import { StyledPinboard } from '../styles';
export const EmptyItem = () => {
const { t } = useTranslation();
return (
<StyledPinboard disable={true} style={{ paddingLeft: '32px' }}>
{t('No item')}
</StyledPinboard>
);
};
export default EmptyItem;

View File

@@ -14,10 +14,11 @@ import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
import type { BlockSuiteWorkspace } from '../../../../shared'; import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils'; import { toast } from '../../../../utils';
import { CopyLink, MoveToTrash } from '../../operation-menu-items'; import { CopyLink, MoveToTrash } from '../../operation-menu-items';
import { PivotsMenu } from '../PivotsMenu/PivotsMenu'; import { PinboardMenu } from '../pinboard-menu/';
import { StyledOperationButton } from '../styles'; import { StyledOperationButton } from '../styles';
export type OperationButtonProps = { export type OperationButtonProps = {
isRoot: boolean;
onAdd: () => void; onAdd: () => void;
onDelete: () => void; onDelete: () => void;
metas: PageMeta[]; metas: PageMeta[];
@@ -28,6 +29,7 @@ export type OperationButtonProps = {
onMenuClose?: () => void; onMenuClose?: () => void;
}; };
export const OperationButton = ({ export const OperationButton = ({
isRoot,
onAdd, onAdd,
onDelete, onDelete,
metas, metas,
@@ -44,7 +46,7 @@ export const OperationButton = ({
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [operationMenuOpen, setOperationMenuOpen] = useState(false); const [operationMenuOpen, setOperationMenuOpen] = useState(false);
const [pivotsMenuOpen, setPivotsMenuOpen] = useState(false); const [pinboardMenuOpen, setPinboardMenuOpen] = useState(false);
const [confirmModalOpen, setConfirmModalOpen] = useState(false); const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const menuIndex = useMemo(() => modalIndex + 1, [modalIndex]); const menuIndex = useMemo(() => modalIndex + 1, [modalIndex]);
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
@@ -53,7 +55,7 @@ export const OperationButton = ({
<MuiClickAwayListener <MuiClickAwayListener
onClickAway={() => { onClickAway={() => {
setOperationMenuOpen(false); setOperationMenuOpen(false);
setPivotsMenuOpen(false); setPinboardMenuOpen(false);
}} }}
> >
<div <div
@@ -62,11 +64,11 @@ export const OperationButton = ({
}} }}
onMouseLeave={() => { onMouseLeave={() => {
setOperationMenuOpen(false); setOperationMenuOpen(false);
setPivotsMenuOpen(false); setPinboardMenuOpen(false);
}} }}
> >
<StyledOperationButton <StyledOperationButton
data-testid="pivot-operation-button" data-testid="pinboard-operation-button"
ref={ref => setAnchorEl(ref)} ref={ref => setAnchorEl(ref)}
size="small" size="small"
onClick={() => { onClick={() => {
@@ -78,7 +80,7 @@ export const OperationButton = ({
</StyledOperationButton> </StyledOperationButton>
<PureMenu <PureMenu
data-testid="pivot-operation-menu" data-testid="pinboard-operation-menu"
width={256} width={256}
anchorEl={anchorEl} anchorEl={anchorEl}
open={operationMenuOpen} open={operationMenuOpen}
@@ -86,7 +88,7 @@ export const OperationButton = ({
zIndex={menuIndex} zIndex={menuIndex}
> >
<MenuItem <MenuItem
data-testid="pivot-operation-add" data-testid="pinboard-operation-add"
onClick={() => { onClick={() => {
onAdd(); onAdd();
setOperationMenuOpen(false); setOperationMenuOpen(false);
@@ -96,47 +98,53 @@ export const OperationButton = ({
> >
{t('Add a subpage inside')} {t('Add a subpage inside')}
</MenuItem> </MenuItem>
<MenuItem {!isRoot && (
data-testid="pivot-operation-move-to" <MenuItem
onClick={() => { data-testid="pinboard-operation-move-to"
setOperationMenuOpen(false); onClick={() => {
setPivotsMenuOpen(true); setOperationMenuOpen(false);
}} setPinboardMenuOpen(true);
icon={<MoveToIcon />} }}
> icon={<MoveToIcon />}
{t('Move to')} >
</MenuItem> {t('Move to')}
<MenuItem </MenuItem>
data-testid="pivot-operation-rename" )}
onClick={() => { {!isRoot && (
onRename?.(); <MenuItem
setOperationMenuOpen(false); data-testid="pinboard-operation-rename"
onMenuClose?.(); onClick={() => {
}} onRename?.();
icon={<PenIcon />} setOperationMenuOpen(false);
> onMenuClose?.();
{t('Rename')} }}
</MenuItem> icon={<PenIcon />}
<MoveToTrash >
testId="pivot-operation-move-to-trash" {t('Rename')}
onItemClick={() => { </MenuItem>
setOperationMenuOpen(false); )}
setConfirmModalOpen(true); {!isRoot && (
onMenuClose?.(); <MoveToTrash
}} testId="pinboard-operation-move-to-trash"
/> onItemClick={() => {
setOperationMenuOpen(false);
setConfirmModalOpen(true);
onMenuClose?.();
}}
/>
)}
<CopyLink /> <CopyLink />
</PureMenu> </PureMenu>
<PivotsMenu <PinboardMenu
anchorEl={anchorEl} anchorEl={anchorEl}
open={pivotsMenuOpen} open={pinboardMenuOpen}
placement="bottom-start" placement="bottom-start"
zIndex={menuIndex} zIndex={menuIndex}
metas={metas} metas={metas}
currentMeta={currentMeta} currentMeta={currentMeta}
blockSuiteWorkspace={blockSuiteWorkspace} blockSuiteWorkspace={blockSuiteWorkspace}
showRemovePivots={true} showRemovePinboard={true}
/> />
<MoveToTrash.ConfirmModal <MoveToTrash.ConfirmModal
open={confirmModalOpen} open={confirmModalOpen}

View File

@@ -0,0 +1,122 @@
import { Input } from '@affine/component';
import {
ArrowDownSmallIcon,
EdgelessIcon,
PageIcon,
PivotsIcon,
} from '@blocksuite/icons';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import { useMemo, useState } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import type { PinboardNode } from '../../../../hooks/affine/use-pinboard-data';
import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
import { StyledCollapsedButton, StyledPinboard } from '../styles';
import EmptyItem from './EmptyItem';
import { OperationButton } from './OperationButton';
const getIcon = (type: 'root' | 'edgeless' | 'page') => {
switch (type) {
case 'root':
return <PivotsIcon />;
case 'edgeless':
return <EdgelessIcon />;
default:
return <PageIcon />;
}
};
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 (
<>
<StyledPinboard
data-testid={`pinboard-${node.id}`}
onClick={e => {
onClick?.(e, node);
}}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
isOver={isOver || isSelected}
active={active}
>
<StyledCollapsedButton
collapse={collapsed}
show={!!node.children?.length}
onClick={e => {
e.stopPropagation();
setCollapsed(node.id, !collapsed);
}}
>
<ArrowDownSmallIcon />
</StyledCollapsedButton>
{getIcon(isRoot ? 'root' : record[node.id])}
{showRename ? (
<Input
data-testid={`pinboard-input-${node.id}`}
value={currentMeta.title || ''}
placeholder="Untitled"
onClick={e => 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);
}}
/>
) : (
<span>{isRoot ? 'Pinboard' : currentMeta.title || 'Untitled'}</span>
)}
{showOperationButton && (
<OperationButton
isRoot={isRoot}
onAdd={onAdd}
onDelete={onDelete}
metas={metas}
currentMeta={currentMeta!}
blockSuiteWorkspace={blockSuiteWorkspace!}
isHover={isHover}
onMenuClose={() => setIsHover(false)}
onRename={() => {
setShowRename(true);
setIsHover(false);
}}
/>
)}
</StyledPinboard>
{useMemo(
() =>
isRoot &&
!metas.find(m => (currentMeta.subpageIds ?? []).includes(m.id)),
[currentMeta.subpageIds, isRoot, metas]
) && <EmptyItem />}
</>
);
};
export default PinboardRender;

View File

@@ -21,14 +21,14 @@ export const StyledCollapsedButton = styled('button')<{
margin: 'auto', margin: 'auto',
color: theme.colors.iconColor, color: theme.colors.iconColor,
opacity: '.6', opacity: '.6',
display: show ? 'block' : 'none', display: show ? 'flex' : 'none',
svg: { svg: {
transform: `rotate(${collapse ? '0' : '-90'}deg)`, transform: `rotate(${collapse ? '0' : '-90'}deg)`,
}, },
}; };
}); });
export const StyledPivot = styled('div')<{ export const StyledPinboard = styled('div')<{
disable?: boolean; disable?: boolean;
active?: boolean; active?: boolean;
isOver?: boolean; isOver?: boolean;

View File

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

View File

@@ -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<NodeRenderProps>['data'];
} & Pick<PivotsMenuProps, 'blockSuiteWorkspace' | 'currentMeta'>;
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 (
<>
<StyledPivot
data-testid="root-pivot-button-in-pivots-menu"
id="root-pivot-button-in-pivots-menu"
onClick={() => {
setPageMeta(currentMeta.id, { isPivots: true });
toast(`Moved "${currentMeta.title}" to Pivots`);
}}
>
<StyledCollapsedButton
onClick={useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setShowPivot(!showPivot);
},
[showPivot]
)}
collapse={showPivot}
>
<ArrowDownSmallIcon />
</StyledCollapsedButton>
<PivotsIcon />
{t('Pivots')}
</StyledPivot>
<MuiCollapse
in={showPivot}
style={{
maxHeight: 300,
paddingLeft: '16px',
overflowY: 'auto',
}}
>
{isPivotEmpty ? (
<EmptyItem />
) : (
<TreeView data={data} indent={16} enableDnd={false} />
)}
</MuiCollapse>
</>
);
};
export default Pivots;

View File

@@ -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<TreeNode[]>((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;

View File

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

View File

@@ -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 (
<StyledPivot
data-testid={`pivot-${node.id}`}
onClick={e => {
onClick?.(e, node);
}}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
isOver={isOver || isSelected}
active={active}
>
<StyledCollapsedButton
collapse={collapsed}
show={!!node.children?.length}
onClick={e => {
e.stopPropagation();
setCollapsed(node.id, !collapsed);
}}
>
<ArrowDownSmallIcon />
</StyledCollapsedButton>
{record[node.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
{showRename ? (
<Input
data-testid={`pivot-input-${node.id}`}
value={currentMeta?.title ?? ''}
placeholder="Untitled"
onClick={e => 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);
}}
/>
) : (
<span>{currentMeta?.title || 'Untitled'}</span>
)}
{showOperationButton && (
<OperationButton
onAdd={onAdd}
onDelete={onDelete}
metas={metas}
currentMeta={currentMeta!}
blockSuiteWorkspace={blockSuiteWorkspace!}
isHover={isHover}
onMenuClose={() => setIsHover(false)}
onRename={() => {
setShowRename(true);
setIsHover(false);
}}
/>
)}
</StyledPivot>
);
};

View File

@@ -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<HTMLDivElement>, node: TreeNode) => void;
showOperationButton?: boolean;
};
export type NodeRenderProps = RenderProps & {
metas: PageMeta[];
currentMeta: PageMeta;
};
export type TreeNode = Node<NodeRenderProps>;

View File

@@ -44,6 +44,7 @@ export const OperationCell: React.FC<OperationCellProps> = ({
const { id, favorite } = pageMeta; const { id, favorite } = pageMeta;
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const OperationMenu = ( const OperationMenu = (
<> <>
<MenuItem <MenuItem
@@ -65,20 +66,24 @@ export const OperationCell: React.FC<OperationCellProps> = ({
> >
{t('Open in new tab')} {t('Open in new tab')}
</MenuItem> </MenuItem>
<MoveTo {!pageMeta.isRootPinboard && (
metas={metas} <MoveTo
currentMeta={pageMeta} metas={metas}
blockSuiteWorkspace={blockSuiteWorkspace} currentMeta={pageMeta}
/> blockSuiteWorkspace={blockSuiteWorkspace}
<MenuItem />
data-testid="move-to-trash" )}
onClick={() => { {!pageMeta.isRootPinboard && (
setOpen(true); <MenuItem
}} data-testid="move-to-trash"
icon={<DeleteTemporarilyIcon />} onClick={() => {
> setOpen(true);
{t('Move to Trash')} }}
</MenuItem> icon={<DeleteTemporarilyIcon />}
>
{t('Move to Trash')}
</MenuItem>
)}
</> </>
); );
return ( return (
@@ -90,7 +95,7 @@ export const OperationCell: React.FC<OperationCellProps> = ({
disablePortal={true} disablePortal={true}
trigger="click" trigger="click"
> >
<IconButton> <IconButton data-testid="page-list-operation-button">
<MoreVerticalIcon /> <MoreVerticalIcon />
</IconButton> </IconButton>
</Menu> </Menu>

View File

@@ -82,17 +82,21 @@ export const EditorOptionMenu = () => {
{mode === 'page' ? t('Edgeless') : t('Page')} {mode === 'page' ? t('Edgeless') : t('Page')}
</MenuItem> </MenuItem>
<Export /> <Export />
<MoveTo {!pageMeta.isRootPinboard && (
metas={allMetas} <MoveTo
currentMeta={pageMeta} metas={allMetas}
blockSuiteWorkspace={blockSuiteWorkspace} currentMeta={pageMeta}
/> blockSuiteWorkspace={blockSuiteWorkspace}
<MoveToTrash />
testId="editor-option-menu-delete" )}
onItemClick={() => { {!pageMeta.isRootPinboard && (
setOpenConfirm(true); <MoveToTrash
}} testId="editor-option-menu-delete"
/> onItemClick={() => {
setOpenConfirm(true);
}}
/>
)}
</> </>
); );

View File

@@ -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<HTMLDivElement>, 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 (
<div data-testid="sidebar-pinboard-container">
<TreeView
data={data}
onAdd={handleAdd}
onDelete={handleDelete}
onDrop={handleDrop}
indent={16}
/>
</div>
);
};
export default Pinboard;

View File

@@ -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<HTMLDivElement>, node: TreeNode) => {
openPage(node.id);
},
[openPage]
);
const onAdd = useCallback(
(id: string) => {
openPage(id);
},
[openPage]
);
const { data } = usePivotData({
metas: allMetas.filter(meta => !meta.trash),
pivotRender: PivotRender,
blockSuiteWorkspace: blockSuiteWorkspace,
onClick: handlePivotClick,
showOperationButton: true,
});
const { handleAdd, handleDelete, handleDrop } = usePivotHandler({
blockSuiteWorkspace: blockSuiteWorkspace,
metas: allMetas,
onAdd,
});
return (
<TreeView
data={data}
onAdd={handleAdd}
onDelete={handleDelete}
onDrop={handleDrop}
indent={16}
/>
);
};
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 (
<div data-testid="sidebar-pivots-container">
<StyledListItem
onClick={useCallback(() => {
setShowPivot(!showPivot);
}, [showPivot])}
>
<StyledCollapseButton collapse={showPivot}>
<ArrowDownSmallIcon />
</StyledCollapseButton>
<PivotsIcon />
{t('Pivots')}
</StyledListItem>
<MuiCollapse in={showPivot} style={{ paddingLeft: '16px' }}>
{isPivotEmpty ? (
<EmptyItem />
) : (
<PivotInternal
blockSuiteWorkspace={blockSuiteWorkspace}
openPage={openPage}
allMetas={metas}
/>
)}
</MuiCollapse>
</div>
);
};
export default Pivots;

View File

@@ -7,8 +7,9 @@ import {
useGuideHidden, useGuideHidden,
useGuideHiddenUntilNextUpdate, useGuideHiddenUntilNextUpdate,
} from '../../../../hooks/affine/use-is-first-load'; } from '../../../../hooks/affine/use-is-first-load';
import { StyledChangeLog, StyledChangeLogWarper } from '../shared-styles'; import { StyledChangeLog, StyledChangeLogWrapper } from '../shared-styles';
import { StyledLink } from '../style'; import { StyledLink } from '../style';
export const ChangeLog = () => { export const ChangeLog = () => {
const [guideHidden, setGuideHidden] = useGuideHidden(); const [guideHidden, setGuideHidden] = useGuideHidden();
const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] = const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] =
@@ -33,7 +34,7 @@ export const ChangeLog = () => {
return <></>; return <></>;
} }
return ( return (
<StyledChangeLogWarper isClose={isClose}> <StyledChangeLogWrapper isClose={isClose}>
<StyledChangeLog data-testid="change-log" isClose={isClose}> <StyledChangeLog data-testid="change-log" isClose={isClose}>
<StyledLink href={'https://affine.pro'} target="_blank"> <StyledLink href={'https://affine.pro'} target="_blank">
<NewIcon /> <NewIcon />
@@ -49,7 +50,7 @@ export const ChangeLog = () => {
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
</StyledChangeLog> </StyledChangeLog>
</StyledChangeLogWarper> </StyledChangeLogWrapper>
); );
}; };

View File

@@ -22,7 +22,7 @@ import type { AllWorkspace } from '../../../shared';
import { SidebarSwitch } from '../../affine/sidebar-switch'; import { SidebarSwitch } from '../../affine/sidebar-switch';
import { ChangeLog } from './changeLog'; import { ChangeLog } from './changeLog';
import Favorite from './favorite'; import Favorite from './favorite';
import { Pivots } from './Pivots'; import { Pinboard } from './Pinboard';
import { StyledListItem } from './shared-styles'; import { StyledListItem } from './shared-styles';
import { import {
StyledLink, StyledLink,
@@ -188,7 +188,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
currentWorkspace={currentWorkspace} currentWorkspace={currentWorkspace}
/> />
{!!blockSuiteWorkspace && ( {!!blockSuiteWorkspace && (
<Pivots <Pinboard
blockSuiteWorkspace={blockSuiteWorkspace} blockSuiteWorkspace={blockSuiteWorkspace}
openPage={openPage} openPage={openPage}
allMetas={pageMeta} allMetas={pageMeta}

View File

@@ -53,7 +53,7 @@ export const StyledCollapseButton = styled('button')<{
margin: 'auto', margin: 'auto',
color: theme.colors.iconColor, color: theme.colors.iconColor,
opacity: '.6', opacity: '.6',
display: show ? 'block' : 'none', display: show ? 'flex' : 'none',
svg: { svg: {
transform: `rotate(${collapse ? '0' : '-90'}deg)`, transform: `rotate(${collapse ? '0' : '-90'}deg)`,
}, },
@@ -188,7 +188,7 @@ export const StyledChangeLog = styled('div')<{
}, },
}; };
}); });
export const StyledChangeLogWarper = styled('div')<{ export const StyledChangeLogWrapper = styled('div')<{
isClose?: boolean; isClose?: boolean;
}>(({ isClose }) => { }>(({ isClose }) => {
return { return {

View File

@@ -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<HTMLDivElement>, node: PinboardNode) => void;
showOperationButton?: boolean;
};
export type NodeRenderProps = RenderProps & {
metas: PageMeta[];
currentMeta: PageMeta;
};
export type PinboardNode = Node<NodeRenderProps>;
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<PinboardNode[]>(
(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;

View File

@@ -4,21 +4,21 @@ import type { PageMeta } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useBlockSuiteWorkspaceHelper } from '../../../../hooks/use-blocksuite-workspace-helper'; import type { BlockSuiteWorkspace } from '../../shared';
import { usePageMetaHelper } from '../../../../hooks/use-page-meta'; import { useBlockSuiteWorkspaceHelper } from '../use-blocksuite-workspace-helper';
import type { BlockSuiteWorkspace } from '../../../../shared'; import { usePageMetaHelper } from '../use-page-meta';
import type { NodeRenderProps, TreeNode } from '../types'; 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)); const parentMeta = metas.find(m => m.subpageIds?.includes(id));
if (!parentMeta) { if (!parentMeta) {
return [id]; return [id];
} }
return [parentMeta.id, ...findRootIds(metas, parentMeta.id)]; return [parentMeta.id, ...findRootIds(metas, parentMeta.id)];
}; }
export const usePivotHandler = ({ export function usePinboardHandler({
blockSuiteWorkspace, blockSuiteWorkspace,
metas, metas,
onAdd, onAdd,
@@ -30,13 +30,12 @@ export const usePivotHandler = ({
onAdd?: (addedId: string, parentId: string) => void; onAdd?: (addedId: string, parentId: string) => void;
onDelete?: TreeViewProps<NodeRenderProps>['onDelete']; onDelete?: TreeViewProps<NodeRenderProps>['onDelete'];
onDrop?: TreeViewProps<NodeRenderProps>['onDrop']; onDrop?: TreeViewProps<NodeRenderProps>['onDrop'];
}) => { }) {
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace); const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { getPageMeta, setPageMeta, shiftPageMeta } = const { getPageMeta, setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
usePageMetaHelper(blockSuiteWorkspace);
const handleAdd = useCallback( const handleAdd = useCallback(
(node: TreeNode) => { (node: PinboardNode) => {
const id = nanoid(); const id = nanoid();
createPage(id, node.id); createPage(id, node.id);
onAdd?.(id, node.id); onAdd?.(id, node.id);
@@ -45,7 +44,7 @@ export const usePivotHandler = ({
); );
const handleDelete = useCallback( const handleDelete = useCallback(
(node: TreeNode) => { (node: PinboardNode) => {
const removeToTrash = (currentMeta: PageMeta) => { const removeToTrash = (currentMeta: PageMeta) => {
const { subpageIds = [] } = currentMeta; const { subpageIds = [] } = currentMeta;
setPageMeta(currentMeta.id, { setPageMeta(currentMeta.id, {
@@ -80,91 +79,57 @@ export const usePivotHandler = ({
if (dropRootIds.includes(dragId)) { if (dropRootIds.includes(dragId)) {
return; return;
} }
const { topLine, bottomLine } = position;
logger.info('handleDrop', { logger.info('handleDrop', {
dragId, dragId,
dropId, dropId,
bottomLine, position,
metas, metas,
}); });
const { topLine, bottomLine } = position;
const dragParentMeta = metas.find(meta => const dragParentMeta = metas.find(meta =>
meta.subpageIds?.includes(dragId) meta.subpageIds?.includes(dragId)
); );
if (bottomLine || topLine) { if (bottomLine || topLine) {
const insertOffset = bottomLine ? 1 : 0; const insertOffset = bottomLine ? 1 : 0;
const dropParentMeta = metas.find(m => m.subpageIds?.includes(dropId)); const dropParentMeta = metas.find(m => m.subpageIds?.includes(dropId));
if (dropParentMeta?.id === dragParentMeta?.id) {
if (!dropParentMeta) { // same parent
// drop into root const newSubpageIds = [...(dragParentMeta?.subpageIds ?? [])];
logger.info('drop into root and resort');
if (dragParentMeta) {
const newSubpageIds = [...(dragParentMeta.subpageIds ?? [])];
const deleteIndex = dragParentMeta.subpageIds?.findIndex(
id => id === dragId
);
newSubpageIds.splice(deleteIndex, 1);
setPageMeta(dragParentMeta.id, {
subpageIds: newSubpageIds,
});
}
logger.info('resort root meta');
const insertIndex =
metas.findIndex(m => m.id === dropId) + insertOffset;
shiftPageMeta(dragId, insertIndex);
return onDrop?.(dragId, dropId, position);
}
if (
dragParentMeta &&
(dragParentMeta.id === dropId ||
dragParentMeta.id === dropParentMeta!.id)
) {
logger.info('drop to resort');
// need to resort
const newSubpageIds = [...(dragParentMeta.subpageIds ?? [])];
const deleteIndex = newSubpageIds.findIndex(id => id === dragId); const deleteIndex = newSubpageIds.findIndex(id => id === dragId);
newSubpageIds.splice(deleteIndex, 1); newSubpageIds.splice(deleteIndex, 1);
const insertIndex = const insertIndex =
newSubpageIds.findIndex(id => id === dropId) + insertOffset; newSubpageIds.findIndex(id => id === dropId) + insertOffset;
newSubpageIds.splice(insertIndex, 0, dragId); newSubpageIds.splice(insertIndex, 0, dragId);
setPageMeta(dropParentMeta.id, { dragParentMeta &&
subpageIds: newSubpageIds, setPageMeta(dragParentMeta.id, {
}); subpageIds: newSubpageIds,
});
return onDrop?.(dragId, dropId, position); 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'); const newDropParentSubpageIds = [...(dropParentMeta?.subpageIds ?? [])];
const insertIndex =
if (dragParentMeta) { newDropParentSubpageIds.findIndex(id => id === dropId) + insertOffset;
const metaIndex = dragParentMeta.subpageIds.findIndex( newDropParentSubpageIds.splice(insertIndex, 0, dragId);
id => id === dragId dragParentMeta &&
);
const newSubpageIds = [...dragParentMeta.subpageIds];
newSubpageIds.splice(metaIndex, 1);
setPageMeta(dragParentMeta.id, { 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); return onDrop?.(dragId, dropId, position);
} }
logger.info('drop into the drop node');
// drop into the node // drop into the node
if (dragParentMeta && dragParentMeta.id === dropId) { if (dragParentMeta && dragParentMeta.id === dropId) {
return; return;
@@ -185,7 +150,7 @@ export const usePivotHandler = ({
subpageIds: newSubpageIds, subpageIds: newSubpageIds,
}); });
}, },
[metas, onDrop, setPageMeta, shiftPageMeta] [metas, onDrop, setPageMeta]
); );
return { return {
@@ -193,6 +158,6 @@ export const usePivotHandler = ({
handleAdd, handleAdd,
handleDelete, handleDelete,
}; };
}; }
export default usePivotHandler; export default usePinboardHandler;

View File

@@ -13,8 +13,7 @@ declare module '@blocksuite/store' {
trashDate?: number; trashDate?: number;
// whether to create the page with the default template // whether to create the page with the default template
init?: boolean; init?: boolean;
// use for subpage isRootPinboard?: boolean;
isPivots?: boolean;
} }
} }
@@ -44,10 +43,13 @@ export function usePageMeta(
return pageMeta; return pageMeta;
} }
export function usePageMetaHelper(blockSuiteWorkspace: BlockSuiteWorkspace) { export function usePageMetaHelper(
blockSuiteWorkspace: BlockSuiteWorkspace | null
) {
return useMemo( return useMemo(
() => ({ () => ({
setPageTitle: (pageId: string, newTitle: string) => { setPageTitle: (pageId: string, newTitle: string) => {
assertExists(blockSuiteWorkspace);
const page = blockSuiteWorkspace.getPage(pageId); const page = blockSuiteWorkspace.getPage(pageId);
assertExists(page); assertExists(page);
const pageBlock = page const pageBlock = page
@@ -58,15 +60,19 @@ export function usePageMetaHelper(blockSuiteWorkspace: BlockSuiteWorkspace) {
pageBlock.title.delete(0, pageBlock.title.length); pageBlock.title.delete(0, pageBlock.title.length);
pageBlock.title.insert(newTitle, 0); pageBlock.title.insert(newTitle, 0);
}); });
assertExists(blockSuiteWorkspace);
blockSuiteWorkspace.meta.setPageMeta(pageId, { title: newTitle }); blockSuiteWorkspace.meta.setPageMeta(pageId, { title: newTitle });
}, },
setPageMeta: (pageId: string, pageMeta: Partial<PageMeta>) => { setPageMeta: (pageId: string, pageMeta: Partial<PageMeta>) => {
assertExists(blockSuiteWorkspace);
blockSuiteWorkspace.meta.setPageMeta(pageId, pageMeta); blockSuiteWorkspace.meta.setPageMeta(pageId, pageMeta);
}, },
getPageMeta: (pageId: string) => { getPageMeta: (pageId: string) => {
assertExists(blockSuiteWorkspace);
return blockSuiteWorkspace.meta.getPageMeta(pageId); return blockSuiteWorkspace.meta.getPageMeta(pageId);
}, },
shiftPageMeta: (pageId: string, index: number) => { shiftPageMeta: (pageId: string, index: number) => {
assertExists(blockSuiteWorkspace);
return blockSuiteWorkspace.meta.shiftPageMeta(pageId, index); return blockSuiteWorkspace.meta.shiftPageMeta(pageId, index);
}, },
}), }),

View File

@@ -19,6 +19,7 @@ import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-route
import { WorkspaceLayout } from '../../../layouts'; import { WorkspaceLayout } from '../../../layouts';
import { WorkspacePlugins } from '../../../plugins'; import { WorkspacePlugins } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared'; import type { NextPageWithLayout } from '../../../shared';
import { ensureRootPinboard } from '../../../utils';
const AllPage: NextPageWithLayout = () => { const AllPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
@@ -35,6 +36,8 @@ const AllPage: NextPageWithLayout = () => {
} }
if (currentWorkspace.flavour !== WorkspaceFlavour.LOCAL) { if (currentWorkspace.flavour !== WorkspaceFlavour.LOCAL) {
// only create a new page for local workspace // only create a new page for local workspace
// just ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
return; return;
} }
const localProvider = currentWorkspace.providers.find( const localProvider = currentWorkspace.providers.find(
@@ -53,6 +56,8 @@ const AllPage: NextPageWithLayout = () => {
}); });
jumpToPage(currentWorkspace.id, pageId); jumpToPage(currentWorkspace.id, pageId);
} }
// no matter workspace is empty, ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
}; };
provider.callbacks.add(callback); provider.callbacks.add(callback);
return () => { return () => {

View File

@@ -1,8 +1,10 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import markdown from '@affine/templates/Welcome-to-AFFiNE.md'; import markdown from '@affine/templates/Welcome-to-AFFiNE.md';
import { ContentParser } from '@blocksuite/blocks/content-parser'; import { ContentParser } from '@blocksuite/blocks/content-parser';
import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import type { BlockSuiteWorkspace } from '../shared';
const demoTitle = markdown const demoTitle = markdown
.split('\n') .split('\n')
@@ -15,7 +17,7 @@ const demoText = markdown.split('\n').slice(1).join('\n');
const logger = new DebugLogger('init-page'); const logger = new DebugLogger('init-page');
export function initPage(page: Page, editor: Readonly<EditorContainer>): void { export function initPage(page: Page): void {
logger.debug('initEmptyPage', page.id); logger.debug('initEmptyPage', page.id);
// Add page block and surface block at root level // Add page block and surface block at root level
const isFirstPage = page.meta.init === true; const isFirstPage = page.meta.init === true;
@@ -23,26 +25,23 @@ export function initPage(page: Page, editor: Readonly<EditorContainer>): void {
page.workspace.setPageMeta(page.id, { page.workspace.setPageMeta(page.id, {
init: false, init: false,
}); });
_initPageWithDemoMarkdown(page, editor); _initPageWithDemoMarkdown(page);
} else { } else {
_initEmptyPage(page, editor); _initEmptyPage(page);
} }
page.resetHistory(); page.resetHistory();
} }
export function _initEmptyPage(page: Page, _: Readonly<EditorContainer>) { export function _initEmptyPage(page: Page, title?: string): void {
const pageBlockId = page.addBlock('affine:page', { const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(''), title: new page.Text(title ?? ''),
}); });
page.addBlock('affine:surface', {}, null); page.addBlock('affine:surface', {}, null);
const frameId = page.addBlock('affine:frame', {}, pageBlockId); const frameId = page.addBlock('affine:frame', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId); page.addBlock('affine:paragraph', {}, frameId);
} }
export function _initPageWithDemoMarkdown( export function _initPageWithDemoMarkdown(page: Page): void {
page: Page,
editor: Readonly<EditorContainer>
): void {
logger.debug('initPageWithDefaultMarkdown', page.id); logger.debug('initPageWithDefaultMarkdown', page.id);
const pageBlockId = page.addBlock('affine:page', { const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(demoTitle), title: new page.Text(demoTitle),
@@ -63,3 +62,25 @@ export function _initPageWithDemoMarkdown(
}); });
page.workspace.setPageMeta(page.id, { demoTitle }); 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;
}

View File

@@ -144,11 +144,12 @@ export const TreeNode = <RenderProps,>({
}: TreeNodeProps<RenderProps>) => { }: TreeNodeProps<RenderProps>) => {
const { indent, enableDnd, collapsedIds } = otherProps; const { indent, enableDnd, collapsedIds } = otherProps;
const collapsed = collapsedIds.includes(node.id); const collapsed = collapsedIds.includes(node.id);
const { renderTopLine = true, renderBottomLine = true } = node;
return ( return (
<StyledTreeNodeContainer ref={dragRef} isDragging={isDragging}> <StyledTreeNodeContainer ref={dragRef} isDragging={isDragging}>
<StyledTreeNodeWrapper> <StyledTreeNodeWrapper>
{enableDnd && index === 0 && ( {enableDnd && renderTopLine && index === 0 && (
<NodeLine <NodeLine
node={node} node={node}
{...otherProps} {...otherProps}
@@ -174,13 +175,15 @@ export const TreeNode = <RenderProps,>({
/> />
)} )}
{enableDnd && (!node.children?.length || collapsed) && ( {enableDnd &&
<NodeLine renderBottomLine &&
node={node} (!node.children?.length || collapsed) && (
{...otherProps} <NodeLine
allowDrop={!isDragging && allowDrop} node={node}
/> {...otherProps}
)} allowDrop={!isDragging && allowDrop}
/>
)}
</StyledTreeNodeWrapper> </StyledTreeNodeWrapper>
<StyledCollapse in={!collapsed} indent={indent}> <StyledCollapse in={!collapsed} indent={indent}>
{node.children && {node.children &&

View File

@@ -26,6 +26,8 @@ export type Node<RenderProps = unknown> = {
}, },
renderProps?: RenderProps renderProps?: RenderProps
) => ReactNode; ) => ReactNode;
renderTopLine?: boolean;
renderBottomLine?: boolean;
}; };
type CommonProps<RenderProps = unknown> = { type CommonProps<RenderProps = unknown> = {

View File

@@ -1,3 +1,4 @@
import type { PageMeta } from '@blocksuite/store';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
@@ -123,3 +124,9 @@ export async function loginUser(
export async function openHomePage(page: Page) { export async function openHomePage(page: Page) {
return page.goto('http://localhost:8080'); return page.goto('http://localhost:8080');
} }
export async function getMetas(page: Page): Promise<PageMeta[]> {
return page.evaluate(
() => globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas ?? []
);
}

View File

@@ -4,8 +4,9 @@ import { expect } from '@playwright/test';
import { openHomePage } from '../libs/load-page'; import { openHomePage } from '../libs/load-page';
import { clickPageMoreActions, newPage } from '../libs/page-logic'; import { clickPageMoreActions, newPage } from '../libs/page-logic';
import { test } from '../libs/playwright'; 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 newPage(page);
await page.focus('.affine-default-page-block-title'); await page.focus('.affine-default-page-block-title');
await page.type('.affine-default-page-block-title', 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 clickPageMoreActions(page);
await page.getByTestId('move-to-menu-item').click(); 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); async function initHomePageWithPinboard(page: 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) {
await openHomePage(page); await openHomePage(page);
await createRootPage(page, 'parent1'); await page.waitForSelector('[data-testid="sidebar-pinboard-container"]');
await createRootPage(page, 'parent2'); return (await getMetas(page)).find(m => m.isRootPinboard);
await createPivotPage(page, 'child1', 'parent1');
await createPivotPage(page, 'child2', 'parent1');
} }
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.describe('PinBoard interaction', () => {
test('Add pivot', async ({ page }) => { test('Have initial root pinboard page when first in', async ({ page }) => {
await initPinBoard(page); const rootPinboardMeta = await initHomePageWithPinboard(page);
expect(rootPinboardMeta).not.toBeUndefined();
}); });
test('Remove pivots', async ({ page }) => {
await initPinBoard(page);
const child2Meta = await page.evaluate(() => { test('Root pinboard page have no operation of "trash" ,"rename" and "move to"', async ({
return globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas.find( page,
m => m.title === 'child2' }) => {
); const rootPinboardMeta = await initHomePageWithPinboard(page);
}); await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? '');
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);
expect( 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); ).toEqual(0);
}); });
test('search pivot', async ({ page }) => { test('Click Pinboard in sidebar should open root pinboard page', async ({
await initPinBoard(page); 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(() => { test('Add pinboard by header operation menu', async ({ page }) => {
return globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas.find( const rootPinboardMeta = await initHomePageWithPinboard(page);
m => m.title === 'child2' 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 test('Add pinboard by sidebar operation menu', async ({ page }) => {
.getByTestId('sidebar-pivots-container') const rootPinboardMeta = await initHomePageWithPinboard(page);
.getByTestId(`pivot-${child2Meta?.id}`);
await child2.hover();
await child2.getByTestId('pivot-operation-button').click();
await page.getByTestId('pivot-operation-move-to').click();
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); 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( expect(
await page.locator('[data-testid="pivot-search-result"]').count() await page.locator('[data-testid="pinboard-search-result"]').count()
).toEqual(1); ).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());
});
}); });