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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 381 additions and 287 deletions

View File

@ -10,7 +10,7 @@ import type { BlockSuiteWorkspace } from '../../../shared';
dayjs.extend(localizedFormat);
export const formatDate = (date?: number | unknown) => {
const dateStr =
typeof date === 'number' ? dayjs(date).format('YYYY-MM-DD HH:mm') : '--';
typeof date === 'number' ? dayjs(date).format('MM-DD HH:mm') : '--';
return dateStr;
};

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': {

View File

@ -10,7 +10,7 @@ import type {
} from '../components/page-list/all-page';
import { PageListTrashView } from '../components/page-list/all-page';
import PageList from '../components/page-list/all-page';
import { NewPageButton } from '../components/page-list/new-page-buttton';
import { NewPageButton } from '../components/page-list/components/new-page-buttton';
import type { OperationCellProps } from '../components/page-list/operation-cell';
import { OperationCell } from '../components/page-list/operation-cell';
import { toast } from '../ui/toast';
@ -99,6 +99,14 @@ AffineAllPageList.args = {
],
};
export const AffinePublicPageList: StoryFn<PageListProps> = ({ ...props }) => (
<PageList {...props} />
);
AffinePublicPageList.args = {
...AffineAllPageList.args,
isPublicWorkspace: true,
};
export const AffineAllPageMobileList: StoryFn<PageListProps> = ({
...props
}) => <PageList {...props} />;

View File

@ -16,10 +16,12 @@ export const Menu = (props: MenuProps) => {
content,
placement = 'bottom-start',
children,
...popperProps
} = props;
return content ? (
<Popper
{...props}
placement={placement}
{...popperProps}
showArrow={false}
content={
<StyledMenuWrapper

View File

@ -1,18 +1,16 @@
import { styled, textEllipsis } from '../../styles';
import type { TableCellProps } from './interface';
export const StyledTable = styled('table')<{ tableLayout: 'auto' | 'fixed' }>(
({ tableLayout }) => {
return {
fontSize: 'var(--affine-font-base)',
color: 'var(--affine-text-primary-color)',
tableLayout,
width: '100%',
borderCollapse: 'separate',
borderSpacing: '0',
};
}
);
export const StyledTable = styled('table')(() => {
return {
fontSize: 'var(--affine-font-base)',
color: 'var(--affine-text-primary-color)',
tableLayout: 'fixed',
width: '100%',
borderCollapse: 'separate',
borderSpacing: '0',
};
});
export const StyledTableBody = styled('tbody')(() => {
return {
@ -37,7 +35,7 @@ export const StyledTableCell = styled('td')<
return {
width,
height: '52px',
padding: '0 30px',
paddingLeft: '16px',
boxSizing: 'border-box',
textAlign: align,
verticalAlign: 'middle',

View File

@ -1,11 +1,4 @@
import type { HTMLAttributes, PropsWithChildren } from 'react';
import { StyledTableBody } from './styles';
export const TableBody = ({
children,
...props
}: PropsWithChildren<HTMLAttributes<HTMLTableSectionElement>>) => {
return <StyledTableBody {...props}>{children}</StyledTableBody>;
};
export const TableBody = StyledTableBody;
export default TableBody;

View File

@ -1,7 +1,4 @@
import type { TableCellProps } from './interface';
import { StyledTableCell } from './styles';
export const TableCell = ({ children, ...props }: TableCellProps) => {
return <StyledTableCell {...props}>{children}</StyledTableCell>;
};
export const TableCell = StyledTableCell;
export default TableCell;

View File

@ -1,11 +1,4 @@
import type { HTMLAttributes, PropsWithChildren } from 'react';
import { StyledTableRow } from './styles';
export const TableRow = ({
children,
...props
}: PropsWithChildren<HTMLAttributes<HTMLTableRowElement>>) => {
return <StyledTableRow {...props}>{children}</StyledTableRow>;
};
export const TableRow = StyledTableRow;
export default TableRow;

View File

@ -1,31 +1,5 @@
import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
import { Children } from 'react';
import { StyledTable } from './styles';
const childrenHasEllipsis = (children: ReactNode | ReactNode[]): boolean => {
return Children.toArray(children).some(child => {
if (typeof child === 'object' && 'props' in child) {
if (!child.props.ellipsis && child.props.children) {
return childrenHasEllipsis(child.props.children);
}
return child.props.ellipsis ?? false;
}
return false;
});
};
export const Table = ({
children,
...props
}: PropsWithChildren<HTMLAttributes<HTMLTableElement>>) => {
const tableLayout = childrenHasEllipsis(children) ? 'fixed' : 'auto';
return (
<StyledTable tableLayout={tableLayout} {...props}>
{children}
</StyledTable>
);
};
export const Table = StyledTable;
export default Table;