feat: supports sort all page (#2356)

This commit is contained in:
Whitewater
2023-05-15 08:50:43 -07:00
committed by GitHub
parent 0c561da061
commit 9ff7dbffb7
6 changed files with 192 additions and 47 deletions

View File

@@ -98,7 +98,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
pageId: pageMeta.id, pageId: pageMeta.id,
title: pageMeta.title, title: pageMeta.title,
createDate: formatDate(pageMeta.createDate), createDate: formatDate(pageMeta.createDate),
updatedDate: formatDate(pageMeta.updatedDate), updatedDate: formatDate(pageMeta.updatedDate ?? pageMeta.createDate),
onClickPage: () => onOpenPage(pageMeta.id), onClickPage: () => onOpenPage(pageMeta.id),
onClickRestore: () => { onClickRestore: () => {
restoreFromTrash(pageMeta.id); restoreFromTrash(pageMeta.id);
@@ -125,7 +125,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
favorite: !!pageMeta.favorite, favorite: !!pageMeta.favorite,
isPublicPage: !!pageMeta.isPublic, isPublicPage: !!pageMeta.isPublic,
createDate: formatDate(pageMeta.createDate), createDate: formatDate(pageMeta.createDate),
updatedDate: formatDate(pageMeta.updatedDate), updatedDate: formatDate(pageMeta.updatedDate ?? pageMeta.createDate),
onClickPage: () => onOpenPage(pageMeta.id), onClickPage: () => onOpenPage(pageMeta.id),
onOpenPageInNewTab: () => onOpenPage(pageMeta.id, true), onOpenPageInNewTab: () => onOpenPage(pageMeta.id, true),
onClickRestore: () => { onClickRestore: () => {

View File

@@ -11,7 +11,12 @@ import {
} from '@affine/component'; } from '@affine/component';
import { OperationCell, TrashOperationCell } from '@affine/component/page-list'; import { OperationCell, TrashOperationCell } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons'; import {
ArrowDownBigIcon,
ArrowUpBigIcon,
FavoritedIcon,
FavoriteIcon,
} from '@blocksuite/icons';
import { useMediaQuery, useTheme } from '@mui/material'; import { useMediaQuery, useTheme } from '@mui/material';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
@@ -21,15 +26,14 @@ import {
StyledTitleLink, StyledTitleLink,
StyledTitleWrapper, StyledTitleWrapper,
} from './styles'; } from './styles';
import { useSorter } from './use-sorter';
export type FavoriteTagProps = {
active: boolean;
};
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
const FavoriteTag = forwardRef< const FavoriteTag = forwardRef<
HTMLButtonElement, HTMLButtonElement,
FavoriteTagProps & Omit<IconButtonProps, 'children'> {
active: boolean;
} & Omit<IconButtonProps, 'children'>
>(({ active, onClick, ...props }, ref) => { >(({ active, onClick, ...props }, ref) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
return ( return (
@@ -64,6 +68,9 @@ const FavoriteTag = forwardRef<
export type PageListProps = { export type PageListProps = {
isPublicWorkspace?: boolean; isPublicWorkspace?: boolean;
list: ListData[]; list: ListData[];
/**
* @deprecated
*/
listType: 'all' | 'favorite' | 'shared' | 'public'; listType: 'all' | 'favorite' | 'shared' | 'public';
onClickPage: (pageId: string, newTab?: boolean) => void; onClickPage: (pageId: string, newTab?: boolean) => void;
}; };
@@ -115,35 +122,77 @@ export const PageList: React.FC<PageListProps> = ({
listType, listType,
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const sorter = useSorter<ListData>({
data: list,
key: 'createDate',
order: 'desc',
});
const isShared = listType === 'shared'; 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={list} />; return <PageListMobileView list={sorter.data} />;
} }
const ListHead = () => { const ListHead = () => {
const t = useAFFiNEI18N(); 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 ( return (
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell proportion={0.5}>{t['Title']()}</TableCell> {titleList.map(({ key, text, proportion, sortable = true }) => (
<TableCell proportion={0.2}>{t['Created']()}</TableCell> <TableCell
<TableCell proportion={0.2}> key={key}
{isShared proportion={proportion}
? // TODO add i18n active={sorter.key === key}
'Shared' onClick={
: t['Updated']()} sortable
</TableCell> ? () => sorter.shiftOrder(key as keyof ListData)
<TableCell proportion={0.1}></TableCell> : 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> </TableRow>
</TableHead> </TableHead>
); );
}; };
const ListItems = list.map( const ListItems = sorter.data.map(
( (
{ {
pageId, pageId,
@@ -170,13 +219,6 @@ export const PageList: React.FC<PageListProps> = ({
icon={icon} icon={icon}
text={title || t['Untitled']()} text={title || t['Untitled']()}
data-testid="title" data-testid="title"
suffix={
<FavoriteTag
className={favorite ? '' : 'favorite-button'}
onClick={bookmarkPage}
active={!!favorite}
/>
}
onClick={onClickPage} onClick={onClickPage}
/> />
<TableCell <TableCell
@@ -195,9 +237,14 @@ export const PageList: React.FC<PageListProps> = ({
</TableCell> </TableCell>
{!isPublicWorkspace && ( {!isPublicWorkspace && (
<TableCell <TableCell
style={{ padding: 0 }} style={{ padding: 0, display: 'flex', alignItems: 'center' }}
data-testid={`more-actions-${pageId}`} data-testid={`more-actions-${pageId}`}
> >
<FavoriteTag
className={favorite ? '' : 'favorite-button'}
onClick={bookmarkPage}
active={!!favorite}
/>
<OperationCell <OperationCell
title={title} title={title}
favorite={favorite} favorite={favorite}

View File

@@ -0,0 +1,82 @@
import { useState } from 'react';
type Sorter<T> = {
data: T[];
key: keyof T;
order: 'asc' | 'desc' | 'none';
};
const defaultSortingFn = <T extends Record<keyof any, unknown>>(
ctx: {
key: keyof T;
order: 'asc' | 'desc' | 'none';
},
a: T,
b: T
) => {
const valA = a[ctx.key];
const valB = b[ctx.key];
const revert = ctx.order === 'desc';
if (typeof valA !== typeof valB) {
return 0;
}
if (typeof valA === 'string') {
return valA.localeCompare(valB as string) * (revert ? 1 : -1);
}
if (typeof valA === 'number') {
return valA - (valB as number) * (revert ? 1 : -1);
}
return 0;
};
export const useSorter = <T extends Record<keyof any, unknown>>({
data,
...defaultSorter
}: Sorter<T> & { order: 'asc' | 'desc' }) => {
const [sorter, setSorter] = useState<Omit<Sorter<T>, 'data'>>({
...defaultSorter,
// We should not show sorting icon at first time
order: 'none',
});
const sortCtx =
sorter.order === 'none'
? {
key: defaultSorter.key,
order: defaultSorter.order,
}
: {
key: sorter.key,
order: sorter.order,
};
const sortingFn = (a: T, b: T) => defaultSortingFn(sortCtx, a, b);
const sortedData = data.sort(sortingFn);
const shiftOrder = (key?: keyof T) => {
const orders = ['asc', 'desc', 'none'] as const;
if (key && key !== sorter.key) {
// Key changed
setSorter({
...sorter,
key,
order: orders[0],
});
return;
}
setSorter({
...sorter,
order: orders[(orders.indexOf(sorter.order) + 1) % orders.length],
});
};
return {
data: sortedData,
order: sorter.order,
key: sorter.order !== 'none' ? sorter.key : null,
/**
* @deprecated In most cases, we no necessary use `setSorter` directly.
*/
updateSorter: (newVal: Partial<Sorter<T>>) =>
setSorter({ ...sorter, ...newVal }),
shiftOrder,
resetSorter: () => setSorter(defaultSorter),
};
};

View File

@@ -50,9 +50,9 @@ AffineAllPageList.args = {
favorite: false, favorite: false,
icon: <PageIcon />, icon: <PageIcon />,
isPublicPage: true, isPublicPage: true,
title: 'Example Public Page with long title that will be truncated', title: '1 Example Public Page with long title that will be truncated',
createDate: '2021-01-01', createDate: '2021-01-01',
updatedDate: '2021-01-01', updatedDate: '2021-01-02',
bookmarkPage: () => toast('Bookmark page'), bookmarkPage: () => toast('Bookmark page'),
onClickPage: () => toast('Click page'), onClickPage: () => toast('Click page'),
onDisablePublicSharing: () => toast('Disable public sharing'), onDisablePublicSharing: () => toast('Disable public sharing'),
@@ -64,8 +64,8 @@ AffineAllPageList.args = {
favorite: true, favorite: true,
isPublicPage: false, isPublicPage: false,
icon: <PageIcon />, icon: <PageIcon />,
title: 'Favorited Page', title: '2 Favorited Page',
createDate: '2021-01-01', createDate: '2021-01-02',
updatedDate: '2021-01-01', updatedDate: '2021-01-01',
bookmarkPage: () => toast('Bookmark page'), bookmarkPage: () => toast('Bookmark page'),
onClickPage: () => toast('Click page'), onClickPage: () => toast('Click page'),
@@ -90,7 +90,7 @@ AffineTrashPageList.args = {
pageId: '1', pageId: '1',
icon: <PageIcon />, icon: <PageIcon />,
title: 'Example Page', title: 'Example Page',
updatedDate: '2021-01-01', updatedDate: '2021-02-01',
createDate: '2021-01-01', createDate: '2021-01-01',
trashDate: '2021-01-01', trashDate: '2021-01-01',
onClickPage: () => toast('Click page'), onClickPage: () => toast('Click page'),

View File

@@ -4,6 +4,7 @@ export type TableCellProps = {
align?: 'left' | 'right' | 'center'; align?: 'left' | 'right' | 'center';
ellipsis?: boolean; ellipsis?: boolean;
proportion?: number; proportion?: number;
active?: boolean;
style?: CSSProperties; style?: CSSProperties;
} & PropsWithChildren & } & PropsWithChildren &
HTMLAttributes<HTMLTableCellElement>; HTMLAttributes<HTMLTableCellElement>;

View File

@@ -21,25 +21,40 @@ export const StyledTableBody = styled('tbody')(() => {
}); });
export const StyledTableCell = styled('td')< export const StyledTableCell = styled('td')<
Pick<TableCellProps, 'ellipsis' | 'align' | 'proportion'> Pick<
>(({ align = 'left', ellipsis = false, proportion }) => { TableCellProps,
const width = proportion ? `${proportion * 100}%` : 'auto'; 'ellipsis' | 'align' | 'proportion' | 'active' | 'onClick'
return { >
width, >(
height: '52px', ({
lineHeight: '52px', align = 'left',
padding: '0 30px', ellipsis = false,
boxSizing: 'border-box', proportion,
textAlign: align, active = false,
verticalAlign: 'middle', onClick,
...(ellipsis ? textEllipsis(1) : {}), }) => {
overflowWrap: 'break-word', const width = proportion ? `${proportion * 100}%` : 'auto';
}; return {
}); width,
height: '52px',
lineHeight: '52px',
padding: '0 30px',
boxSizing: 'border-box',
textAlign: align,
verticalAlign: 'middle',
overflowWrap: 'break-word',
userSelect: 'none',
...(active ? { color: 'var(--affine-text-primary-color)' } : {}),
...(ellipsis ? textEllipsis(1) : {}),
...(onClick ? { cursor: 'pointer' } : {}),
};
}
);
export const StyledTableHead = styled('thead')(() => { export const StyledTableHead = styled('thead')(() => {
return { return {
fontWeight: 500, fontWeight: 500,
color: 'var(--affine-text-secondary-color)',
tr: { tr: {
td: { td: {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',