feat: add responvise page view (#2453)

This commit is contained in:
Whitewater
2023-05-21 16:25:25 -07:00
committed by GitHub
parent 1f510799e2
commit d68b421a4b
15 changed files with 381 additions and 287 deletions

View File

@@ -1,72 +1,23 @@
import type { IconButtonProps, TableCellProps } from '@affine/component';
import {
Content,
IconButton,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Tooltip,
} from '@affine/component';
import { OperationCell, TrashOperationCell } from '@affine/component/page-list';
import { TrashOperationCell } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
ArrowDownBigIcon,
ArrowUpBigIcon,
FavoritedIcon,
FavoriteIcon,
} from '@blocksuite/icons';
import { ArrowDownBigIcon, ArrowUpBigIcon } from '@blocksuite/icons';
import { useMediaQuery, useTheme } from '@mui/material';
import type { CSSProperties } from 'react';
import { forwardRef } from 'react';
import { NewPageButton } from './new-page-buttton';
import {
StyledTableContainer,
StyledTableRow,
StyledTitleLink,
StyledTitleWrapper,
} from './styles';
import { AllPagesBody } from './all-pages-body';
import { NewPageButton } from './components/new-page-buttton';
import { TitleCell } from './components/title-cell';
import { AllPageListMobileView, TrashListMobileView } from './mobile';
import { StyledTableContainer, StyledTableRow } from './styles';
import { useSorter } from './use-sorter';
// eslint-disable-next-line react/display-name
const FavoriteTag = forwardRef<
HTMLButtonElement,
{
active: boolean;
} & Omit<IconButtonProps, 'children'>
>(({ active, onClick, ...props }, ref) => {
const t = useAFFiNEI18N();
return (
<Tooltip
content={active ? t['Favorited']() : t['Favorite']()}
placement="top-start"
>
<IconButton
ref={ref}
iconSize={[20, 20]}
style={{
color: active
? 'var(--affine-primary-color)'
: 'var(--affine-icon-color)',
}}
onClick={e => {
e.stopPropagation();
onClick?.(e);
}}
{...props}
>
{active ? (
<FavoritedIcon data-testid="favorited-icon" />
) : (
<FavoriteIcon />
)}
</IconButton>
</Tooltip>
);
});
export type PageListProps = {
isPublicWorkspace?: boolean;
list: ListData[];
@@ -74,36 +25,13 @@ export type PageListProps = {
onCreateNewEdgeless: () => void;
};
const TitleCell = ({
icon,
text,
suffix,
...props
}: {
icon: JSX.Element;
text: string;
suffix?: JSX.Element;
} & TableCellProps) => {
return (
<TableCell {...props}>
<StyledTitleWrapper>
<StyledTitleLink>
{icon}
<Content ellipsis={true} color="inherit">
{text}
</Content>
</StyledTitleLink>
{suffix}
</StyledTitleWrapper>
</TableCell>
);
};
const AllPagesHead = ({
isPublicWorkspace,
sorter,
createNewPage,
createNewEdgeless,
}: {
isPublicWorkspace: boolean;
sorter: ReturnType<typeof useSorter<ListData>>;
createNewPage: () => void;
createNewEdgeless: () => void;
@@ -125,6 +53,7 @@ const AllPagesHead = ({
content: t['Updated'](),
proportion: 0.2,
},
{
key: 'unsortable_action',
content: (
@@ -133,11 +62,10 @@ const AllPagesHead = ({
createNewEdgeless={createNewEdgeless}
/>
),
showWhen: () => !isPublicWorkspace,
sortable: false,
styles: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
justifyContent: 'flex-end',
} satisfies CSSProperties,
},
];
@@ -145,8 +73,9 @@ const AllPagesHead = ({
return (
<TableHead>
<TableRow>
{titleList.map(
({ key, content, proportion, sortable = true, styles }) => (
{titleList
.filter(({ showWhen = () => true }) => showWhen())
.map(({ key, content, proportion, sortable = true, styles }) => (
<TableCell
key={key}
proportion={proportion}
@@ -156,18 +85,24 @@ const AllPagesHead = ({
? () => sorter.shiftOrder(key as keyof ListData)
: undefined
}
style={styles}
>
{content}
{sorter.key === key &&
(sorter.order === 'asc' ? (
<ArrowUpBigIcon width={24} height={24} />
) : (
<ArrowDownBigIcon width={24} height={24} />
))}
<div
style={{
display: 'flex',
alignItems: 'center',
...styles,
}}
>
{content}
{sorter.key === key &&
(sorter.order === 'asc' ? (
<ArrowUpBigIcon width={24} height={24} />
) : (
<ArrowDownBigIcon width={24} height={24} />
))}
</div>
</TableCell>
)
)}
))}
</TableRow>
</TableHead>
);
@@ -189,13 +124,12 @@ export type ListData = {
onDisablePublicSharing: () => void;
};
export const PageList: React.FC<PageListProps> = ({
export const PageList = ({
isPublicWorkspace = false,
list,
onCreateNewPage,
onCreateNewEdgeless,
}) => {
const t = useAFFiNEI18N();
}: PageListProps) => {
const sorter = useSorter<ListData>({
data: list,
key: 'createDate',
@@ -205,93 +139,29 @@ export const PageList: React.FC<PageListProps> = ({
const theme = useTheme();
const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm'));
if (isSmallDevices) {
return <PageListMobileView list={sorter.data} />;
return (
<AllPageListMobileView
isPublicWorkspace={isPublicWorkspace}
createNewPage={onCreateNewPage}
createNewEdgeless={onCreateNewEdgeless}
list={sorter.data}
/>
);
}
const ListItems = sorter.data.map(
(
{
pageId,
title,
icon,
isPublicPage,
favorite,
createDate,
updatedDate,
onClickPage,
bookmarkPage,
onOpenPageInNewTab,
removeToTrash,
onDisablePublicSharing,
},
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}
onClick={onClickPage}
>
{createDate}
</TableCell>
<TableCell
data-testid="updated-date"
ellipsis={true}
onClick={onClickPage}
>
{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>
);
}
);
return (
<StyledTableContainer>
<Table>
<AllPagesHead
isPublicWorkspace={isPublicWorkspace}
sorter={sorter}
createNewPage={onCreateNewPage}
createNewEdgeless={onCreateNewEdgeless}
/>
<TableBody>{ListItems}</TableBody>
<AllPagesBody
isPublicWorkspace={isPublicWorkspace}
data={sorter.data}
/>
</Table>
</StyledTableContainer>
);
@@ -338,7 +208,7 @@ export const PageListTrashView: React.FC<{
pageId,
onClickPage,
}));
return <PageListMobileView list={mobileList} />;
return <TrashListMobileView list={mobileList} />;
}
const ListItems = list.map(
(
@@ -395,43 +265,4 @@ export const PageListTrashView: React.FC<{
);
};
const PageListMobileView: React.FC<{
list: {
pageId: string;
title: string;
icon: JSX.Element;
onClickPage: () => void;
}[];
}> = ({ list }) => {
const t = useAFFiNEI18N();
const ListItems = list.map(({ pageId, title, icon, onClickPage }, index) => {
return (
<StyledTableRow
data-testid={`page-list-item-${pageId}`}
key={`${pageId}-${index}`}
>
<TableCell onClick={onClickPage}>
<StyledTitleWrapper>
<StyledTitleLink>
{icon}
<Content ellipsis={true} color="inherit">
{title || t['Untitled']()}
</Content>
</StyledTitleLink>
</StyledTitleWrapper>
</TableCell>
</StyledTableRow>
);
});
return (
<StyledTableContainer>
<Table>
<TableBody>{ListItems}</TableBody>
</Table>
</StyledTableContainer>
);
};
export default PageList;

View File

@@ -0,0 +1,101 @@
import { TableBody, TableCell } from '@affine/component';
import { OperationCell } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMediaQuery, useTheme } from '@mui/material';
import type { ListData } from './all-page';
import { FavoriteTag } from './components/favorite-tag';
import { TitleCell } from './components/title-cell';
import { StyledTableRow } from './styles';
export const AllPagesBody = ({
isPublicWorkspace,
data,
}: {
isPublicWorkspace: boolean;
data: ListData[];
}) => {
const t = useAFFiNEI18N();
const theme = useTheme();
const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm'));
return (
<TableBody>
{data.map(
(
{
pageId,
title,
icon,
isPublicPage,
favorite,
createDate,
updatedDate,
onClickPage,
bookmarkPage,
onOpenPageInNewTab,
removeToTrash,
onDisablePublicSharing,
},
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}
>
{createDate}
</TableCell>
<TableCell
data-testid="updated-date"
ellipsis={true}
hidden={isSmallDevices}
onClick={onClickPage}
>
{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>
);
}
)}
</TableBody>
);
};

View File

@@ -0,0 +1,42 @@
import type { IconButtonProps } from '@affine/component';
import { IconButton, Tooltip } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons';
import { forwardRef } from 'react';
export const FavoriteTag = forwardRef<
HTMLButtonElement,
{
active: boolean;
} & Omit<IconButtonProps, 'children'>
>(({ active, onClick, ...props }, ref) => {
const t = useAFFiNEI18N();
return (
<Tooltip
content={active ? t['Favorited']() : t['Favorite']()}
placement="top-start"
>
<IconButton
ref={ref}
iconSize={[20, 20]}
style={{
color: active
? 'var(--affine-primary-color)'
: 'var(--affine-icon-color)',
}}
onClick={e => {
e.stopPropagation();
onClick?.(e);
}}
{...props}
>
{active ? (
<FavoritedIcon data-testid="favorited-icon" />
) : (
<FavoriteIcon />
)}
</IconButton>
</Tooltip>
);
});
FavoriteTag.displayName = 'FavoriteTag';

View File

@@ -2,9 +2,9 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { useState } from 'react';
import { DropdownButton } from '../../ui/button/dropdown';
import { Menu } from '../../ui/menu/menu';
import { BlockCard } from '../card/block-card';
import { DropdownButton } from '../../../ui/button/dropdown';
import { Menu } from '../../../ui/menu/menu';
import { BlockCard } from '../../card/block-card';
type NewPageButtonProps = {
createNewPage: () => void;

View File

@@ -0,0 +1,27 @@
import type { TableCellProps } from '@affine/component';
import { Content, TableCell } from '@affine/component';
import { StyledTitleLink } from '../styles';
export const TitleCell = ({
icon,
text,
suffix,
...props
}: {
icon: JSX.Element;
text: string;
suffix?: JSX.Element;
} & TableCellProps) => {
return (
<TableCell {...props}>
<StyledTitleLink>
{icon}
<Content ellipsis={true} color="inherit">
{text}
</Content>
</StyledTitleLink>
{suffix}
</TableCell>
);
};

View File

@@ -0,0 +1,118 @@
import {
Content,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { ListData } from './all-page';
import { AllPagesBody } from './all-pages-body';
import { NewPageButton } from './components/new-page-buttton';
import {
StyledTableContainer,
StyledTableRow,
StyledTitleLink,
} from './styles';
const MobileHead = ({
isPublicWorkspace,
createNewPage,
createNewEdgeless,
}: {
isPublicWorkspace: boolean;
createNewPage: () => void;
createNewEdgeless: () => void;
}) => {
const t = useAFFiNEI18N();
return (
<TableHead>
<TableRow>
<TableCell proportion={0.8}>{t['Title']()}</TableCell>
{!isPublicWorkspace && (
<TableCell>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
}}
>
<NewPageButton
createNewPage={createNewPage}
createNewEdgeless={createNewEdgeless}
/>
</div>
</TableCell>
)}
</TableRow>
</TableHead>
);
};
export const AllPageListMobileView = ({
list,
isPublicWorkspace,
createNewPage,
createNewEdgeless,
}: {
isPublicWorkspace: boolean;
list: ListData[];
createNewPage: () => void;
createNewEdgeless: () => void;
}) => {
return (
<StyledTableContainer>
<Table>
<MobileHead
isPublicWorkspace={isPublicWorkspace}
createNewPage={createNewPage}
createNewEdgeless={createNewEdgeless}
/>
<AllPagesBody isPublicWorkspace={isPublicWorkspace} data={list} />
</Table>
</StyledTableContainer>
);
};
// TODO align to {@link AllPageListMobileView}
export const TrashListMobileView = ({
list,
}: {
list: {
pageId: string;
title: string;
icon: JSX.Element;
onClickPage: () => void;
}[];
}) => {
const t = useAFFiNEI18N();
const ListItems = list.map(({ pageId, title, icon, onClickPage }, index) => {
return (
<StyledTableRow
data-testid={`page-list-item-${pageId}`}
key={`${pageId}-${index}`}
>
<TableCell onClick={onClickPage}>
<StyledTitleLink>
{icon}
<Content ellipsis={true} color="inherit">
{title || t['Untitled']()}
</Content>
</StyledTitleLink>
</TableCell>
</StyledTableRow>
);
});
return (
<StyledTableContainer>
<Table>
<TableBody>{ListItems}</TableBody>
</Table>
</StyledTableContainer>
);
};

View File

@@ -4,14 +4,26 @@ import { TableRow } from '@affine/component';
export const StyledTableContainer = styled('div')(({ theme }) => {
return {
height: 'calc(100vh - 52px)',
padding: '78px 72px',
padding: '52px 32px',
maxWidth: '100%',
overflowY: 'auto',
[theme.breakpoints.down('md')]: {
padding: '12px 24px',
[theme.breakpoints.down('sm')]: {
padding: '52px 0px',
'tr > td:first-of-type': {
borderTopLeftRadius: '0px',
borderBottomLeftRadius: '0px',
},
'tr > td:last-of-type': {
borderTopRightRadius: '0px',
borderBottomRightRadius: '0px',
},
},
};
});
/**
* @deprecated
*/
export const StyledTitleWrapper = styled('div')(() => {
return {
...displayFlex('flex-start', 'center'),
@@ -28,8 +40,6 @@ export const StyledTitleWrapper = styled('div')(() => {
});
export const StyledTitleLink = styled('div')(() => {
return {
maxWidth: '80%',
marginRight: '18px',
...displayFlex('flex-start', 'center'),
color: 'var(--affine-text-primary-color)',
'>svg': {