feat: add new page button (#2417)

This commit is contained in:
Whitewater 2023-05-18 13:07:05 -07:00 committed by GitHub
parent 11370bc07e
commit 530dd5ff7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 378 additions and 116 deletions

View File

@ -96,7 +96,13 @@ export const openDisableCloudAlertModalAtom = atom(false);
export { workspacesAtom } from './root'; export { workspacesAtom } from './root';
type View = { id: string; mode: 'page' | 'edgeless' }; type View = {
id: string;
/**
* @deprecated Use `mode` from `useWorkspacePreferredMode` instead.
*/
mode: 'page' | 'edgeless';
};
export type WorkspaceRecentViews = Record<string, View[]>; export type WorkspaceRecentViews = Record<string, View[]>;
@ -106,6 +112,9 @@ export const workspaceRecentViewsAtom = atomWithStorage<WorkspaceRecentViews>(
); );
export type PreferredModeRecord = Record<Page['id'], 'page' | 'edgeless'>; export type PreferredModeRecord = Record<Page['id'], 'page' | 'edgeless'>;
/**
* @deprecated Use `useWorkspacePreferredMode` instead.
*/
export const workspacePreferredModeAtom = atomWithStorage<PreferredModeRecord>( export const workspacePreferredModeAtom = atomWithStorage<PreferredModeRecord>(
'preferredMode', 'preferredMode',
{} {}

View File

@ -5,17 +5,14 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons'; import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import { useAtomValue } from 'jotai';
import type React from 'react'; import type React from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { workspacePreferredModeAtom } from '../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import type { BlockSuiteWorkspace } from '../../../shared'; import type { BlockSuiteWorkspace } from '../../../shared';
import { toast } from '../../../utils'; import { toast } from '../../../utils';
import { pageListEmptyStyle } from './index.css'; import { pageListEmptyStyle } from './index.css';
import { formatDate, usePageHelper } from './utils';
export type BlockSuitePageListProps = { export type BlockSuitePageListProps = {
blockSuiteWorkspace: BlockSuiteWorkspace; blockSuiteWorkspace: BlockSuiteWorkspace;
@ -34,13 +31,6 @@ const filter = {
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash, shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
}; };
dayjs.extend(localizedFormat);
const formatDate = (date?: number | unknown) => {
const dateStr =
typeof date === 'number' ? dayjs(date).format('YYYY-MM-DD HH:mm') : '--';
return dateStr;
};
const PageListEmpty = (props: { const PageListEmpty = (props: {
listType: BlockSuitePageListProps['listType']; listType: BlockSuitePageListProps['listType'];
}) => { }) => {
@ -80,12 +70,13 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
permanentlyDeletePage, permanentlyDeletePage,
cancelPublicPage, cancelPublicPage,
} = useBlockSuiteMetaHelper(blockSuiteWorkspace); } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const { createPage, createEdgeless, isPreferredEdgeless } =
usePageHelper(blockSuiteWorkspace);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const list = useMemo( const list = useMemo(
() => pageMetas.filter(pageMeta => filter[listType](pageMeta, pageMetas)), () => pageMetas.filter(pageMeta => filter[listType](pageMeta, pageMetas)),
[pageMetas, listType] [pageMetas, listType]
); );
const record = useAtomValue(workspacePreferredModeAtom);
if (list.length === 0) { if (list.length === 0) {
return <PageListEmpty listType={listType} />; return <PageListEmpty listType={listType} />;
} }
@ -93,8 +84,11 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
if (listType === 'trash') { if (listType === 'trash') {
const pageList: TrashListData[] = list.map(pageMeta => { const pageList: TrashListData[] = list.map(pageMeta => {
return { return {
icon: icon: isPreferredEdgeless(pageMeta.id) ? (
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />, <EdgelessIcon />
) : (
<PageIcon />
),
pageId: pageMeta.id, pageId: pageMeta.id,
title: pageMeta.title, title: pageMeta.title,
createDate: formatDate(pageMeta.createDate), createDate: formatDate(pageMeta.createDate),
@ -118,8 +112,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
const pageList: ListData[] = list.map(pageMeta => { const pageList: ListData[] = list.map(pageMeta => {
return { return {
icon: icon: isPreferredEdgeless(pageMeta.id) ? <EdgelessIcon /> : <PageIcon />,
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
pageId: pageMeta.id, pageId: pageMeta.id,
title: pageMeta.title, title: pageMeta.title,
favorite: !!pageMeta.favorite, favorite: !!pageMeta.favorite,
@ -158,10 +151,10 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
return ( return (
<PageList <PageList
onClickPage={onOpenPage} onCreateNewPage={createPage}
onCreateNewEdgeless={createEdgeless}
isPublicWorkspace={isPublic} isPublicWorkspace={isPublic}
list={pageList} list={pageList}
listType={listType}
/> />
); );
}; };

View File

@ -0,0 +1,40 @@
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import { useRouter } from 'next/router';
import { useWorkspacePreferredMode } from '../../../hooks/use-recent-views';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
dayjs.extend(localizedFormat);
export const formatDate = (date?: number | unknown) => {
const dateStr =
typeof date === 'number' ? dayjs(date).format('YYYY-MM-DD HH:mm') : '--';
return dateStr;
};
export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
const router = useRouter();
const { openPage } = useRouterHelper(router);
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { getPreferredMode, setPreferredMode } = useWorkspacePreferredMode();
const isPreferredEdgeless = (pageId: string) => {
return getPreferredMode(pageId) === 'edgeless';
};
const createPageAndOpen = () => {
const page = createPage();
openPage(blockSuiteWorkspace.id, page.id);
};
const createEdgelessAndOpen = () => {
const page = createPage();
setPreferredMode(page.id, 'edgeless');
openPage(blockSuiteWorkspace.id, page.id);
};
return {
createPage: createPageAndOpen,
createEdgeless: createEdgelessAndOpen,
isPreferredEdgeless: isPreferredEdgeless,
};
};

View File

@ -84,7 +84,7 @@ export const RootAppSidebar = ({
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace; const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const onClickNewPage = useCallback(async () => { const onClickNewPage = useCallback(async () => {
const page = await createPage(); const page = createPage();
openPage(page.id); openPage(page.id);
}, [createPage, openPage]); }, [createPage, openPage]);

View File

@ -1,6 +1,6 @@
import type { Workspace } from '@blocksuite/store'; import type { Page, Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { NextRouter } from 'next/router'; import type { NextRouter } from 'next/router';
import { useEffect } from 'react'; import { useEffect } from 'react';
@ -11,6 +11,19 @@ import {
} from '../atoms'; } from '../atoms';
import { useCurrentWorkspace } from './current/use-current-workspace'; import { useCurrentWorkspace } from './current/use-current-workspace';
export const useWorkspacePreferredMode = () => {
const [record, setPreferred] = useAtom(workspacePreferredModeAtom);
return {
getPreferredMode: (pageId: Page['id']) => record[pageId] ?? 'page',
setPreferredMode: (pageId: Page['id'], mode: 'page' | 'edgeless') => {
setPreferred(record => ({
...record,
[pageId]: mode,
}));
},
};
};
export function useRecentlyViewed() { export function useRecentlyViewed() {
const [workspace] = useCurrentWorkspace(); const [workspace] = useCurrentWorkspace();
const workspaceId = workspace?.id || null; const workspaceId = workspace?.id || null;
@ -30,15 +43,19 @@ export function useSyncRecentViewsWithRouter(
const meta = useBlockSuitePageMeta(blockSuiteWorkspace).find( const meta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId meta => meta.id === pageId
); );
const currentMode = useAtomValue(workspacePreferredModeAtom)[ const { getPreferredMode } = useWorkspacePreferredMode();
pageId as string
]; const currentMode =
typeof pageId === 'string' ? getPreferredMode(pageId) : 'page';
useEffect(() => { useEffect(() => {
if (!workspaceId) return; if (!workspaceId) return;
if (pageId && meta) { if (pageId && meta) {
set(workspaceId, { set(workspaceId, {
id: pageId as string, id: pageId as string,
mode: currentMode ?? 'page', /**
* @deprecated No necessary update `mode` at here, use `mode` from {@link useWorkspacePreferredMode} directly.
*/
mode: currentMode,
}); });
} }
}, [pageId, meta, workspaceId, set, currentMode]); }, [pageId, meta, workspaceId, set, currentMode]);

View File

@ -9,6 +9,7 @@ export const blockCard = style({
borderRadius: '4px', borderRadius: '4px',
userSelect: 'none', userSelect: 'none',
cursor: 'pointer', cursor: 'pointer',
textAlign: 'start',
selectors: { selectors: {
'&:hover': { '&:hover': {
boxShadow: 'var(--affine-shadow-1)', boxShadow: 'var(--affine-shadow-1)',

View File

@ -18,8 +18,10 @@ import {
FavoriteIcon, FavoriteIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { useMediaQuery, useTheme } from '@mui/material'; import { useMediaQuery, useTheme } from '@mui/material';
import type { CSSProperties } from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { NewPageButton } from './new-page-buttton';
import { import {
StyledTableContainer, StyledTableContainer,
StyledTableRow, StyledTableRow,
@ -68,11 +70,8 @@ const FavoriteTag = forwardRef<
export type PageListProps = { export type PageListProps = {
isPublicWorkspace?: boolean; isPublicWorkspace?: boolean;
list: ListData[]; list: ListData[];
/** onCreateNewPage: () => void;
* @deprecated onCreateNewEdgeless: () => void;
*/
listType: 'all' | 'favorite' | 'shared' | 'public';
onClickPage: (pageId: string, newTab?: boolean) => void;
}; };
const TitleCell = ({ const TitleCell = ({
@ -100,6 +99,80 @@ const TitleCell = ({
); );
}; };
const AllPagesHead = ({
sorter,
createNewPage,
createNewEdgeless,
}: {
sorter: ReturnType<typeof useSorter<ListData>>;
createNewPage: () => void;
createNewEdgeless: () => void;
}) => {
const t = useAFFiNEI18N();
const titleList = [
{
key: 'title',
content: t['Title'](),
proportion: 0.5,
},
{
key: 'createDate',
content: t['Created'](),
proportion: 0.2,
},
{
key: 'updatedDate',
content: t['Updated'](),
proportion: 0.2,
},
{
key: 'unsortable_action',
content: (
<NewPageButton
createNewPage={createNewPage}
createNewEdgeless={createNewEdgeless}
/>
),
sortable: false,
styles: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
} satisfies CSSProperties,
},
];
return (
<TableHead>
<TableRow>
{titleList.map(
({ key, content, proportion, sortable = true, styles }) => (
<TableCell
key={key}
proportion={proportion}
active={sorter.key === key}
onClick={
sortable
? () => sorter.shiftOrder(key as keyof ListData)
: undefined
}
style={styles}
>
{content}
{sorter.key === key &&
(sorter.order === 'asc' ? (
<ArrowUpBigIcon width={24} height={24} />
) : (
<ArrowDownBigIcon width={24} height={24} />
))}
</TableCell>
)
)}
</TableRow>
</TableHead>
);
};
export type ListData = { export type ListData = {
pageId: string; pageId: string;
icon: JSX.Element; icon: JSX.Element;
@ -119,7 +192,8 @@ export type ListData = {
export const PageList: React.FC<PageListProps> = ({ export const PageList: React.FC<PageListProps> = ({
isPublicWorkspace = false, isPublicWorkspace = false,
list, list,
listType, onCreateNewPage,
onCreateNewEdgeless,
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const sorter = useSorter<ListData>({ const sorter = useSorter<ListData>({
@ -128,70 +202,12 @@ export const PageList: React.FC<PageListProps> = ({
order: 'desc', order: 'desc',
}); });
const isShared = listType === 'shared';
const theme = useTheme(); const theme = useTheme();
const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm')); const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm'));
if (isSmallDevices) { if (isSmallDevices) {
return <PageListMobileView list={sorter.data} />; return <PageListMobileView list={sorter.data} />;
} }
const ListHead = () => {
const t = useAFFiNEI18N();
const titleList = [
{
key: 'title',
text: t['Title'](),
proportion: 0.5,
},
{
key: 'createDate',
text: t['Created'](),
proportion: 0.2,
},
{
key: 'updatedDate',
text: isShared
? // TODO deprecated
'Shared'
: t['Updated'](),
proportion: 0.2,
},
{ key: 'unsortable_action', sortable: false },
];
return (
<TableHead>
<TableRow>
{titleList.map(({ key, text, proportion, sortable = true }) => (
<TableCell
key={key}
proportion={proportion}
active={sorter.key === key}
onClick={
sortable
? () => sorter.shiftOrder(key as keyof ListData)
: undefined
}
>
<div
style={{ display: 'flex', alignItems: 'center', width: '100%' }}
>
{text}
{sorter.key === key &&
(sorter.order === 'asc' ? (
<ArrowUpBigIcon width={24} height={24} />
) : (
<ArrowDownBigIcon width={24} height={24} />
))}
</div>
</TableCell>
))}
</TableRow>
</TableHead>
);
};
const ListItems = sorter.data.map( const ListItems = sorter.data.map(
( (
{ {
@ -237,7 +253,13 @@ export const PageList: React.FC<PageListProps> = ({
</TableCell> </TableCell>
{!isPublicWorkspace && ( {!isPublicWorkspace && (
<TableCell <TableCell
style={{ padding: 0, display: 'flex', alignItems: 'center' }} style={{
padding: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '10px',
}}
data-testid={`more-actions-${pageId}`} data-testid={`more-actions-${pageId}`}
> >
<FavoriteTag <FavoriteTag
@ -264,7 +286,11 @@ export const PageList: React.FC<PageListProps> = ({
return ( return (
<StyledTableContainer> <StyledTableContainer>
<Table> <Table>
<ListHead /> <AllPagesHead
sorter={sorter}
createNewPage={onCreateNewPage}
createNewEdgeless={onCreateNewEdgeless}
/>
<TableBody>{ListItems}</TableBody> <TableBody>{ListItems}</TableBody>
</Table> </Table>
</StyledTableContainer> </StyledTableContainer>

View File

@ -0,0 +1,85 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { useState } from 'react';
import { DropdownButton } from '../../ui/button/dropdown';
import { Menu } from '../../ui/menu/menu';
import { BlockCard } from '../card/block-card';
type NewPageButtonProps = {
createNewPage: () => void;
createNewEdgeless: () => void;
};
export const CreateNewPagePopup = ({
createNewPage,
createNewEdgeless,
}: NewPageButtonProps) => {
const t = useAFFiNEI18N();
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
padding: '8px',
}}
>
<BlockCard
title={t['New Page']()}
desc={t['com.affine.write_with_a_blank_page']()}
right={<PageIcon width={20} height={20} />}
onClick={createNewPage}
/>
<BlockCard
title={t['com.affine.new_edgeless']()}
desc={t['com.affine.draw_with_a_blank_whiteboard']()}
right={<EdgelessIcon width={20} height={20} />}
onClick={createNewEdgeless}
/>
{/* TODO Import */}
</div>
);
};
export const NewPageButton = ({
createNewPage,
createNewEdgeless,
}: NewPageButtonProps) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
return (
<Menu
visible={open}
placement="bottom-end"
trigger={['click']}
disablePortal={true}
onClickAway={() => {
setOpen(false);
}}
menuStyles={{ padding: '0px' }}
content={
<CreateNewPagePopup
createNewPage={() => {
createNewPage();
setOpen(false);
}}
createNewEdgeless={() => {
createNewEdgeless();
setOpen(false);
}}
/>
}
>
<DropdownButton
onClick={() => {
createNewPage();
setOpen(false);
}}
onClickDropDown={() => setOpen(!open)}
>
{t['New Page']()}
</DropdownButton>
</Menu>
);
};

View File

@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
type Sorter<T> = { type SorterConfig<T> = {
data: T[]; data: T[];
key: keyof T; key: keyof T;
order: 'asc' | 'desc' | 'none'; order: 'asc' | 'desc' | 'none';
@ -32,8 +32,8 @@ const defaultSortingFn = <T extends Record<keyof any, unknown>>(
export const useSorter = <T extends Record<keyof any, unknown>>({ export const useSorter = <T extends Record<keyof any, unknown>>({
data, data,
...defaultSorter ...defaultSorter
}: Sorter<T> & { order: 'asc' | 'desc' }) => { }: SorterConfig<T> & { order: 'asc' | 'desc' }) => {
const [sorter, setSorter] = useState<Omit<Sorter<T>, 'data'>>({ const [sorter, setSorter] = useState<Omit<SorterConfig<T>, 'data'>>({
...defaultSorter, ...defaultSorter,
// We should not show sorting icon at first time // We should not show sorting icon at first time
order: 'none', order: 'none',
@ -74,7 +74,7 @@ export const useSorter = <T extends Record<keyof any, unknown>>({
/** /**
* @deprecated In most cases, we no necessary use `setSorter` directly. * @deprecated In most cases, we no necessary use `setSorter` directly.
*/ */
updateSorter: (newVal: Partial<Sorter<T>>) => updateSorter: (newVal: Partial<SorterConfig<T>>) =>
setSorter({ ...sorter, ...newVal }), setSorter({ ...sorter, ...newVal }),
shiftOrder, shiftOrder,
resetSorter: () => setSorter(defaultSorter), resetSorter: () => setSorter(defaultSorter),

View File

@ -1,5 +1,7 @@
import { PageIcon } from '@blocksuite/icons'; import { PageIcon } from '@blocksuite/icons';
import { expect } from '@storybook/jest';
import type { StoryFn } from '@storybook/react'; import type { StoryFn } from '@storybook/react';
import { userEvent } from '@storybook/testing-library';
import { AffineLoading } from '../components/affine-loading'; import { AffineLoading } from '../components/affine-loading';
import type { import type {
@ -8,6 +10,7 @@ import type {
} from '../components/page-list/all-page'; } from '../components/page-list/all-page';
import { PageListTrashView } from '../components/page-list/all-page'; import { PageListTrashView } from '../components/page-list/all-page';
import PageList from '../components/page-list/all-page'; import PageList from '../components/page-list/all-page';
import { NewPageButton } from '../components/page-list/new-page-buttton';
import type { OperationCellProps } from '../components/page-list/operation-cell'; import type { OperationCellProps } from '../components/page-list/operation-cell';
import { OperationCell } from '../components/page-list/operation-cell'; import { OperationCell } from '../components/page-list/operation-cell';
import { toast } from '../ui/toast'; import { toast } from '../ui/toast';
@ -19,11 +22,7 @@ export default {
export const AffineOperationCell: StoryFn<OperationCellProps> = ({ export const AffineOperationCell: StoryFn<OperationCellProps> = ({
...props ...props
}) => ( }) => <OperationCell {...props} />;
<div>
<OperationCell {...props} />
</div>
);
AffineOperationCell.args = { AffineOperationCell.args = {
title: 'Example Page', title: 'Example Page',
@ -34,16 +33,40 @@ AffineOperationCell.args = {
onOpenPageInNewTab: () => toast('Open page in new tab'), onOpenPageInNewTab: () => toast('Open page in new tab'),
onRemoveToTrash: () => toast('Remove to trash'), onRemoveToTrash: () => toast('Remove to trash'),
}; };
AffineOperationCell.play = async ({ canvasElement }) => {
{
const button = canvasElement.querySelector(
'[data-testid="page-list-operation-button"]'
) as HTMLButtonElement;
expect(button).not.toBeNull();
userEvent.click(button);
}
};
export const AffineNewPageButton: StoryFn<typeof NewPageButton> = ({
...props
}) => <NewPageButton {...props} />;
AffineNewPageButton.args = {
createNewPage: () => toast('Create new page'),
createNewEdgeless: () => toast('Create new edgeless'),
};
AffineNewPageButton.play = async ({ canvasElement }) => {
const button = canvasElement.querySelector('button') as HTMLButtonElement;
expect(button).not.toBeNull();
const dropdown = button.querySelector('svg') as SVGSVGElement;
expect(dropdown).not.toBeNull();
userEvent.click(dropdown);
};
export const AffineAllPageList: StoryFn<PageListProps> = ({ ...props }) => ( export const AffineAllPageList: StoryFn<PageListProps> = ({ ...props }) => (
<div> <PageList {...props} />
<PageList {...props} />
</div>
); );
AffineAllPageList.args = { AffineAllPageList.args = {
isPublicWorkspace: false, isPublicWorkspace: false,
listType: 'all', onCreateNewPage: () => toast('Create new page'),
onCreateNewEdgeless: () => toast('Create new edgeless'),
list: [ list: [
{ {
pageId: '1', pageId: '1',
@ -76,13 +99,20 @@ AffineAllPageList.args = {
], ],
}; };
export const AffineAllPageMobileList: StoryFn<PageListProps> = ({
...props
}) => <PageList {...props} />;
AffineAllPageMobileList.args = AffineAllPageList.args;
AffineAllPageMobileList.parameters = {
viewport: {
defaultViewport: 'mobile2',
},
};
export const AffineTrashPageList: StoryFn<{ export const AffineTrashPageList: StoryFn<{
list: TrashListData[]; list: TrashListData[];
}> = ({ ...props }) => ( }> = ({ ...props }) => <PageListTrashView {...props} />;
<div>
<PageListTrashView {...props} />
</div>
);
AffineTrashPageList.args = { AffineTrashPageList.args = {
list: [ list: [

View File

@ -6,16 +6,27 @@ import { StyledMenuWrapper } from './styles';
export type MenuProps = { export type MenuProps = {
width?: CSSProperties['width']; width?: CSSProperties['width'];
menuStyles?: CSSProperties;
} & PopperProps & } & PopperProps &
Omit<TooltipProps, 'title' | 'content' | 'placement'>; Omit<TooltipProps, 'title' | 'content' | 'placement'>;
export const Menu = (props: MenuProps) => { export const Menu = (props: MenuProps) => {
const { width, content, placement = 'bottom-start', children } = props; const {
width,
menuStyles,
content,
placement = 'bottom-start',
children,
} = props;
return content ? ( return content ? (
<Popper <Popper
{...props} {...props}
showArrow={false} showArrow={false}
content={ content={
<StyledMenuWrapper width={width} placement={placement}> <StyledMenuWrapper
width={width}
placement={placement}
style={menuStyles}
>
{content} {content}
</StyledMenuWrapper> </StyledMenuWrapper>
} }

View File

@ -3,7 +3,10 @@ import type { CSSProperties } from 'react';
import { displayFlex, styled, textEllipsis } from '../../styles'; import { displayFlex, styled, textEllipsis } from '../../styles';
import StyledPopperContainer from '../shared/container'; import StyledPopperContainer from '../shared/container';
export const StyledMenuWrapper = styled(StyledPopperContainer)<{ export const StyledMenuWrapper = styled(StyledPopperContainer, {
shouldForwardProp: propName =>
!['width', 'height'].includes(propName as string),
})<{
width?: CSSProperties['width']; width?: CSSProperties['width'];
height?: CSSProperties['height']; height?: CSSProperties['height'];
}>(({ width, height }) => { }>(({ width, height }) => {

View File

@ -37,7 +37,6 @@ export const StyledTableCell = styled('td')<
return { return {
width, width,
height: '52px', height: '52px',
lineHeight: '52px',
padding: '0 30px', padding: '0 30px',
boxSizing: 'border-box', boxSizing: 'border-box',
textAlign: align, textAlign: align,

View File

@ -10,7 +10,7 @@ import { useMemo } from 'react';
export function useBlockSuiteWorkspaceHelper(blockSuiteWorkspace: Workspace) { export function useBlockSuiteWorkspaceHelper(blockSuiteWorkspace: Workspace) {
return useMemo( return useMemo(
() => ({ () => ({
createPage: (pageId: string): Page => { createPage: (pageId?: string): Page => {
assertExists(blockSuiteWorkspace); assertExists(blockSuiteWorkspace);
return blockSuiteWorkspace.createPage({ id: pageId }); return blockSuiteWorkspace.createPage({ id: pageId });
}, },

View File

@ -19,6 +19,9 @@
"Import": "Import", "Import": "Import",
"Trash": "Trash", "Trash": "Trash",
"New Page": "New Page", "New Page": "New Page",
"com.affine.write_with_a_blank_page": "Write with a blank page",
"com.affine.new_edgeless": "New Edgeless",
"com.affine.draw_with_a_blank_whiteboard": "Draw with a blank whiteboard",
"New Keyword Page": "New '{{query}}' page", "New Keyword Page": "New '{{query}}' page",
"Find 0 result": "Find 0 result", "Find 0 result": "Find 0 result",
"Find results": "Find {{number}} results", "Find results": "Find {{number}} results",

View File

@ -1,11 +1,56 @@
import { test } from '@affine-test/kit/playwright'; import { test } from '@affine-test/kit/playwright';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { openHomePage } from '../libs/load-page'; import { openHomePage } from '../libs/load-page';
import { waitMarkdownImported } from '../libs/page-logic'; import {
getBlockSuiteEditorTitle,
waitMarkdownImported,
} from '../libs/page-logic';
import { clickSideBarAllPageButton } from '../libs/sidebar'; import { clickSideBarAllPageButton } from '../libs/sidebar';
function getAllPage(page: Page) {
const newPageButton = page
.locator('table')
.getByRole('button', { name: 'New Page' });
const newPageDropdown = newPageButton.locator('svg');
const edgelessBlockCard = page.locator('table').getByText('New Edgeless');
async function clickNewPageButton() {
return newPageButton.click();
}
async function clickNewEdgelessDropdown() {
await newPageDropdown.click();
await edgelessBlockCard.click();
}
return { clickNewPageButton, clickNewEdgelessDropdown };
}
test('all page', async ({ page }) => { test('all page', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitMarkdownImported(page); await waitMarkdownImported(page);
await clickSideBarAllPageButton(page); await clickSideBarAllPageButton(page);
}); });
test('all page can create new page', async ({ page }) => {
const { clickNewPageButton } = getAllPage(page);
await openHomePage(page);
await waitMarkdownImported(page);
await clickSideBarAllPageButton(page);
await clickNewPageButton();
const title = getBlockSuiteEditorTitle(page);
await title.fill('this is a new page');
await clickSideBarAllPageButton(page);
const cell = page.getByRole('cell', { name: 'this is a new page' });
expect(cell).not.toBeUndefined();
});
test('all page can create new edgeless page', async ({ page }) => {
const { clickNewEdgelessDropdown } = getAllPage(page);
await openHomePage(page);
await waitMarkdownImported(page);
await clickSideBarAllPageButton(page);
await clickNewEdgelessDropdown();
await expect(page.locator('affine-edgeless-page')).toBeVisible();
});