mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-28 08:05:21 +03:00
feat: add root pinboard & rename pivots to pinboard (#1843)
This commit is contained in:
parent
d4b2b9ab44
commit
e50bf9fbfe
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
2
apps/web/src/components/affine/pinboard/index.ts
Normal file
2
apps/web/src/components/affine/pinboard/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './pinboard-menu';
|
||||
export * from './pinboard-render/';
|
@ -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>
|
||||
);
|
||||
})}
|
||||
</>
|
@ -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;
|
@ -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;
|
@ -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}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -1,5 +0,0 @@
|
||||
export * from './hooks/usePivotData';
|
||||
export * from './hooks/usePivotHandler';
|
||||
export * from './pivot-render/PivotRender';
|
||||
export * from './PivotsMenu/PivotsMenu';
|
||||
export * from './types';
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>;
|
@ -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>
|
||||
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
79
apps/web/src/hooks/affine/use-pinboard-data.ts
Normal file
79
apps/web/src/hooks/affine/use-pinboard-data.ts
Normal 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;
|
@ -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;
|
@ -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);
|
||||
},
|
||||
}),
|
||||
|
@ -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 () => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -144,11 +144,12 @@ export const TreeNode = <RenderProps,>({
|
||||
}: TreeNodeProps<RenderProps>) => {
|
||||
const { indent, enableDnd, collapsedIds } = otherProps;
|
||||
const collapsed = collapsedIds.includes(node.id);
|
||||
const { renderTopLine = true, renderBottomLine = true } = node;
|
||||
|
||||
return (
|
||||
<StyledTreeNodeContainer ref={dragRef} isDragging={isDragging}>
|
||||
<StyledTreeNodeWrapper>
|
||||
{enableDnd && index === 0 && (
|
||||
{enableDnd && renderTopLine && index === 0 && (
|
||||
<NodeLine
|
||||
node={node}
|
||||
{...otherProps}
|
||||
@ -174,13 +175,15 @@ export const TreeNode = <RenderProps,>({
|
||||
/>
|
||||
)}
|
||||
|
||||
{enableDnd && (!node.children?.length || collapsed) && (
|
||||
<NodeLine
|
||||
node={node}
|
||||
{...otherProps}
|
||||
allowDrop={!isDragging && allowDrop}
|
||||
/>
|
||||
)}
|
||||
{enableDnd &&
|
||||
renderBottomLine &&
|
||||
(!node.children?.length || collapsed) && (
|
||||
<NodeLine
|
||||
node={node}
|
||||
{...otherProps}
|
||||
allowDrop={!isDragging && allowDrop}
|
||||
/>
|
||||
)}
|
||||
</StyledTreeNodeWrapper>
|
||||
<StyledCollapse in={!collapsed} indent={indent}>
|
||||
{node.children &&
|
||||
|
@ -26,6 +26,8 @@ export type Node<RenderProps = unknown> = {
|
||||
},
|
||||
renderProps?: RenderProps
|
||||
) => ReactNode;
|
||||
renderTopLine?: boolean;
|
||||
renderBottomLine?: boolean;
|
||||
};
|
||||
|
||||
type CommonProps<RenderProps = unknown> = {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
@ -123,3 +124,9 @@ export async function loginUser(
|
||||
export async function openHomePage(page: Page) {
|
||||
return page.goto('http://localhost:8080');
|
||||
}
|
||||
|
||||
export async function getMetas(page: Page): Promise<PageMeta[]> {
|
||||
return page.evaluate(
|
||||
() => globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas ?? []
|
||||
);
|
||||
}
|
||||
|
@ -4,8 +4,9 @@ import { expect } from '@playwright/test';
|
||||
import { openHomePage } from '../libs/load-page';
|
||||
import { clickPageMoreActions, newPage } from '../libs/page-logic';
|
||||
import { test } from '../libs/playwright';
|
||||
import { getMetas } from '../libs/utils';
|
||||
|
||||
async function createRootPage(page: Page, title: string) {
|
||||
async function createPinboardPage(page: Page, parentId: string, title: string) {
|
||||
await newPage(page);
|
||||
await page.focus('.affine-default-page-block-title');
|
||||
await page.type('.affine-default-page-block-title', title, {
|
||||
@ -13,73 +14,249 @@ async function createRootPage(page: Page, title: string) {
|
||||
});
|
||||
await clickPageMoreActions(page);
|
||||
await page.getByTestId('move-to-menu-item').click();
|
||||
await page.getByTestId('root-pivot-button-in-pivots-menu').click();
|
||||
await page
|
||||
.getByTestId('pinboard-menu')
|
||||
.getByTestId(`pinboard-${parentId}`)
|
||||
.click();
|
||||
}
|
||||
async function createPivotPage(page: Page, title: string, parentTitle: string) {
|
||||
await newPage(page);
|
||||
await page.focus('.affine-default-page-block-title');
|
||||
await page.type('.affine-default-page-block-title', title, {
|
||||
delay: 100,
|
||||
});
|
||||
await clickPageMoreActions(page);
|
||||
await page.getByTestId('move-to-menu-item').click();
|
||||
await page.getByTestId('pivots-menu').getByText(parentTitle).click();
|
||||
}
|
||||
export async function initPinBoard(page: Page) {
|
||||
|
||||
async function initHomePageWithPinboard(page: Page) {
|
||||
await openHomePage(page);
|
||||
await createRootPage(page, 'parent1');
|
||||
await createRootPage(page, 'parent2');
|
||||
await createPivotPage(page, 'child1', 'parent1');
|
||||
await createPivotPage(page, 'child2', 'parent1');
|
||||
await page.waitForSelector('[data-testid="sidebar-pinboard-container"]');
|
||||
return (await getMetas(page)).find(m => m.isRootPinboard);
|
||||
}
|
||||
|
||||
async function openPinboardPageOperationMenu(page: Page, id: string) {
|
||||
const node = await page
|
||||
.getByTestId('sidebar-pinboard-container')
|
||||
.getByTestId(`pinboard-${id}`);
|
||||
await node.hover();
|
||||
await node.getByTestId('pinboard-operation-button').click();
|
||||
}
|
||||
|
||||
test.describe('PinBoard interaction', () => {
|
||||
test('Add pivot', async ({ page }) => {
|
||||
await initPinBoard(page);
|
||||
test('Have initial root pinboard page when first in', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
expect(rootPinboardMeta).not.toBeUndefined();
|
||||
});
|
||||
test('Remove pivots', async ({ page }) => {
|
||||
await initPinBoard(page);
|
||||
|
||||
const child2Meta = await page.evaluate(() => {
|
||||
return globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas.find(
|
||||
m => m.title === 'child2'
|
||||
);
|
||||
});
|
||||
|
||||
const child2 = await page
|
||||
.getByTestId('sidebar-pivots-container')
|
||||
.getByTestId(`pivot-${child2Meta?.id}`);
|
||||
await child2.hover();
|
||||
await child2.getByTestId('pivot-operation-button').click();
|
||||
await page.getByTestId('pivot-operation-move-to').click();
|
||||
await page.getByTestId('remove-from-pivots-button').click();
|
||||
await page.waitForTimeout(1000);
|
||||
test('Root pinboard page have no operation of "trash" ,"rename" and "move to"', async ({
|
||||
page,
|
||||
}) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? '');
|
||||
expect(
|
||||
await page.locator(`[data-testid="pivot-${child2Meta.id}"]`).count()
|
||||
await page
|
||||
.locator(`[data-testid="pinboard-operation-move-to-trash"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
expect(
|
||||
await page.locator(`[data-testid="pinboard-operation-rename"]`).count()
|
||||
).toEqual(0);
|
||||
expect(
|
||||
await page.locator(`[data-testid="pinboard-operation-move-to"]`).count()
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
test('search pivot', async ({ page }) => {
|
||||
await initPinBoard(page);
|
||||
test('Click Pinboard in sidebar should open root pinboard page', async ({
|
||||
page,
|
||||
}) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await page.getByTestId(`pinboard-${rootPinboardMeta?.id}`).click();
|
||||
await page.waitForTimeout(200);
|
||||
expect(await page.locator('affine-editor-container')).not.toBeNull();
|
||||
});
|
||||
|
||||
const child2Meta = await page.evaluate(() => {
|
||||
return globalThis.currentWorkspace.blockSuiteWorkspace.meta.pageMetas.find(
|
||||
m => m.title === 'child2'
|
||||
);
|
||||
});
|
||||
test('Add pinboard by header operation menu', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const meta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
expect(meta).not.toBeUndefined();
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('[data-testid="sidebar-pinboard-container"]')
|
||||
.getByTestId(`pinboard-${meta?.id}`)
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
const child2 = await page
|
||||
.getByTestId('sidebar-pivots-container')
|
||||
.getByTestId(`pivot-${child2Meta?.id}`);
|
||||
await child2.hover();
|
||||
await child2.getByTestId('pivot-operation-button').click();
|
||||
await page.getByTestId('pivot-operation-move-to').click();
|
||||
test('Add pinboard by sidebar operation menu', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
|
||||
await page.fill('[data-testid="pivots-menu-search"]', '111');
|
||||
await openPinboardPageOperationMenu(page, rootPinboardMeta?.id ?? '');
|
||||
await page.getByTestId('pinboard-operation-add').click();
|
||||
const newPageMeta = (await getMetas(page)).find(
|
||||
m => m.id !== rootPinboardMeta?.id
|
||||
);
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('sidebar-pinboard-container')
|
||||
.getByTestId(`pinboard-${newPageMeta?.id}`)
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
test('Move pinboard to another in sidebar', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2');
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
await page.getByTestId('pinboard-operation-move-to').click();
|
||||
await page
|
||||
.getByTestId('pinboard-menu')
|
||||
.getByTestId(`pinboard-${childMeta2?.id}`)
|
||||
.click();
|
||||
expect(
|
||||
(await getMetas(page)).find(m => m.title === 'test2')?.subpageIds.length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('Should no show pinboard self in move to menu', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2');
|
||||
|
||||
await page.getByTestId('all-pages').click();
|
||||
await page
|
||||
.getByTestId(`page-list-item-${childMeta?.id}`)
|
||||
.getByTestId('page-list-operation-button')
|
||||
.click();
|
||||
await page.getByTestId('move-to-menu-item').click();
|
||||
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('pinboard-menu')
|
||||
.locator(`[data-testid="pinboard-${childMeta?.id}"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
});
|
||||
test('Move pinboard to another in page list', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test2');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
const childMeta2 = (await getMetas(page)).find(m => m.title === 'test2');
|
||||
|
||||
await page.getByTestId('all-pages').click();
|
||||
await page
|
||||
.getByTestId(`page-list-item-${childMeta?.id}`)
|
||||
.getByTestId('page-list-operation-button')
|
||||
.click();
|
||||
await page.getByTestId('move-to-menu-item').click();
|
||||
await page
|
||||
.getByTestId('pinboard-menu')
|
||||
.getByTestId(`pinboard-${childMeta2?.id}`)
|
||||
.click();
|
||||
expect(
|
||||
(await getMetas(page)).find(m => m.title === 'test2')?.subpageIds.length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('Remove from pinboard', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('pinboard-operation-move-to').click();
|
||||
await page.getByTestId('remove-from-pinboard-button').click();
|
||||
await page.waitForTimeout(1000);
|
||||
expect(
|
||||
await page.locator(`[data-testid="pinboard-${childMeta?.id}"]`).count()
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
test('search pinboard', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('pinboard-operation-move-to').click();
|
||||
|
||||
await page.fill('[data-testid="pinboard-menu-search"]', '111');
|
||||
expect(await page.locator('[alt="no result"]').count()).toEqual(1);
|
||||
|
||||
await page.fill('[data-testid="pivots-menu-search"]', 'parent2');
|
||||
await page.fill('[data-testid="pinboard-menu-search"]', 'test1');
|
||||
expect(
|
||||
await page.locator('[data-testid="pivot-search-result"]').count()
|
||||
await page.locator('[data-testid="pinboard-search-result"]').count()
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
test('Rename pinboard', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('pinboard-operation-rename').click();
|
||||
await page.fill(`[data-testid="pinboard-input-${childMeta?.id}"]`, 'test2');
|
||||
|
||||
const title = (await page
|
||||
.locator('.affine-default-page-block-title')
|
||||
.textContent()) as string;
|
||||
|
||||
expect(title).toEqual('test2');
|
||||
});
|
||||
|
||||
test('Move pinboard to trash', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
await createPinboardPage(page, childMeta?.id ?? '', 'test2');
|
||||
const grandChildMeta = (await getMetas(page)).find(
|
||||
m => m.title === 'test2'
|
||||
);
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('pinboard-operation-move-to-trash').click();
|
||||
(
|
||||
await page.waitForSelector('[data-testid="move-to-trash-confirm"]')
|
||||
).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('sidebar-pinboard-container')
|
||||
.locator(`[data-testid="pinboard-${childMeta?.id}"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
|
||||
expect(
|
||||
await page
|
||||
.getByTestId('sidebar-pinboard-container')
|
||||
.locator(`[data-testid="pinboard-${grandChildMeta?.id}"]`)
|
||||
.count()
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
// FIXME
|
||||
test.skip('Copy link', async ({ page }) => {
|
||||
const rootPinboardMeta = await initHomePageWithPinboard(page);
|
||||
await createPinboardPage(page, rootPinboardMeta?.id ?? '', 'test1');
|
||||
const childMeta = (await getMetas(page)).find(m => m.title === 'test1');
|
||||
|
||||
await openPinboardPageOperationMenu(page, childMeta?.id ?? '');
|
||||
|
||||
await page.getByTestId('copy-link').click();
|
||||
|
||||
await page.evaluate(() => {
|
||||
const input = document.createElement('input');
|
||||
input.id = 'paste-input';
|
||||
document.body.appendChild(input);
|
||||
input.focus();
|
||||
});
|
||||
await page.keyboard.press(`Meta+v`, { delay: 50 });
|
||||
await page.keyboard.press(`Control+v`, { delay: 50 });
|
||||
const copiedValue = await page
|
||||
.locator('#paste-input')
|
||||
.evaluate((input: HTMLInputElement) => input.value);
|
||||
expect(copiedValue).toEqual(page.url());
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user