From 9ff7dbffb7ed4135cf2efe4b436673be27d8e170 Mon Sep 17 00:00:00 2001 From: Whitewater Date: Mon, 15 May 2023 08:50:43 -0700 Subject: [PATCH] feat: supports sort all page (#2356) --- .../block-suite-page-list/index.tsx | 4 +- .../src/components/page-list/all-page.tsx | 97 ++++++++++++++----- .../src/components/page-list/use-sorter.ts | 82 ++++++++++++++++ .../src/stories/page-list.stories.tsx | 10 +- packages/component/src/ui/table/interface.ts | 1 + packages/component/src/ui/table/styles.ts | 45 ++++++--- 6 files changed, 192 insertions(+), 47 deletions(-) create mode 100644 packages/component/src/components/page-list/use-sorter.ts diff --git a/apps/web/src/components/blocksuite/block-suite-page-list/index.tsx b/apps/web/src/components/blocksuite/block-suite-page-list/index.tsx index 4e0500fa45..5360933bd8 100644 --- a/apps/web/src/components/blocksuite/block-suite-page-list/index.tsx +++ b/apps/web/src/components/blocksuite/block-suite-page-list/index.tsx @@ -98,7 +98,7 @@ export const BlockSuitePageList: React.FC = ({ pageId: pageMeta.id, title: pageMeta.title, createDate: formatDate(pageMeta.createDate), - updatedDate: formatDate(pageMeta.updatedDate), + updatedDate: formatDate(pageMeta.updatedDate ?? pageMeta.createDate), onClickPage: () => onOpenPage(pageMeta.id), onClickRestore: () => { restoreFromTrash(pageMeta.id); @@ -125,7 +125,7 @@ export const BlockSuitePageList: React.FC = ({ favorite: !!pageMeta.favorite, isPublicPage: !!pageMeta.isPublic, createDate: formatDate(pageMeta.createDate), - updatedDate: formatDate(pageMeta.updatedDate), + updatedDate: formatDate(pageMeta.updatedDate ?? pageMeta.createDate), onClickPage: () => onOpenPage(pageMeta.id), onOpenPageInNewTab: () => onOpenPage(pageMeta.id, true), onClickRestore: () => { diff --git a/packages/component/src/components/page-list/all-page.tsx b/packages/component/src/components/page-list/all-page.tsx index 2f79e1739a..de10a620b7 100644 --- a/packages/component/src/components/page-list/all-page.tsx +++ b/packages/component/src/components/page-list/all-page.tsx @@ -11,7 +11,12 @@ import { } from '@affine/component'; import { OperationCell, TrashOperationCell } from '@affine/component/page-list'; 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 { forwardRef } from 'react'; @@ -21,15 +26,14 @@ import { StyledTitleLink, StyledTitleWrapper, } from './styles'; - -export type FavoriteTagProps = { - active: boolean; -}; +import { useSorter } from './use-sorter'; // eslint-disable-next-line react/display-name const FavoriteTag = forwardRef< HTMLButtonElement, - FavoriteTagProps & Omit + { + active: boolean; + } & Omit >(({ active, onClick, ...props }, ref) => { const t = useAFFiNEI18N(); return ( @@ -64,6 +68,9 @@ const FavoriteTag = forwardRef< export type PageListProps = { isPublicWorkspace?: boolean; list: ListData[]; + /** + * @deprecated + */ listType: 'all' | 'favorite' | 'shared' | 'public'; onClickPage: (pageId: string, newTab?: boolean) => void; }; @@ -115,35 +122,77 @@ export const PageList: React.FC = ({ listType, }) => { const t = useAFFiNEI18N(); + const sorter = useSorter({ + data: list, + key: 'createDate', + order: 'desc', + }); const isShared = listType === 'shared'; const theme = useTheme(); const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm')); if (isSmallDevices) { - return ; + return ; } 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 ( - {t['Title']()} - {t['Created']()} - - {isShared - ? // TODO add i18n - 'Shared' - : t['Updated']()} - - + {titleList.map(({ key, text, proportion, sortable = true }) => ( + sorter.shiftOrder(key as keyof ListData) + : undefined + } + > +
+ {text} + {sorter.key === key && + (sorter.order === 'asc' ? ( + + ) : ( + + ))} +
+
+ ))}
); }; - const ListItems = list.map( + const ListItems = sorter.data.map( ( { pageId, @@ -170,13 +219,6 @@ export const PageList: React.FC = ({ icon={icon} text={title || t['Untitled']()} data-testid="title" - suffix={ - - } onClick={onClickPage} /> = ({ {!isPublicWorkspace && ( + = { + data: T[]; + key: keyof T; + order: 'asc' | 'desc' | 'none'; +}; + +const defaultSortingFn = >( + 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 = >({ + data, + ...defaultSorter +}: Sorter & { order: 'asc' | 'desc' }) => { + const [sorter, setSorter] = useState, '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>) => + setSorter({ ...sorter, ...newVal }), + shiftOrder, + resetSorter: () => setSorter(defaultSorter), + }; +}; diff --git a/packages/component/src/stories/page-list.stories.tsx b/packages/component/src/stories/page-list.stories.tsx index 7ffb6ea246..02cdda6313 100644 --- a/packages/component/src/stories/page-list.stories.tsx +++ b/packages/component/src/stories/page-list.stories.tsx @@ -50,9 +50,9 @@ AffineAllPageList.args = { favorite: false, icon: , 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', - updatedDate: '2021-01-01', + updatedDate: '2021-01-02', bookmarkPage: () => toast('Bookmark page'), onClickPage: () => toast('Click page'), onDisablePublicSharing: () => toast('Disable public sharing'), @@ -64,8 +64,8 @@ AffineAllPageList.args = { favorite: true, isPublicPage: false, icon: , - title: 'Favorited Page', - createDate: '2021-01-01', + title: '2 Favorited Page', + createDate: '2021-01-02', updatedDate: '2021-01-01', bookmarkPage: () => toast('Bookmark page'), onClickPage: () => toast('Click page'), @@ -90,7 +90,7 @@ AffineTrashPageList.args = { pageId: '1', icon: , title: 'Example Page', - updatedDate: '2021-01-01', + updatedDate: '2021-02-01', createDate: '2021-01-01', trashDate: '2021-01-01', onClickPage: () => toast('Click page'), diff --git a/packages/component/src/ui/table/interface.ts b/packages/component/src/ui/table/interface.ts index ebd6b3515a..f9ea744735 100644 --- a/packages/component/src/ui/table/interface.ts +++ b/packages/component/src/ui/table/interface.ts @@ -4,6 +4,7 @@ export type TableCellProps = { align?: 'left' | 'right' | 'center'; ellipsis?: boolean; proportion?: number; + active?: boolean; style?: CSSProperties; } & PropsWithChildren & HTMLAttributes; diff --git a/packages/component/src/ui/table/styles.ts b/packages/component/src/ui/table/styles.ts index 533423187f..b4882e4831 100644 --- a/packages/component/src/ui/table/styles.ts +++ b/packages/component/src/ui/table/styles.ts @@ -21,25 +21,40 @@ export const StyledTableBody = styled('tbody')(() => { }); export const StyledTableCell = styled('td')< - Pick ->(({ align = 'left', ellipsis = false, proportion }) => { - const width = proportion ? `${proportion * 100}%` : 'auto'; - return { - width, - height: '52px', - lineHeight: '52px', - padding: '0 30px', - boxSizing: 'border-box', - textAlign: align, - verticalAlign: 'middle', - ...(ellipsis ? textEllipsis(1) : {}), - overflowWrap: 'break-word', - }; -}); + Pick< + TableCellProps, + 'ellipsis' | 'align' | 'proportion' | 'active' | 'onClick' + > +>( + ({ + align = 'left', + ellipsis = false, + proportion, + active = false, + onClick, + }) => { + 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')(() => { return { fontWeight: 500, + color: 'var(--affine-text-secondary-color)', tr: { td: { whiteSpace: 'nowrap',