refactor: clean all pages component (#2176)

Co-authored-by: himself65 <himself65@outlook.com>
This commit is contained in:
Whitewater
2023-05-04 20:59:16 -07:00
committed by GitHub
parent 2c49c774af
commit 84b36c1d35
32 changed files with 772 additions and 504 deletions
@@ -0,0 +1,356 @@
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 { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons';
import { useMediaQuery, useTheme } from '@mui/material';
import { forwardRef } from 'react';
import {
StyledTableContainer,
StyledTableRow,
StyledTitleLink,
StyledTitleWrapper,
} from './styles';
export type FavoriteTagProps = {
active: boolean;
};
// eslint-disable-next-line react/display-name
const FavoriteTag = forwardRef<
HTMLButtonElement,
FavoriteTagProps & 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[];
listType: 'all' | 'favorite' | 'shared' | 'public';
onClickPage: (pageId: string, newTab?: boolean) => 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>
);
};
export type ListData = {
pageId: string;
icon: JSX.Element;
title: string;
favorite: boolean;
createDate: string;
updatedDate?: string;
trashDate?: string;
isPublicPage: boolean;
onClickPage: () => void;
onOpenPageInNewTab: () => void;
bookmarkPage: () => void;
removeToTrash: () => void;
onDisablePublicSharing: () => void;
};
export const PageList: React.FC<PageListProps> = ({
isPublicWorkspace = false,
list,
listType,
}) => {
const t = useAFFiNEI18N();
const isShared = listType === 'shared';
const theme = useTheme();
const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm'));
if (isSmallDevices) {
return <PageListMobileView list={list} />;
}
const ListHead = () => {
const t = useAFFiNEI18N();
return (
<TableHead>
<TableRow>
<TableCell proportion={0.5}>{t['Title']()}</TableCell>
<TableCell proportion={0.2}>{t['Created']()}</TableCell>
<TableCell proportion={0.2}>
{isShared
? // TODO add i18n
'Shared'
: t['Updated']()}
</TableCell>
<TableCell proportion={0.1}></TableCell>
</TableRow>
</TableHead>
);
};
const ListItems = list.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']()}
suffix={
<FavoriteTag
className={favorite ? '' : 'favorite-button'}
onClick={bookmarkPage}
active={!!favorite}
/>
}
onClick={onClickPage}
/>
<TableCell ellipsis={true} onClick={onClickPage}>
{createDate}
</TableCell>
<TableCell ellipsis={true} onClick={onClickPage}>
{updatedDate ?? createDate}
</TableCell>
{!isPublicWorkspace && (
<TableCell
style={{ padding: 0 }}
data-testid={`more-actions-${pageId}`}
>
<OperationCell
title={title}
favorite={favorite}
isPublic={isPublicPage}
onOpenPageInNewTab={onOpenPageInNewTab}
onToggleFavoritePage={bookmarkPage}
onRemoveToTrash={removeToTrash}
onDisablePublicSharing={onDisablePublicSharing}
/>
</TableCell>
)}
</StyledTableRow>
);
}
);
return (
<StyledTableContainer>
<Table>
<ListHead />
<TableBody>{ListItems}</TableBody>
</Table>
</StyledTableContainer>
);
};
const TrashListHead = () => {
const t = useAFFiNEI18N();
return (
<TableHead>
<TableRow>
<TableCell proportion={0.5}>{t['Title']()}</TableCell>
<TableCell proportion={0.2}>{t['Created']()}</TableCell>
<TableCell proportion={0.2}>{t['Moved to Trash']()}</TableCell>
<TableCell proportion={0.1}></TableCell>
</TableRow>
</TableHead>
);
};
export type TrashListData = {
pageId: string;
icon: JSX.Element;
title: string;
favorite: boolean;
createDate: string;
updatedDate?: string;
trashDate?: string;
// isPublic: boolean;
onClickPage: () => void;
onRestorePage: () => void;
onPermanentlyDeletePage: () => void;
};
export const PageListTrashView: React.FC<{
list: TrashListData[];
}> = ({ list }) => {
const t = useAFFiNEI18N();
const theme = useTheme();
const isSmallDevices = useMediaQuery(theme.breakpoints.down('sm'));
if (isSmallDevices) {
const mobileList = list.map(({ pageId, icon, title, onClickPage }) => ({
title,
icon,
pageId,
onClickPage,
}));
return <PageListMobileView list={mobileList} />;
}
const ListItems = list.map(
(
{
pageId,
title,
icon,
createDate,
trashDate,
onClickPage,
onPermanentlyDeletePage,
onRestorePage,
},
index
) => {
return (
<StyledTableRow
data-testid={`page-list-item-${pageId}`}
key={`${pageId}-${index}`}
>
<TitleCell
icon={icon}
text={title || t['Untitled']()}
onClick={onClickPage}
/>
<TableCell ellipsis={true} onClick={onClickPage}>
{createDate}
</TableCell>
<TableCell ellipsis={true} onClick={onClickPage}>
{trashDate}
</TableCell>
<TableCell
style={{ padding: 0 }}
data-testid={`more-actions-${pageId}`}
>
<TrashOperationCell
onPermanentlyDeletePage={onPermanentlyDeletePage}
onRestorePage={onRestorePage}
onOpenPage={onClickPage}
/>
</TableCell>
</StyledTableRow>
);
}
);
return (
<StyledTableContainer>
<Table>
<TrashListHead />
<TableBody>{ListItems}</TableBody>
</Table>
</StyledTableContainer>
);
};
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;
@@ -0,0 +1,4 @@
export * from './all-page';
export * from './operation-cell';
export * from './operation-menu-items';
export * from './styles';
@@ -0,0 +1,172 @@
import {
Confirm,
FlexWrapper,
IconButton,
Menu,
MenuItem,
Tooltip,
} from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeletePermanentlyIcon,
FavoritedIcon,
FavoriteIcon,
MoreVerticalIcon,
OpenInNewIcon,
ResetIcon,
} from '@blocksuite/icons';
import type React from 'react';
import { useState } from 'react';
import { DisablePublicSharing, MoveToTrash } from './operation-menu-items';
export type OperationCellProps = {
title: string;
favorite: boolean;
isPublic: boolean;
onOpenPageInNewTab: () => void;
onToggleFavoritePage: () => void;
onRemoveToTrash: () => void;
onDisablePublicSharing: () => void;
};
export const OperationCell: React.FC<OperationCellProps> = ({
title,
favorite,
isPublic,
onOpenPageInNewTab,
onToggleFavoritePage,
onRemoveToTrash,
onDisablePublicSharing,
}) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
const [openDisableShared, setOpenDisableShared] = useState(false);
const OperationMenu = (
<>
{isPublic && (
<DisablePublicSharing
testId="disable-public-sharing"
onItemClick={() => {
setOpenDisableShared(true);
}}
/>
)}
<MenuItem
onClick={onToggleFavoritePage}
icon={
favorite ? (
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
) : (
<FavoriteIcon />
)
}
>
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
</MenuItem>
{!environment.isDesktop && (
<MenuItem onClick={onOpenPageInNewTab} icon={<OpenInNewIcon />}>
{t['Open in new tab']()}
</MenuItem>
)}
<MoveToTrash
testId="move-to-trash"
onItemClick={() => {
setOpen(true);
}}
/>
</>
);
return (
<>
<FlexWrapper alignItems="center" justifyContent="center">
<Menu
content={OperationMenu}
placement="bottom"
disablePortal={true}
trigger="click"
>
<IconButton data-testid="page-list-operation-button">
<MoreVerticalIcon />
</IconButton>
</Menu>
</FlexWrapper>
<MoveToTrash.ConfirmModal
open={open}
title={title}
onConfirm={() => {
onRemoveToTrash();
setOpen(false);
}}
onClose={() => {
setOpen(false);
}}
onCancel={() => {
setOpen(false);
}}
/>
<DisablePublicSharing.DisablePublicSharingModal
onConfirmDisable={onDisablePublicSharing}
open={openDisableShared}
onClose={() => {
setOpenDisableShared(false);
}}
/>
</>
);
};
export type TrashOperationCellProps = {
onPermanentlyDeletePage: () => void;
onRestorePage: () => void;
onOpenPage: () => void;
};
export const TrashOperationCell: React.FC<TrashOperationCellProps> = ({
onPermanentlyDeletePage,
onRestorePage,
}) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
return (
<FlexWrapper>
<Tooltip content={t['Restore it']()} placement="top-start">
<IconButton
style={{ marginRight: '12px' }}
onClick={() => {
onRestorePage();
}}
>
<ResetIcon />
</IconButton>
</Tooltip>
<Tooltip content={t['Delete permanently']()} placement="top-start">
<IconButton
onClick={() => {
setOpen(true);
}}
>
<DeletePermanentlyIcon />
</IconButton>
</Tooltip>
<Confirm
title={t['Delete permanently?']()}
content={t['TrashButtonGroupDescription']()}
confirmText={t['Delete']()}
confirmType="danger"
open={open}
onConfirm={() => {
onPermanentlyDeletePage();
setOpen(false);
}}
onClose={() => {
setOpen(false);
}}
onCancel={() => {
setOpen(false);
}}
/>
</FlexWrapper>
);
};
@@ -0,0 +1,29 @@
import { MenuItem, toast } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CopyIcon } from '@blocksuite/icons';
import { useCallback } from 'react';
import type { CommonMenuItemProps } from './types';
export const CopyLink = ({ onItemClick, onSelect }: CommonMenuItemProps) => {
const t = useAFFiNEI18N();
const copyUrl = useCallback(() => {
navigator.clipboard.writeText(window.location.href);
toast(t['Copied link to clipboard']());
}, [t]);
return (
<MenuItem
data-testid="copy-link"
onClick={() => {
copyUrl();
onItemClick?.();
onSelect?.();
}}
icon={<CopyIcon />}
>
{t['Copy Link']()}
</MenuItem>
);
};
@@ -0,0 +1,49 @@
import { MenuItem, styled } from '@affine/component';
import { PublicLinkDisableModal } from '@affine/component/share-menu';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ShareIcon } from '@blocksuite/icons';
import type { CommonMenuItemProps } from './types';
const StyledMenuItem = styled(MenuItem)(({ theme }) => {
return {
div: {
color: theme.palette.error.main,
svg: {
color: theme.palette.error.main,
},
},
':hover': {
div: {
color: theme.palette.error.main,
svg: {
color: theme.palette.error.main,
},
},
},
};
});
export const DisablePublicSharing = ({
onSelect,
onItemClick,
testId,
}: CommonMenuItemProps) => {
const t = useAFFiNEI18N();
return (
<>
<StyledMenuItem
data-testid={testId}
onClick={() => {
onItemClick?.();
onSelect?.();
}}
style={{ color: 'red' }}
icon={<ShareIcon />}
>
{t['Disable Public Sharing']()}
</StyledMenuItem>
</>
);
};
DisablePublicSharing.DisablePublicSharingModal = PublicLinkDisableModal;
@@ -0,0 +1,73 @@
import { Menu, MenuItem } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ContentParser } from '@blocksuite/blocks/content-parser';
import {
ArrowRightSmallIcon,
ExportIcon,
ExportToHtmlIcon,
ExportToMarkdownIcon,
} from '@blocksuite/icons';
import { useRef } from 'react';
import type { CommonMenuItemProps } from './types';
export const Export = ({
onSelect,
onItemClick,
}: CommonMenuItemProps<{ type: 'markdown' | 'html' }>) => {
const t = useAFFiNEI18N();
const contentParserRef = useRef<ContentParser>();
return (
<Menu
width={248}
placement="left"
trigger="click"
content={
<>
<MenuItem
data-testid="export-to-html"
onClick={() => {
if (!contentParserRef.current) {
contentParserRef.current = new ContentParser(
globalThis.currentEditor!.page
);
}
contentParserRef.current.onExportHtml();
onSelect?.({ type: 'html' });
}}
icon={<ExportToHtmlIcon />}
>
{t['Export to HTML']()}
</MenuItem>
<MenuItem
data-testid="export-to-markdown"
onClick={() => {
if (!contentParserRef.current) {
contentParserRef.current = new ContentParser(
globalThis.currentEditor!.page
);
}
contentParserRef.current.onExportMarkdown();
onSelect?.({ type: 'markdown' });
}}
icon={<ExportToMarkdownIcon />}
>
{t['Export to Markdown']()}
</MenuItem>
</>
}
>
<MenuItem
data-testid="export-menu"
icon={<ExportIcon />}
endIcon={<ArrowRightSmallIcon />}
onClick={e => {
e.stopPropagation();
onItemClick?.();
}}
>
{t.Export()}
</MenuItem>
</Menu>
);
};
@@ -0,0 +1,52 @@
import type { ConfirmProps } from '@affine/component';
import { Confirm, MenuItem } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DeleteTemporarilyIcon } from '@blocksuite/icons';
import type { CommonMenuItemProps } from './types';
export const MoveToTrash = ({
onSelect,
onItemClick,
testId,
}: CommonMenuItemProps) => {
const t = useAFFiNEI18N();
return (
<>
<MenuItem
data-testid={testId}
onClick={() => {
onItemClick?.();
onSelect?.();
}}
icon={<DeleteTemporarilyIcon />}
>
{t['Move to Trash']()}
</MenuItem>
</>
);
};
const ConfirmModal = ({
title,
...confirmModalProps
}: {
title: string;
} & ConfirmProps) => {
const t = useAFFiNEI18N();
return (
<Confirm
title={t['Delete page?']()}
content={t['will be moved to Trash']({
title: title || 'Untitled',
})}
confirmText={t.Delete()}
confirmType="danger"
{...confirmModalProps}
/>
);
};
MoveToTrash.ConfirmModal = ConfirmModal;
@@ -0,0 +1,5 @@
export * from './CopyLink';
export * from './DisablePublicSharing';
export * from './Export';
// export * from './MoveTo';
export * from './MoveToTrash';
@@ -0,0 +1,7 @@
export type CommonMenuItemProps<SelectParams = undefined> = {
// onItemClick is triggered when the item is clicked, sometimes after item click, it still has some internal logic to run(like popover a new menu), so we need to have a separate callback for that
onItemClick?: () => void;
// onSelect is triggered when the item is selected, it's the final callback for the item click
onSelect?: (params?: SelectParams) => void;
testId?: string;
};
@@ -0,0 +1,55 @@
import { displayFlex, styled } from '@affine/component';
import { TableRow } from '@affine/component';
export const StyledTableContainer = styled('div')(({ theme }) => {
return {
height: 'calc(100vh - 52px)',
padding: '78px 72px',
maxWidth: '100%',
overflowY: 'auto',
[theme.breakpoints.down('md')]: {
padding: '12px 24px',
},
};
});
export const StyledTitleWrapper = styled('div')(() => {
return {
...displayFlex('flex-start', 'center'),
a: {
color: 'inherit',
},
'a:visited': {
color: 'unset',
},
'a:hover': {
color: 'var(--affine-primary-color)',
},
};
});
export const StyledTitleLink = styled('div')(() => {
return {
maxWidth: '80%',
marginRight: '18px',
...displayFlex('flex-start', 'center'),
color: 'var(--affine-text-primary-color)',
'>svg': {
fontSize: '24px',
marginRight: '12px',
color: 'var(--affine-icon-color)',
},
};
});
export const StyledTableRow = styled(TableRow)(() => {
return {
cursor: 'pointer',
'.favorite-button': {
visibility: 'hidden',
},
'&:hover': {
'.favorite-button': {
visibility: 'visible',
},
},
};
});
@@ -56,6 +56,12 @@ export const AffineSharePage: FC<ShareMenuProps> = props => {
navigator.clipboard.writeText(sharingUrl);
toast(t['Copied link to clipboard']());
}, [sharingUrl, t]);
const onDisablePublic = useCallback(() => {
setIsPublic(false);
toast('Successfully disabled', {
portal: document.body,
});
}, [setIsPublic]);
return (
<div className={menuItemStyle}>
@@ -104,8 +110,8 @@ export const AffineSharePage: FC<ShareMenuProps> = props => {
{t['Disable Public Link']()}
</StyledDisableButton>
<PublicLinkDisableModal
page={props.currentPage}
open={showDisable}
onConfirmDisable={onDisablePublic}
onClose={() => {
setShowDisable(false);
}}
@@ -1,9 +1,6 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Page } from '@blocksuite/store';
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-block-suite-workspace-page-is-public';
import { useCallback } from 'react';
import { Modal, ModalCloseButton, toast } from '../../..';
import { Modal, ModalCloseButton } from '../../..';
import {
StyledButton,
StyledButtonContent,
@@ -14,25 +11,17 @@ import {
} from './style';
export type PublicLinkDisableProps = {
page: Page;
open: boolean;
onConfirmDisable: () => void;
onClose: () => void;
};
export const PublicLinkDisableModal = ({
page,
open,
onConfirmDisable,
onClose,
}: PublicLinkDisableProps) => {
const t = useAFFiNEI18N();
const [, setIsPublic] = useBlockSuiteWorkspacePageIsPublic(page);
const handleDisable = useCallback(() => {
setIsPublic(false);
toast('Successfully disabled', {
portal: document.body,
});
onClose();
}, [onClose, setIsPublic]);
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
@@ -47,7 +36,10 @@ export const PublicLinkDisableModal = ({
<StyledButton onClick={onClose}>{t['Cancel']()}</StyledButton>
<StyledDangerButton
data-testid="disable-public-link-confirm-button"
onClick={handleDisable}
onClick={() => {
onConfirmDisable();
onClose();
}}
style={{ marginLeft: '24px' }}
>
{t['Disable']()}