mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 20:11:38 +03:00
feat: add new page button (#2417)
This commit is contained in:
parent
11370bc07e
commit
530dd5ff7f
@ -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',
|
||||||
{}
|
{}
|
||||||
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
@ -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]);
|
||||||
|
|
||||||
|
@ -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]);
|
||||||
|
@ -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)',
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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),
|
||||||
|
@ -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: [
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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,
|
||||||
|
@ -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 });
|
||||||
},
|
},
|
||||||
|
@ -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",
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user