feat: group all page by date (#2532)

Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
Whitewater 2023-05-25 22:23:51 -07:00 committed by GitHub
parent 60057c666d
commit 7dcbe64d4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 220 additions and 62 deletions

View File

@ -5,6 +5,7 @@ import {
TableHead, TableHead,
TableRow, TableRow,
} from '@affine/component'; } from '@affine/component';
import { DEFAULT_SORT_KEY } from "@affine/env/constant";
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowDownBigIcon, ArrowUpBigIcon } from '@blocksuite/icons'; import { ArrowDownBigIcon, ArrowUpBigIcon } from '@blocksuite/icons';
import { useMediaQuery, useTheme } from '@mui/material'; import { useMediaQuery, useTheme } from '@mui/material';
@ -111,7 +112,7 @@ export const PageList = ({
}: PageListProps) => { }: PageListProps) => {
const sorter = useSorter<ListData>({ const sorter = useSorter<ListData>({
data: list, data: list,
key: 'updatedDate', key: DEFAULT_SORT_KEY,
order: 'desc', order: 'desc',
}); });
@ -128,6 +129,14 @@ export const PageList = ({
); );
} }
const groupKey =
sorter.key === 'createDate' || sorter.key === 'updatedDate'
? sorter.key
: // default sort
!sorter.key
? DEFAULT_SORT_KEY
: undefined;
return ( return (
<StyledTableContainer> <StyledTableContainer>
<Table> <Table>
@ -139,6 +148,7 @@ export const PageList = ({
/> />
<AllPagesBody <AllPagesBody
isPublicWorkspace={isPublicWorkspace} isPublicWorkspace={isPublicWorkspace}
groupKey={groupKey}
data={sorter.data} data={sorter.data}
/> />
</Table> </Table>

View File

@ -1,29 +1,35 @@
import { TableBody, TableCell } from '@affine/component'; import { TableBody, TableCell } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMediaQuery, useTheme } from '@mui/material'; import { useMediaQuery, useTheme } from '@mui/material';
import { Fragment } from 'react';
import { FavoriteTag } from './components/favorite-tag'; import { FavoriteTag } from './components/favorite-tag';
import { TitleCell } from './components/title-cell'; import { TitleCell } from './components/title-cell';
import { OperationCell } from './operation-cell'; import { OperationCell } from './operation-cell';
import { StyledTableRow } from './styles'; import { StyledTableRow } from './styles';
import type { ListData } from './type'; import type { DateKey, ListData } from './type';
import { useDateGroup } from './use-date-group';
import { formatDate } from './utils'; import { formatDate } from './utils';
export const AllPagesBody = ({ export const AllPagesBody = ({
isPublicWorkspace, isPublicWorkspace,
data, data,
groupKey,
}: { }: {
isPublicWorkspace: boolean; isPublicWorkspace: boolean;
data: ListData[]; data: ListData[];
groupKey?: DateKey;
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const theme = useTheme(); const theme = useTheme();
const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm')); const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm'));
const dataWithGroup = useDateGroup({ data, key: groupKey });
return ( return (
<TableBody> <TableBody>
{data.map( {dataWithGroup.map(
( (
{ {
groupName,
pageId, pageId,
title, title,
icon, icon,
@ -40,60 +46,74 @@ export const AllPagesBody = ({
index index
) => { ) => {
return ( return (
<StyledTableRow <Fragment key={pageId}>
data-testid={`page-list-item-${pageId}`} {groupName &&
key={`${pageId}-${index}`} (index === 0 ||
> dataWithGroup[index - 1].groupName !== groupName) && (
<TitleCell <StyledTableRow>
icon={icon} <TableCell
text={title || t['Untitled']()} style={{
data-testid="title" color: 'var(--affine-text-secondary-color)',
onClick={onClickPage} background: 'initial',
/> cursor: 'default',
<TableCell }}
data-testid="created-date" >
ellipsis={true} {groupName}
hidden={isSmallDevices} </TableCell>
onClick={onClickPage} </StyledTableRow>
> )}
{formatDate(createDate)} <StyledTableRow data-testid={`page-list-item-${pageId}`}>
</TableCell> <TitleCell
<TableCell icon={icon}
data-testid="updated-date" text={title || t['Untitled']()}
ellipsis={true} data-testid="title"
hidden={isSmallDevices} onClick={onClickPage}
onClick={onClickPage} />
>
{formatDate(updatedDate ?? createDate)}
</TableCell>
{!isPublicWorkspace && (
<TableCell <TableCell
style={{ data-testid="created-date"
padding: 0, ellipsis={true}
display: 'flex', hidden={isSmallDevices}
justifyContent: 'center', onClick={onClickPage}
alignItems: 'center',
gap: '10px',
}}
data-testid={`more-actions-${pageId}`}
> >
<FavoriteTag {formatDate(createDate)}
className={favorite ? '' : 'favorite-button'}
onClick={bookmarkPage}
active={!!favorite}
/>
<OperationCell
title={title}
favorite={favorite}
isPublic={isPublicPage}
onOpenPageInNewTab={onOpenPageInNewTab}
onToggleFavoritePage={bookmarkPage}
onRemoveToTrash={removeToTrash}
onDisablePublicSharing={onDisablePublicSharing}
/>
</TableCell> </TableCell>
)} <TableCell
</StyledTableRow> data-testid="updated-date"
ellipsis={true}
hidden={isSmallDevices}
onClick={onClickPage}
>
{formatDate(updatedDate ?? createDate)}
</TableCell>
{!isPublicWorkspace && (
<TableCell
style={{
padding: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '10px',
}}
data-testid={`more-actions-${pageId}`}
>
<FavoriteTag
className={favorite ? '' : 'favorite-button'}
onClick={bookmarkPage}
active={!!favorite}
/>
<OperationCell
title={title}
favorite={favorite}
isPublic={isPublicPage}
onOpenPageInNewTab={onOpenPageInNewTab}
onToggleFavoritePage={bookmarkPage}
onRemoveToTrash={removeToTrash}
onDisablePublicSharing={onDisablePublicSharing}
/>
</TableCell>
)}
</StyledTableRow>
</Fragment>
); );
} }
)} )}

View File

@ -71,7 +71,12 @@ export const AllPageListMobileView = ({
createNewPage={createNewPage} createNewPage={createNewPage}
createNewEdgeless={createNewEdgeless} createNewEdgeless={createNewEdgeless}
/> />
<AllPagesBody isPublicWorkspace={isPublicWorkspace} data={list} /> <AllPagesBody
isPublicWorkspace={isPublicWorkspace}
data={list}
// update groupKey after support sort by create date
groupKey="updatedDate"
/>
</Table> </Table>
</StyledTableContainer> </StyledTableContainer>
); );

View File

@ -1,3 +1,12 @@
/**
* Get the keys of an object type whose values are of a given type
*
* See https://stackoverflow.com/questions/54520676/in-typescript-how-to-get-the-keys-of-an-object-type-whose-values-are-of-a-given
*/
export type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];
export type ListData = { export type ListData = {
pageId: string; pageId: string;
icon: JSX.Element; icon: JSX.Element;
@ -13,6 +22,8 @@ export type ListData = {
onDisablePublicSharing: () => void; onDisablePublicSharing: () => void;
}; };
export type DateKey = KeysMatching<ListData, Date>;
export type TrashListData = { export type TrashListData = {
pageId: string; pageId: string;
icon: JSX.Element; icon: JSX.Element;

View File

@ -0,0 +1,65 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { DateKey, ListData } from './type';
import {
isLastMonth,
isLastWeek,
isLastYear,
isToday,
isYesterday,
} from './utils';
export const useDateGroup = ({
data,
key,
}: {
data: ListData[];
key?: DateKey;
}) => {
const t = useAFFiNEI18N();
if (!key) {
return data.map(item => ({ ...item, groupName: '' }));
}
const fallbackGroup = {
id: 'earlier',
label: t['com.affine.earlier'](),
match: (_date: Date) => true,
};
const groups = [
{
id: 'today',
label: t['com.affine.today'](),
match: (date: Date) => isToday(date),
},
{
id: 'yesterday',
label: t['com.affine.yesterday'](),
match: (date: Date) => isYesterday(date) && !isToday(date),
},
{
id: 'lastWeek',
label: t['com.affine.lastWeek'](),
match: (date: Date) => isLastWeek(date) && !isYesterday(date),
},
{
id: 'lastMonth',
label: t['com.affine.lastMonth'](),
match: (date: Date) => isLastMonth(date) && !isLastWeek(date),
},
{
id: 'lastYear',
label: t['com.affine.lastYear'](),
match: (date: Date) => isLastYear(date) && !isLastMonth(date),
},
] as const;
return data.map(item => {
const group = groups.find(group => group.match(item[key])) ?? fallbackGroup;
return {
...item,
groupName: group.label,
};
});
};

View File

@ -1,11 +1,51 @@
const isToday = (date: Date) => { export function isToday(date: Date): boolean {
const today = new Date(); const today = new Date();
return ( return (
date.getDate() == today.getDate() && date.getDate() == today.getDate() &&
date.getMonth() == today.getMonth() && date.getMonth() == today.getMonth() &&
date.getFullYear() == today.getFullYear() date.getFullYear() == today.getFullYear()
); );
}; }
export function isYesterday(date: Date): boolean {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return (
date.getFullYear() === yesterday.getFullYear() &&
date.getMonth() === yesterday.getMonth() &&
date.getDate() === yesterday.getDate()
);
}
export function isLastWeek(date: Date): boolean {
const today = new Date();
const lastWeek = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() - 7
);
return date >= lastWeek && date < today;
}
export function isLastMonth(date: Date): boolean {
const today = new Date();
const lastMonth = new Date(
today.getFullYear(),
today.getMonth() - 1,
today.getDate()
);
return date >= lastMonth && date < today;
}
export function isLastYear(date: Date): boolean {
const today = new Date();
const lastYear = new Date(
today.getFullYear() - 1,
today.getMonth(),
today.getDate()
);
return date >= lastYear && date < today;
}
export const formatDate = (date: Date): string => { export const formatDate = (date: Date): string => {
// yyyy-mm-dd MM-DD HH:mm // yyyy-mm-dd MM-DD HH:mm

View File

@ -79,7 +79,7 @@ AffineAllPageList.args = {
removeToTrash: () => toast('Remove to trash'), removeToTrash: () => toast('Remove to trash'),
}, },
{ {
pageId: '1', pageId: '3',
favorite: false, favorite: false,
icon: <PageIcon />, icon: <PageIcon />,
isPublicPage: true, isPublicPage: true,

View File

@ -5,6 +5,7 @@ export const DEFAULT_WORKSPACE_NAME = 'Demo Workspace';
export const UNTITLED_WORKSPACE_NAME = 'Untitled'; export const UNTITLED_WORKSPACE_NAME = 'Untitled';
export const DEFAULT_HELLO_WORLD_PAGE_ID = 'hello-world'; export const DEFAULT_HELLO_WORLD_PAGE_ID = 'hello-world';
export const DEFAULT_SORT_KEY = 'updatedDate';
export const enum MessageCode { export const enum MessageCode {
loginError, loginError,
noPermission, noPermission,

View File

@ -279,6 +279,12 @@
"com.affine.updater.update-available": "Update available", "com.affine.updater.update-available": "Update available",
"com.affine.updater.open-download-page": "Open download page", "com.affine.updater.open-download-page": "Open download page",
"com.affine.updater.restart-to-update": "Restart to install update", "com.affine.updater.restart-to-update": "Restart to install update",
"com.affine.today": "Today",
"com.affine.yesterday": "Yesterday",
"com.affine.lastWeek": "Last week",
"com.affine.lastMonth": "Last month",
"com.affine.lastYear": "Last year",
"com.affine.earlier": "Earlier",
"com.affine.workspace.cannot-delete": "You cannot delete the last workspace", "com.affine.workspace.cannot-delete": "You cannot delete the last workspace",
"FILE_ALREADY_EXISTS": "File already exists", "FILE_ALREADY_EXISTS": "File already exists",
"others": "Others", "others": "Others",

View File

@ -62,7 +62,7 @@ test('access public workspace page', async ({ page, browser }) => {
timeout: 10000, timeout: 10000,
}); });
await clickSideBarAllPageButton(page); await clickSideBarAllPageButton(page);
await page.locator('tr').nth(1).click(); await page.locator('tr').nth(2).click();
const url = page.url(); const url = page.url();
const context = await browser.newContext(); const context = await browser.newContext();
const page2 = await context.newPage(); const page2 = await context.newPage();

View File

@ -11,9 +11,9 @@ test('should broadcast a message to all debug pages', async ({
await page.waitForSelector('#__next'); await page.waitForSelector('#__next');
await page2.waitForSelector('#__next'); await page2.waitForSelector('#__next');
await page.click('[data-testid="create-page"]'); await page.click('[data-testid="create-page"]');
expect(await page.locator('tr').count()).toBe(2);
expect(await page2.locator('tr').count()).toBe(2);
await page2.click('[data-testid="create-page"]');
expect(await page.locator('tr').count()).toBe(3); expect(await page.locator('tr').count()).toBe(3);
expect(await page2.locator('tr').count()).toBe(3); expect(await page2.locator('tr').count()).toBe(3);
await page2.click('[data-testid="create-page"]');
expect(await page.locator('tr').count()).toBe(4);
expect(await page2.locator('tr').count()).toBe(4);
}); });