mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-25 08:02:10 +03:00
feat: group all page by date (#2532)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
parent
60057c666d
commit
7dcbe64d4e
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
)}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
@ -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
|
||||
|
@ -79,7 +79,7 @@ AffineAllPageList.args = {
|
||||
removeToTrash: () => toast('Remove to trash'),
|
||||
},
|
||||
{
|
||||
pageId: '1',
|
||||
pageId: '3',
|
||||
favorite: false,
|
||||
icon: <PageIcon />,
|
||||
isPublicPage: true,
|
||||
|
1
packages/env/src/constant.ts
vendored
1
packages/env/src/constant.ts
vendored
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user