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 { 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<PropsWithChildren> = ({ 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 (
<ThemeProvider>
<ProviderWrapper>
<Pivots {...props} />
<Pinboard {...props} />
</ProviderWrapper>
</ThemeProvider>
);
@@ -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();
});
});

View File

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

View File

@@ -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')}
</MenuItem>
<PivotsMenu
<PinboardMenu
anchorEl={anchorEl}
open={open}
placement="left-start"
metas={metas.filter(meta => !meta.trash)}
metas={useMemo(
() => metas.filter(m => !m.trash && m.id !== currentMeta.id),
[metas, currentMeta]
)}
currentMeta={currentMeta}
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 { workspacePreferredModeAtom } from '../../../../atoms';
import { StyledMenuSubTitle, StyledPivot } from '../styles';
import { StyledMenuSubTitle, StyledPinboard } from '../styles';
export const SearchContent = ({
results,
@@ -27,16 +27,16 @@ export const SearchContent = ({
</StyledMenuSubTitle>
{results.map(meta => {
return (
<StyledPivot
<StyledPinboard
key={meta.id}
onClick={() => {
onClick?.(meta.id);
}}
data-testid="pivot-search-result"
data-testid="pinboard-search-result"
>
{record[meta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
{meta.title}
</StyledPivot>
</StyledPinboard>
);
})}
</>

View File

@@ -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"
>
<StyledSearchContainer>
<label>
@@ -93,7 +92,7 @@ export const PivotsMenu = ({
height={32}
noBorder={true}
onClick={e => e.stopPropagation()}
data-testid="pivots-menu-search"
data-testid="pinboard-menu-search"
/>
</StyledSearchContainer>
@@ -104,21 +103,16 @@ export const PivotsMenu = ({
{!isSearching && (
<>
<StyledMenuSubTitle>Suggested</StyledMenuSubTitle>
<Pivots
data={data}
blockSuiteWorkspace={blockSuiteWorkspace}
currentMeta={currentMeta}
/>
<TreeView data={data} indent={16} enableDnd={false} />
</>
)}
</StyledMenuContent>
{showRemovePivots && (
{showRemovePinboard && (
<StyledMenuFooter>
<StyledPivot
data-testid={'remove-from-pivots-button'}
<StyledPinboard
data-testid={'remove-from-pinboard-button'}
onClick={() => {
setPageMeta(currentMeta.id, { isPivots: false });
const parentMeta = metas.find(m =>
m.subpageIds.includes(currentMeta.id)
);
@@ -132,11 +126,13 @@ export const PivotsMenu = ({
}}
>
<RemoveIcon />
{t('Remove from Pivots')}
</StyledPivot>
{t('Remove from Pinboard')}
</StyledPinboard>
<p>{t('RFP')}</p>
</StyledMenuFooter>
)}
</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 { 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 | HTMLElement>(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 = ({
<MuiClickAwayListener
onClickAway={() => {
setOperationMenuOpen(false);
setPivotsMenuOpen(false);
setPinboardMenuOpen(false);
}}
>
<div
@@ -62,11 +64,11 @@ export const OperationButton = ({
}}
onMouseLeave={() => {
setOperationMenuOpen(false);
setPivotsMenuOpen(false);
setPinboardMenuOpen(false);
}}
>
<StyledOperationButton
data-testid="pivot-operation-button"
data-testid="pinboard-operation-button"
ref={ref => setAnchorEl(ref)}
size="small"
onClick={() => {
@@ -78,7 +80,7 @@ export const OperationButton = ({
</StyledOperationButton>
<PureMenu
data-testid="pivot-operation-menu"
data-testid="pinboard-operation-menu"
width={256}
anchorEl={anchorEl}
open={operationMenuOpen}
@@ -86,7 +88,7 @@ export const OperationButton = ({
zIndex={menuIndex}
>
<MenuItem
data-testid="pivot-operation-add"
data-testid="pinboard-operation-add"
onClick={() => {
onAdd();
setOperationMenuOpen(false);
@@ -96,47 +98,53 @@ export const OperationButton = ({
>
{t('Add a subpage inside')}
</MenuItem>
<MenuItem
data-testid="pivot-operation-move-to"
onClick={() => {
setOperationMenuOpen(false);
setPivotsMenuOpen(true);
}}
icon={<MoveToIcon />}
>
{t('Move to')}
</MenuItem>
<MenuItem
data-testid="pivot-operation-rename"
onClick={() => {
onRename?.();
setOperationMenuOpen(false);
onMenuClose?.();
}}
icon={<PenIcon />}
>
{t('Rename')}
</MenuItem>
<MoveToTrash
testId="pivot-operation-move-to-trash"
onItemClick={() => {
setOperationMenuOpen(false);
setConfirmModalOpen(true);
onMenuClose?.();
}}
/>
{!isRoot && (
<MenuItem
data-testid="pinboard-operation-move-to"
onClick={() => {
setOperationMenuOpen(false);
setPinboardMenuOpen(true);
}}
icon={<MoveToIcon />}
>
{t('Move to')}
</MenuItem>
)}
{!isRoot && (
<MenuItem
data-testid="pinboard-operation-rename"
onClick={() => {
onRename?.();
setOperationMenuOpen(false);
onMenuClose?.();
}}
icon={<PenIcon />}
>
{t('Rename')}
</MenuItem>
)}
{!isRoot && (
<MoveToTrash
testId="pinboard-operation-move-to-trash"
onItemClick={() => {
setOperationMenuOpen(false);
setConfirmModalOpen(true);
onMenuClose?.();
}}
/>
)}
<CopyLink />
</PureMenu>
<PivotsMenu
<PinboardMenu
anchorEl={anchorEl}
open={pivotsMenuOpen}
open={pinboardMenuOpen}
placement="bottom-start"
zIndex={menuIndex}
metas={metas}
currentMeta={currentMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
showRemovePivots={true}
showRemovePinboard={true}
/>
<MoveToTrash.ConfirmModal
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',
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;

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

View File

@@ -82,17 +82,21 @@ export const EditorOptionMenu = () => {
{mode === 'page' ? t('Edgeless') : t('Page')}
</MenuItem>
<Export />
<MoveTo
metas={allMetas}
currentMeta={pageMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
<MoveToTrash
testId="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
}}
/>
{!pageMeta.isRootPinboard && (
<MoveTo
metas={allMetas}
currentMeta={pageMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
)}
{!pageMeta.isRootPinboard && (
<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,
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 (
<StyledChangeLogWarper isClose={isClose}>
<StyledChangeLogWrapper isClose={isClose}>
<StyledChangeLog data-testid="change-log" isClose={isClose}>
<StyledLink href={'https://affine.pro'} target="_blank">
<NewIcon />
@@ -49,7 +50,7 @@ export const ChangeLog = () => {
<CloseIcon />
</IconButton>
</StyledChangeLog>
</StyledChangeLogWarper>
</StyledChangeLogWrapper>
);
};

View File

@@ -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<WorkSpaceSliderBarProps> = ({
currentWorkspace={currentWorkspace}
/>
{!!blockSuiteWorkspace && (
<Pivots
<Pinboard
blockSuiteWorkspace={blockSuiteWorkspace}
openPage={openPage}
allMetas={pageMeta}

View File

@@ -53,7 +53,7 @@ export const StyledCollapseButton = 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)`,
},
@@ -188,7 +188,7 @@ export const StyledChangeLog = styled('div')<{
},
};
});
export const StyledChangeLogWarper = styled('div')<{
export const StyledChangeLogWrapper = styled('div')<{
isClose?: boolean;
}>(({ isClose }) => {
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 { 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<NodeRenderProps>['onDelete'];
onDrop?: TreeViewProps<NodeRenderProps>['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;

View File

@@ -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<PageMeta>) => {
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);
},
}),

View File

@@ -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 () => {

View File

@@ -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<EditorContainer>): 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<EditorContainer>): 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<EditorContainer>) {
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<EditorContainer>
): 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;
}