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,
TableRow,
} from '@affine/component';
import { DEFAULT_SORT_KEY } from "@affine/env/constant";
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowDownBigIcon, ArrowUpBigIcon } from '@blocksuite/icons';
import { useMediaQuery, useTheme } from '@mui/material';
@ -111,7 +112,7 @@ export const PageList = ({
}: PageListProps) => {
const sorter = useSorter<ListData>({
data: list,
key: 'updatedDate',
key: DEFAULT_SORT_KEY,
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 (
<StyledTableContainer>
<Table>
@ -139,6 +148,7 @@ export const PageList = ({
/>
<AllPagesBody
isPublicWorkspace={isPublicWorkspace}
groupKey={groupKey}
data={sorter.data}
/>
</Table>

View File

@ -1,29 +1,35 @@
import { TableBody, TableCell } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMediaQuery, useTheme } from '@mui/material';
import { Fragment } from 'react';
import { FavoriteTag } from './components/favorite-tag';
import { TitleCell } from './components/title-cell';
import { OperationCell } from './operation-cell';
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';
export const AllPagesBody = ({
isPublicWorkspace,
data,
groupKey,
}: {
isPublicWorkspace: boolean;
data: ListData[];
groupKey?: DateKey;
}) => {
const t = useAFFiNEI18N();
const theme = useTheme();
const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm'));
const dataWithGroup = useDateGroup({ data, key: groupKey });
return (
<TableBody>
{data.map(
{dataWithGroup.map(
(
{
groupName,
pageId,
title,
icon,
@ -40,60 +46,74 @@ export const AllPagesBody = ({
index
) => {
return (
<StyledTableRow
data-testid={`page-list-item-${pageId}`}
key={`${pageId}-${index}`}
>
<TitleCell
icon={icon}
text={title || t['Untitled']()}
data-testid="title"
onClick={onClickPage}
/>
<TableCell
data-testid="created-date"
ellipsis={true}
hidden={isSmallDevices}
onClick={onClickPage}
>
{formatDate(createDate)}
</TableCell>
<TableCell
data-testid="updated-date"
ellipsis={true}
hidden={isSmallDevices}
onClick={onClickPage}
>
{formatDate(updatedDate ?? createDate)}
</TableCell>
{!isPublicWorkspace && (
<Fragment key={pageId}>
{groupName &&
(index === 0 ||
dataWithGroup[index - 1].groupName !== groupName) && (
<StyledTableRow>
<TableCell
style={{
color: 'var(--affine-text-secondary-color)',
background: 'initial',
cursor: 'default',
}}
>
{groupName}
</TableCell>
</StyledTableRow>
)}
<StyledTableRow data-testid={`page-list-item-${pageId}`}>
<TitleCell
icon={icon}
text={title || t['Untitled']()}
data-testid="title"
onClick={onClickPage}
/>
<TableCell
style={{
padding: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '10px',
}}
data-testid={`more-actions-${pageId}`}
data-testid="created-date"
ellipsis={true}
hidden={isSmallDevices}
onClick={onClickPage}
>
<FavoriteTag
className={favorite ? '' : 'favorite-button'}
onClick={bookmarkPage}
active={!!favorite}
/>
<OperationCell
title={title}
favorite={favorite}
isPublic={isPublicPage}
onOpenPageInNewTab={onOpenPageInNewTab}
onToggleFavoritePage={bookmarkPage}
onRemoveToTrash={removeToTrash}
onDisablePublicSharing={onDisablePublicSharing}
/>
{formatDate(createDate)}
</TableCell>
)}
</StyledTableRow>
<TableCell
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}
createNewEdgeless={createNewEdgeless}
/>
<AllPagesBody isPublicWorkspace={isPublicWorkspace} data={list} />
<AllPagesBody
isPublicWorkspace={isPublicWorkspace}
data={list}
// update groupKey after support sort by create date
groupKey="updatedDate"
/>
</Table>
</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 = {
pageId: string;
icon: JSX.Element;
@ -13,6 +22,8 @@ export type ListData = {
onDisablePublicSharing: () => void;
};
export type DateKey = KeysMatching<ListData, Date>;
export type TrashListData = {
pageId: string;
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();
return (
date.getDate() == today.getDate() &&
date.getMonth() == today.getMonth() &&
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 => {
// yyyy-mm-dd MM-DD HH:mm

View File

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

View File

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

View File

@ -279,6 +279,12 @@
"com.affine.updater.update-available": "Update available",
"com.affine.updater.open-download-page": "Open download page",
"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",
"FILE_ALREADY_EXISTS": "File already exists",
"others": "Others",

View File

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