mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 22:07:09 +08:00
feat: supports sort all page (#2356)
This commit is contained in:
@@ -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: () => {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
82
packages/component/src/components/page-list/use-sorter.ts
Normal file
82
packages/component/src/components/page-list/use-sorter.ts
Normal 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),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user