mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 23:07:02 +08:00
refactor!: next generation AFFiNE code structure (#1176)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
import { BlockSuiteWorkspace } from '../../../shared';
|
||||
import PageList from './page-list';
|
||||
|
||||
export type BlockSuitePageListProps = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
||||
};
|
||||
|
||||
export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
blockSuiteWorkspace,
|
||||
onOpenPage,
|
||||
}) => {
|
||||
return (
|
||||
<PageList
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
onClickPage={onOpenPage}
|
||||
listType="all"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlockSuitePublicPageList: React.FC<BlockSuitePageListProps> = ({
|
||||
blockSuiteWorkspace,
|
||||
onOpenPage,
|
||||
}) => {
|
||||
return (
|
||||
<PageList
|
||||
isPublic={true}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
onClickPage={onOpenPage}
|
||||
listType="all"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { TableCell, TableCellProps } from '@affine/component';
|
||||
import { PageMeta } from '@blocksuite/store';
|
||||
import dayjs from 'dayjs';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import React from 'react';
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export const DateCell = ({
|
||||
pageMeta,
|
||||
dateKey,
|
||||
backupKey = '',
|
||||
...props
|
||||
}: {
|
||||
pageMeta: PageMeta;
|
||||
dateKey: keyof PageMeta;
|
||||
backupKey?: keyof PageMeta;
|
||||
} & Omit<TableCellProps, 'children'>) => {
|
||||
const value = pageMeta[dateKey] ?? pageMeta[backupKey];
|
||||
return (
|
||||
<TableCell ellipsis={true} {...props}>
|
||||
{value ? dayjs(value as string).format('YYYY-MM-DD HH:mm') : '--'}
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateCell;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Empty } from '@affine/component';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import React from 'react';
|
||||
export const PageListEmpty = (props: { listType?: string }) => {
|
||||
const { listType } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getEmptyDescription = () => {
|
||||
if (listType === 'all') {
|
||||
return t('emptyAllPages');
|
||||
}
|
||||
if (listType === 'favorite') {
|
||||
return t('emptyFavorite');
|
||||
}
|
||||
if (listType === 'trash') {
|
||||
return t('emptyTrash');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: 'calc(100% - 60px)' }}>
|
||||
<Empty description={getEmptyDescription()} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageListEmpty;
|
||||
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
Confirm,
|
||||
FlexWrapper,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
} from '@affine/component';
|
||||
import { toast } from '@affine/component';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import {
|
||||
DeletePermanentlyIcon,
|
||||
DeleteTemporarilyIcon,
|
||||
FavoritedIcon,
|
||||
FavoriteIcon,
|
||||
MoreVerticalIcon,
|
||||
OpenInNewIcon,
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { PageMeta } from '@blocksuite/store';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export type OperationCellProps = {
|
||||
pageMeta: PageMeta;
|
||||
onOpenPageInNewTab: (pageId: string) => void;
|
||||
onToggleFavoritePage: (pageId: string) => void;
|
||||
onToggleTrashPage: (pageId: string) => void;
|
||||
};
|
||||
export const OperationCell: React.FC<OperationCellProps> = ({
|
||||
pageMeta,
|
||||
onOpenPageInNewTab,
|
||||
onToggleFavoritePage,
|
||||
onToggleTrashPage,
|
||||
}) => {
|
||||
const { id, favorite } = pageMeta;
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const OperationMenu = (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onToggleFavoritePage(id);
|
||||
toast(
|
||||
favorite ? t('Removed from Favorites') : t('Added to Favorites')
|
||||
);
|
||||
}}
|
||||
icon={favorite ? <FavoritedIcon /> : <FavoriteIcon />}
|
||||
>
|
||||
{favorite ? t('Remove from favorites') : t('Add to Favorites')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onOpenPageInNewTab(id);
|
||||
}}
|
||||
icon={<OpenInNewIcon />}
|
||||
>
|
||||
{t('Open in new tab')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
icon={<DeleteTemporarilyIcon />}
|
||||
>
|
||||
{t('Delete')}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<FlexWrapper alignItems="center" justifyContent="center">
|
||||
<Menu
|
||||
content={OperationMenu}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton darker={true}>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</FlexWrapper>
|
||||
<Confirm
|
||||
open={open}
|
||||
title={t('Delete page?')}
|
||||
content={t('will be permanently deleted', {
|
||||
title: pageMeta.title || 'Untitled',
|
||||
})}
|
||||
confirmText={t('Delete')}
|
||||
confirmType="danger"
|
||||
onConfirm={() => {
|
||||
onToggleTrashPage(id);
|
||||
toast(t('Deleted'));
|
||||
setOpen(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type TrashOperationCellProps = {
|
||||
pageMeta: PageMeta;
|
||||
onPermanentlyDeletePage: (pageId: string) => void;
|
||||
onRestorePage: (pageId: string) => void;
|
||||
onOpenPage: (pageId: string) => void;
|
||||
};
|
||||
|
||||
export const TrashOperationCell: React.FC<TrashOperationCellProps> = ({
|
||||
pageMeta,
|
||||
onPermanentlyDeletePage,
|
||||
onRestorePage,
|
||||
onOpenPage,
|
||||
}) => {
|
||||
const { id, title } = pageMeta;
|
||||
// const { openPage, getPageMeta } = usePageHelper();
|
||||
// const { toggleDeletePage, permanentlyDeletePage } = usePageHelper();
|
||||
// const confirm = useConfirm(store => store.confirm);
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<FlexWrapper>
|
||||
<Tooltip content={t('Restore it')} placement="top-start">
|
||||
<IconButton
|
||||
darker={true}
|
||||
style={{ marginRight: '12px' }}
|
||||
onClick={() => {
|
||||
onRestorePage(id);
|
||||
toast(t('restored', { title: title || 'Untitled' }));
|
||||
onOpenPage(id);
|
||||
}}
|
||||
>
|
||||
<ResetIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content={t('Delete permanently')} placement="top-start">
|
||||
<IconButton
|
||||
darker={true}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<DeletePermanentlyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Confirm
|
||||
title={t('Delete permanently?')}
|
||||
content={t("Once deleted, you can't undo this action.")}
|
||||
confirmText={t('Delete')}
|
||||
confirmType="danger"
|
||||
open={open}
|
||||
onConfirm={() => {
|
||||
onPermanentlyDeletePage(id);
|
||||
toast(t('Permanently deleted'));
|
||||
setOpen(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,216 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from '@affine/component';
|
||||
import { Content, IconButton, toast, Tooltip } from '@affine/component';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import {
|
||||
EdgelessIcon,
|
||||
FavoritedIcon,
|
||||
FavoriteIcon,
|
||||
PaperIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { PageMeta } from '@blocksuite/store';
|
||||
import { useMediaQuery, useTheme as useMuiTheme } from '@mui/material';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
usePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '../../../../hooks/use-page-meta';
|
||||
import { useTheme } from '../../../../providers/ThemeProvider';
|
||||
import { BlockSuiteWorkspace } from '../../../../shared';
|
||||
import DateCell from './DateCell';
|
||||
import Empty from './Empty';
|
||||
import { OperationCell, TrashOperationCell } from './OperationCell';
|
||||
import {
|
||||
StyledTableContainer,
|
||||
StyledTableRow,
|
||||
StyledTitleLink,
|
||||
StyledTitleWrapper,
|
||||
} from './styles';
|
||||
const FavoriteTag = ({
|
||||
pageMeta: { favorite, id },
|
||||
}: {
|
||||
pageMeta: PageMeta;
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Tooltip
|
||||
content={favorite ? t('Favorited') : t('Favorite')}
|
||||
placement="top-start"
|
||||
>
|
||||
<IconButton
|
||||
darker={true}
|
||||
iconSize={[20, 20]}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
// toggleFavoritePage(id);
|
||||
toast(
|
||||
favorite ? t('Removed from Favorites') : t('Added to Favorites')
|
||||
);
|
||||
}}
|
||||
style={{
|
||||
color: favorite ? theme.colors.primaryColor : theme.colors.iconColor,
|
||||
}}
|
||||
className={favorite ? '' : 'favorite-button'}
|
||||
>
|
||||
{favorite ? (
|
||||
<FavoritedIcon data-testid="favorited-icon" />
|
||||
) : (
|
||||
<FavoriteIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
type PageListProps = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
isPublic?: boolean;
|
||||
listType?: 'all' | 'trash' | 'favorite';
|
||||
onClickPage: (pageId: string, newTab?: boolean) => void;
|
||||
};
|
||||
|
||||
const filter = {
|
||||
all: (pageMeta: PageMeta) => !pageMeta.trash,
|
||||
trash: (pageMeta: PageMeta) => pageMeta.trash,
|
||||
favorite: (pageMeta: PageMeta) => pageMeta.favorite,
|
||||
};
|
||||
|
||||
export const PageList: React.FC<PageListProps> = ({
|
||||
blockSuiteWorkspace,
|
||||
isPublic = false,
|
||||
listType,
|
||||
onClickPage,
|
||||
}) => {
|
||||
const pageList = usePageMeta(blockSuiteWorkspace);
|
||||
const helper = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const { t } = useTranslation();
|
||||
const theme = useMuiTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
const isTrash = listType === 'trash';
|
||||
const list = useMemo(
|
||||
() => pageList.filter(filter[listType ?? 'all']),
|
||||
[pageList, listType]
|
||||
);
|
||||
if (list.length === 0) {
|
||||
return <Empty listType={listType} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledTableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{matches && (
|
||||
<>
|
||||
<TableCell proportion={0.5}>{t('Title')}</TableCell>
|
||||
<TableCell proportion={0.2}>{t('Created')}</TableCell>
|
||||
<TableCell proportion={0.2}>
|
||||
{isTrash ? t('Moved to Trash') : t('Updated')}
|
||||
</TableCell>
|
||||
<TableCell proportion={0.1}></TableCell>
|
||||
</>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{list.map((pageMeta, index) => {
|
||||
return (
|
||||
<StyledTableRow
|
||||
data-testid={`page-list-item-${pageMeta.id}}`}
|
||||
key={`${pageMeta.id}-${index}`}
|
||||
>
|
||||
<TableCell
|
||||
onClick={() => {
|
||||
onClickPage(pageMeta.id);
|
||||
}}
|
||||
>
|
||||
<StyledTitleWrapper>
|
||||
<StyledTitleLink>
|
||||
{pageMeta.mode === 'edgeless' ? (
|
||||
<EdgelessIcon />
|
||||
) : (
|
||||
<PaperIcon />
|
||||
)}
|
||||
<Content ellipsis={true} color="inherit">
|
||||
{pageMeta.title || t('Untitled')}
|
||||
</Content>
|
||||
</StyledTitleLink>
|
||||
{!isTrash && <FavoriteTag pageMeta={pageMeta} />}
|
||||
</StyledTitleWrapper>
|
||||
</TableCell>
|
||||
{matches && (
|
||||
<>
|
||||
<DateCell
|
||||
pageMeta={pageMeta}
|
||||
dateKey="createDate"
|
||||
onClick={() => {
|
||||
onClickPage(pageMeta.id);
|
||||
}}
|
||||
/>
|
||||
<DateCell
|
||||
pageMeta={pageMeta}
|
||||
dateKey={isTrash ? 'trashDate' : 'updatedDate'}
|
||||
backupKey={isTrash ? 'trashDate' : 'createDate'}
|
||||
onClick={() => {
|
||||
onClickPage(pageMeta.id);
|
||||
}}
|
||||
/>
|
||||
{!isPublic && (
|
||||
<TableCell
|
||||
style={{ padding: 0 }}
|
||||
data-testid={`more-actions-${pageMeta.id}`}
|
||||
>
|
||||
{isTrash ? (
|
||||
<TrashOperationCell
|
||||
pageMeta={pageMeta}
|
||||
onPermanentlyDeletePage={pageId => {
|
||||
blockSuiteWorkspace.removePage(pageId);
|
||||
}}
|
||||
onRestorePage={() => {
|
||||
helper.setPageMeta(pageMeta.id, {
|
||||
trash: false,
|
||||
});
|
||||
}}
|
||||
onOpenPage={pageId => {
|
||||
onClickPage(pageId, false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<OperationCell
|
||||
pageMeta={pageMeta}
|
||||
onOpenPageInNewTab={pageId => {
|
||||
onClickPage(pageId, true);
|
||||
}}
|
||||
onToggleFavoritePage={(pageId: string) => {
|
||||
helper.setPageMeta(pageId, {
|
||||
favorite: !pageMeta.favorite,
|
||||
});
|
||||
}}
|
||||
onToggleTrashPage={() => {
|
||||
helper.setPageMeta(pageMeta.id, {
|
||||
trash: !pageMeta.trash,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</StyledTableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageList;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
import { TableRow } from '@affine/component';
|
||||
|
||||
export const StyledTableContainer = styled.div(() => {
|
||||
return {
|
||||
height: 'calc(100vh - 60px)',
|
||||
padding: '78px 72px',
|
||||
overflowY: 'auto',
|
||||
};
|
||||
});
|
||||
export const StyledTitleWrapper = styled.div(({ theme }) => {
|
||||
return {
|
||||
...displayFlex('flex-start', 'center'),
|
||||
a: {
|
||||
color: 'inherit',
|
||||
},
|
||||
'a:visited': {
|
||||
color: 'unset',
|
||||
},
|
||||
'a:hover': {
|
||||
color: theme.colors.primaryColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledTitleLink = styled.div(({ theme }) => {
|
||||
return {
|
||||
maxWidth: '80%',
|
||||
marginRight: '18px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
color: theme.colors.textColor,
|
||||
'>svg': {
|
||||
fontSize: '24px',
|
||||
marginRight: '12px',
|
||||
color: theme.colors.iconColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledTableRow = styled(TableRow)(() => {
|
||||
return {
|
||||
cursor: 'pointer',
|
||||
'.favorite-button': {
|
||||
display: 'none',
|
||||
},
|
||||
'&:hover': {
|
||||
'.favorite-button': {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user