Merge branch 'feat/workspace' into feat/dev

# Conflicts:
#	packages/app/src/components/header/page-header.tsx
#	packages/app/src/components/workspace-slider-bar/index.tsx
This commit is contained in:
QiShaoXuan
2022-12-09 19:28:30 +08:00
26 changed files with 889 additions and 301 deletions
+69 -9
View File
@@ -16,13 +16,16 @@ import {
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { useEditor } from '@/providers/editor-provider'; import { useEditor } from '@/providers/editor-provider';
import ThemeModeSwitch from '@/components/theme-mode-switch'; import ThemeModeSwitch from '@/components/theme-mode-switch';
import { IconButton } from '@/ui/button'; import { IconButton, Button } from '@/ui/button';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { getWarningMessage, shouldShowWarning } from './utils'; import { getWarningMessage, shouldShowWarning } from './utils';
import { Menu, MenuItem } from '@/ui/menu'; import { Menu, MenuItem } from '@/ui/menu';
import { useRouter } from 'next/router';
import { useConfirm } from '@/providers/confirm-provider';
const PopoverContent = () => { const PopoverContent = () => {
const { editor, mode, setMode } = useEditor(); const { editor, mode, setMode } = useEditor();
return ( return (
<> <>
<MenuItem <MenuItem
@@ -81,9 +84,71 @@ const BrowserWarning = ({
); );
}; };
export const Header = ({ children }: PropsWithChildren<{}>) => { const HeaderRight = () => {
const [showWarning, setShowWarning] = useState(shouldShowWarning()); const { pageList, toggleDeletePage, permanentlyDeletePage } = useEditor();
const { confirm } = useConfirm();
const router = useRouter();
const currentPageMeta = pageList.find(p => p.id === router.query.pageId);
const isTrash = !!currentPageMeta?.trash;
if (isTrash) {
const { id } = currentPageMeta;
return (
<>
<Button
bold={true}
shape="round"
style={{ marginRight: '24px' }}
onClick={() => {
toggleDeletePage(id);
}}
>
Restore it
</Button>
<Button
bold={true}
shape="round"
type="danger"
onClick={() => {
confirm({
title: 'Permanently delete',
content:
"Once deleted, you can't undo this action. Do you confirm?",
confirmText: 'Delete',
confirmType: 'danger',
}).then(confirm => {
if (confirm) {
router.push({ pathname: '/page-list/all' });
permanentlyDeletePage(id);
}
});
}}
>
Delete permanently
</Button>
</>
);
}
return (
<>
<ThemeModeSwitch />
<Menu content={<PopoverContent />} placement="bottom-end">
<IconButton>
<MoreVertical_24pxIcon />
</IconButton>
</Menu>
</>
);
};
export const Header = ({ children }: PropsWithChildren<{}>) => {
const { pageList } = useEditor();
const [showWarning, setShowWarning] = useState(shouldShowWarning());
const router = useRouter();
const currentPageMeta = pageList.find(p => p.id === router.query.pageId);
const isTrash = !!currentPageMeta?.trash;
console.log('isTrash', isTrash);
return ( return (
<StyledHeaderContainer hasWarning={showWarning}> <StyledHeaderContainer hasWarning={showWarning}>
<BrowserWarning <BrowserWarning
@@ -95,12 +160,7 @@ export const Header = ({ children }: PropsWithChildren<{}>) => {
<StyledHeader hasWarning={showWarning}> <StyledHeader hasWarning={showWarning}>
{children} {children}
<StyledHeaderRightSide> <StyledHeaderRightSide>
<ThemeModeSwitch /> <HeaderRight />
<Menu content={<PopoverContent />} placement="bottom-end">
<IconButton>
<MoreVertical_24pxIcon />
</IconButton>
</Menu>
</StyledHeaderRightSide> </StyledHeaderRightSide>
</StyledHeader> </StyledHeader>
</StyledHeaderContainer> </StyledHeaderContainer>
@@ -1,14 +1,21 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { StyledTitle, StyledTitleWrapper } from './styles'; import {
StyledSearchArrowWrapper,
StyledSwitchWrapper,
StyledTitle,
StyledTitleWrapper,
} from './styles';
import { IconButton } from '@/ui/button';
import { Content } from '@/ui/layout';
import { useEditor } from '@/providers/editor-provider'; import { useEditor } from '@/providers/editor-provider';
import EditorModeSwitch from '@/components/editor-mode-switch'; import EditorModeSwitch from '@/components/editor-mode-switch';
import { MiddleIconArrowDownSmallIcon } from '@blocksuite/icons'; import { MiddleIconArrowDownSmallIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/global-modal-provider';
import IconButton from '@/ui/button/icon-button';
import Header from './header'; import Header from './header';
export const PageHeader = () => { export const PageHeader = () => {
const [title, setTitle] = useState(''); const [title, setTitle] = useState('Untitled');
const [isHover, setIsHover] = useState(false); const [isHover, setIsHover] = useState(false);
const { editor } = useEditor(); const { editor } = useEditor();
@@ -24,35 +31,35 @@ export const PageHeader = () => {
return ( return (
<Header> <Header>
{title ? ( <StyledTitle
<StyledTitle onMouseEnter={() => {
onMouseEnter={() => { setIsHover(true);
setIsHover(true); }}
}} onMouseLeave={() => {
onMouseLeave={() => { setIsHover(false);
setIsHover(false); }}
}} >
> <StyledTitleWrapper>
<EditorModeSwitch <StyledSwitchWrapper>
isHover={isHover} <EditorModeSwitch
style={{ isHover={isHover}
marginRight: '12px', style={{
}} marginRight: '12px',
/> }}
<StyledTitleWrapper>{title}</StyledTitleWrapper> />
<IconButton </StyledSwitchWrapper>
style={{ <Content ellipsis={true}>{title}</Content>
marginLeft: '6px', <StyledSearchArrowWrapper>
}} <IconButton
>
<MiddleIconArrowDownSmallIcon
onClick={() => { onClick={() => {
triggerQuickSearchModal(true); triggerQuickSearchModal(true);
}} }}
/> >
</IconButton> <MiddleIconArrowDownSmallIcon />
</StyledTitle> </IconButton>
) : null} </StyledSearchArrowWrapper>
</StyledTitleWrapper>
</StyledTitle>
</Header> </Header>
); );
}; };
+24 -3
View File
@@ -40,10 +40,9 @@ export const StyledTitle = styled('div')(({ theme }) => ({
export const StyledTitleWrapper = styled('div')({ export const StyledTitleWrapper = styled('div')({
maxWidth: '720px', maxWidth: '720px',
overflow: 'hidden', height: '100%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
position: 'relative', position: 'relative',
...displayFlex('center', 'center'),
}); });
export const StyledHeaderRightSide = styled('div')({ export const StyledHeaderRightSide = styled('div')({
@@ -89,3 +88,25 @@ export const StyledCloseButton = styled.div(({ theme }) => {
}, },
}; };
}); });
export const StyledSwitchWrapper = styled.div(() => {
return {
position: 'absolute',
right: '100%',
top: 0,
bottom: 0,
margin: 'auto',
...displayFlex('center', 'center'),
};
});
export const StyledSearchArrowWrapper = styled.div(() => {
return {
position: 'absolute',
left: 'calc(100% + 4px)',
top: 0,
bottom: 0,
margin: 'auto',
...displayFlex('center', 'center'),
};
});
@@ -0,0 +1,12 @@
import React from 'react';
export const Empty = () => {
return (
<div style={{ textAlign: 'center' }}>
<p>Tips: Click Add to Favourites/Trash and the page will appear here.</p>
<p>(Designer is grappling with designing)</p>
</div>
);
};
export default Empty;
@@ -0,0 +1,97 @@
import { PageMeta, useEditor } from '@/providers/editor-provider';
import {
MiddleFavouritedStatus2Icon,
MiddleFavouritesIcon,
} from '@blocksuite/icons';
import {
StyledFavoriteButton,
StyledTableContainer,
StyledTableRow,
StyledTitleContent,
StyledTitleWrapper,
} from './styles';
import { Table, TableBody, TableCell, TableHead, TableRow } from '@/ui/table';
import { OperationCell, TrashOperationCell } from './operation-cell';
import Empty from './empty';
import Link from 'next/link';
import React from 'react';
const FavoriteTag = ({ pageMeta }: { pageMeta: PageMeta }) => {
const { toggleFavoritePage } = useEditor();
return (
<StyledFavoriteButton
className="favorite-button"
favorite={pageMeta.favorite}
onClick={() => {
toggleFavoritePage(pageMeta.id);
}}
>
{pageMeta.favorite ? (
<MiddleFavouritedStatus2Icon />
) : (
<MiddleFavouritesIcon />
)}
</StyledFavoriteButton>
);
};
export const PageList = ({
pageList,
showFavoriteTag = false,
isTrash = false,
}: {
pageList: PageMeta[];
showFavoriteTag?: boolean;
isTrash?: boolean;
}) => {
if (pageList.length === 0) {
return <Empty />;
}
return (
<StyledTableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell proportion={0.5}>Documents</TableCell>
<TableCell proportion={0.2}>Created</TableCell>
<TableCell proportion={0.2}>
{isTrash ? 'Uploaded' : 'Moved to Trash'}
</TableCell>
<TableCell proportion={0.1}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{pageList.map((pageMeta, index) => {
return (
<StyledTableRow key={`${pageMeta.id}-${index}`}>
<TableCell>
<StyledTitleWrapper>
<Link
href={{ pathname: '/', query: { pageId: pageMeta.id } }}
>
<StyledTitleContent>
{pageMeta.title || pageMeta.id}
</StyledTitleContent>
</Link>
{showFavoriteTag && <FavoriteTag pageMeta={pageMeta} />}
</StyledTitleWrapper>
</TableCell>
<TableCell ellipsis={true}>{pageMeta.createDate}</TableCell>
<TableCell ellipsis={true}>{pageMeta.createDate}</TableCell>
<TableCell>
{isTrash ? (
<TrashOperationCell pageMeta={pageMeta} />
) : (
<OperationCell pageMeta={pageMeta} />
)}
</TableCell>
</StyledTableRow>
);
})}
</TableBody>
</Table>
</StyledTableContainer>
);
};
export default PageList;
@@ -0,0 +1,96 @@
import { PageMeta, useEditor } from '@/providers/editor-provider';
import { useConfirm } from '@/providers/confirm-provider';
import { Menu, MenuItem } from '@/ui/menu';
import { Wrapper } from '@/ui/layout';
import { IconButton } from '@/ui/button';
import {
MoreVertical_24pxIcon,
RestoreIcon,
TrashDeleteforeverIcon,
} from '@blocksuite/icons';
import React from 'react';
export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
const { id, favorite } = pageMeta;
const { openPage, toggleFavoritePage, toggleDeletePage } = useEditor();
const { confirm } = useConfirm();
const OperationMenu = (
<>
<MenuItem
onClick={() => {
toggleFavoritePage(id);
}}
>
{favorite ? 'Remove' : 'Add'} to favourites
</MenuItem>
<MenuItem
onClick={() => {
openPage(id);
}}
>
Open in new tab
</MenuItem>
<MenuItem
onClick={() => {
confirm({
title: 'Delete',
content:
'Deleted items will be moved to Trash Bin. Do you confirm?',
confirmText: 'Delete',
confirmType: 'danger',
}).then(confirm => {
confirm && toggleDeletePage(id);
});
}}
>
Delete
</MenuItem>
</>
);
return (
<Wrapper alignItems="center" justifyContent="center">
<Menu content={OperationMenu} placement="bottom-end" disablePortal={true}>
<IconButton hoverBackground="#E0E6FF">
<MoreVertical_24pxIcon />
</IconButton>
</Menu>
</Wrapper>
);
};
export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
const { id } = pageMeta;
const { permanentlyDeletePage, toggleDeletePage } = useEditor();
const { confirm } = useConfirm();
return (
<Wrapper>
<IconButton
hoverBackground="#E0E6FF"
style={{ marginRight: '12px' }}
onClick={() => {
toggleDeletePage(id);
}}
>
<RestoreIcon />
</IconButton>
<IconButton
hoverBackground="#E0E6FF"
onClick={() => {
confirm({
title: 'Permanently delete',
content:
"Once deleted, you can't undo this action. Do you confirm?",
confirmText: 'Delete',
confirmType: 'danger',
}).then(confirm => {
confirm && permanentlyDeletePage(id);
});
}}
>
<TrashDeleteforeverIcon />
</IconButton>
</Wrapper>
);
};
@@ -0,0 +1,55 @@
import { displayFlex, styled, textEllipsis } from '@/styles';
import { TableRow } from '@/ui/table';
export const StyledTableContainer = styled.div(() => {
return {
height: 'calc(100vh - 60px)',
padding: '78px 72px',
overflowY: 'auto',
};
});
export const StyledTitleWrapper = styled.div(({ theme }) => {
return {
...displayFlex('flex-start', 'center'),
a: {
color: 'inherit',
},
'a:visited': {
color: 'unset',
},
'a:hover': {
color: theme.colors.primaryColor,
},
};
});
export const StyledTitleContent = styled.div(({ theme }) => {
return {
maxWidth: '90%',
marginRight: '18px',
...textEllipsis(1),
};
});
export const StyledFavoriteButton = styled.button<{ favorite: boolean }>(
({ theme, favorite }) => {
return {
width: '32px',
height: '32px',
justifyContent: 'center',
alignItems: 'center',
display: 'none',
color: favorite ? theme.colors.primaryColor : theme.colors.iconColor,
'&:hover': {
color: theme.colors.primaryColor,
},
};
}
);
export const StyledTableRow = styled(TableRow)(({ theme }) => {
return {
'&:hover': {
'.favorite-button': {
display: 'flex',
},
},
};
});
@@ -8,14 +8,51 @@ import {
StyledSubListItem, StyledSubListItem,
} from './style'; } from './style';
import { Arrow } from './icons'; import { Arrow } from './icons';
import Collapse from '@mui/material/Collapse';
import { MiddleIconArrowDownSmallIcon } from '@blocksuite/icons';
import Link from 'next/link'; import Link from 'next/link';
import { useEditor } from '@/providers/editor-provider'; import { useEditor } from '@/providers/editor-provider';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/global-modal-provider';
import { IconButton } from '@/ui/button';
const FavoriteList = ({ showList }: { showList: boolean }) => {
const { pageList, openPage } = useEditor();
const router = useRouter();
const favoriteList = pageList.filter(p => p.favorite);
return (
<Collapse in={showList}>
{favoriteList.map((pageMeta, index) => {
const active = router.query.pageId === pageMeta.id;
return (
<StyledSubListItem
active={active}
key={`${pageMeta}-${index}`}
onClick={() => {
if (active) {
return;
}
openPage(pageMeta.id);
}}
>
{pageMeta.title || pageMeta.id}
</StyledSubListItem>
);
})}
{favoriteList.length === 0 && (
<StyledSubListItem disable={true}>No item</StyledSubListItem>
)}
</Collapse>
);
};
export const WorkSpaceSliderBar = () => { export const WorkSpaceSliderBar = () => {
const { triggerQuickSearchModal } = useModal(); const { triggerQuickSearchModal } = useModal();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [showSubFavorite, setShowSubFavorite] = useState(false);
const { createPage } = useEditor(); const { createPage } = useEditor();
const router = useRouter(); const router = useRouter();
return ( return (
<> <>
<StyledSliderBar show={show}> <StyledSliderBar show={show}>
@@ -38,18 +75,40 @@ export const WorkSpaceSliderBar = () => {
> >
Back to Doc Back to Doc
</StyledListItem> </StyledListItem>
<Link href={{ pathname: '/all-page', query: { name: 'test' } }}> <Link href={{ pathname: '/page-list/all' }}>
<StyledListItem>All pages</StyledListItem> <StyledListItem active={router.pathname === '/page-list/all'}>
All pages
</StyledListItem>
</Link> </Link>
<StyledListItem>Favourites</StyledListItem> <StyledListItem active={router.pathname === '/page-list/favorite'}>
<StyledSubListItem> <Link
document 1, this is a paper icondocument 1 href={{ pathname: '/page-list/favorite' }}
</StyledSubListItem> style={{ flexGrow: 1, textAlign: 'left', color: 'inherit' }}
<StyledSubListItem>document 2</StyledSubListItem> >
<StyledSubListItem>document 4</StyledSubListItem> Favourites
<StyledListItem>Import</StyledListItem> </Link>
<StyledListItem>Bin</StyledListItem> <IconButton
hoverBackground="#E0E6FF"
onClick={() => {
setShowSubFavorite(!showSubFavorite);
}}
>
<MiddleIconArrowDownSmallIcon
style={{
transform: `rotate(${showSubFavorite ? '180' : '0'}deg)`,
}}
/>
</IconButton>
</StyledListItem>
<FavoriteList showList={showSubFavorite} />
<StyledListItem>Import</StyledListItem>
<Link href={{ pathname: '/page-list/trash' }}>
<StyledListItem active={router.pathname === '/page-list/trash'}>
Trash
</StyledListItem>
</Link>
<StyledNewPageButton <StyledNewPageButton
onClick={() => { onClick={() => {
createPage(); createPage();
@@ -5,7 +5,7 @@ export const StyledSliderBar = styled.div<{ show: boolean }>(
return { return {
width: show ? '320px' : '0', width: show ? '320px' : '0',
height: '100vh', height: '100vh',
background: '#FBFBFC', background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
boxShadow: theme.shadow.modal, boxShadow: theme.shadow.modal,
transition: 'width .15s', transition: 'width .15s',
position: 'relative', position: 'relative',
@@ -42,22 +42,25 @@ export const StyledArrowButton = styled.button<{ isShow: boolean }>(
} }
); );
export const StyledListItem = styled.button(({ theme }) => { export const StyledListItem = styled.button<{ active?: boolean }>(
return { ({ theme, active }) => {
width: '296px', return {
height: '32px', width: '296px',
marginTop: '12px', height: '32px',
fontSize: theme.font.sm, marginTop: '12px',
color: theme.colors.popoverColor, fontSize: theme.font.sm,
padding: '0 12px', color: active ? theme.colors.primaryColor : theme.colors.popoverColor,
borderRadius: '5px', backgroundColor: active ? theme.colors.hoverBackground : 'unset',
...displayFlex('flex-start', 'center'), paddingLeft: '12px',
':hover': { borderRadius: '5px',
color: theme.colors.primaryColor, ...displayFlex('flex-start', 'center'),
backgroundColor: theme.colors.hoverBackground, ':hover': {
}, color: theme.colors.primaryColor,
}; backgroundColor: theme.colors.hoverBackground,
}); },
};
}
);
export const StyledNewPageButton = styled(StyledListItem)(({ theme }) => { export const StyledNewPageButton = styled(StyledListItem)(({ theme }) => {
return { return {
@@ -69,20 +72,32 @@ export const StyledNewPageButton = styled(StyledListItem)(({ theme }) => {
}; };
}); });
export const StyledSubListItem = styled.button(({ theme }) => { export const StyledSubListItem = styled.button<{
disable?: boolean;
active?: boolean;
}>(({ theme, disable, active }) => {
return { return {
width: '296px', width: '296px',
height: '32px', height: '32px',
marginTop: '4px', marginTop: '4px',
fontSize: theme.font.sm, fontSize: theme.font.sm,
color: theme.colors.popoverColor, color: disable
? theme.colors.iconColor
: active
? theme.colors.primaryColor
: theme.colors.popoverColor,
backgroundColor: active ? theme.colors.hoverBackground : 'unset',
cursor: disable ? 'not-allowed' : 'pointer',
paddingLeft: '45px', paddingLeft: '45px',
lineHeight: '32px', lineHeight: '32px',
textAlign: 'start', textAlign: 'start',
...textEllipsis(1), ...textEllipsis(1),
':hover': { ':hover': disable
color: theme.colors.primaryColor, ? {}
backgroundColor: theme.colors.hoverBackground, : {
}, color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
},
}; };
}); });
+19
View File
@@ -3,6 +3,11 @@ import { ThemeModeSwitch } from '@/components/theme-mode-switch';
import { Loading } from '@/components/loading'; import { Loading } from '@/components/loading';
import Modal from '@/ui/modal'; import Modal from '@/ui/modal';
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '@/ui/button';
import {
MiddleFavouritedStatus2Icon,
MiddleFavouritesIcon,
} from '@blocksuite/icons';
export const StyledHeader = styled('div')({ export const StyledHeader = styled('div')({
height: '60px', height: '60px',
width: '100vw', width: '100vw',
@@ -35,6 +40,20 @@ const Affine = () => {
<div>hi</div> <div>hi</div>
</Modal> </Modal>
<Loading /> <Loading />
<Button icon={<MiddleFavouritedStatus2Icon />}>click me!</Button>
<Button icon={<MiddleFavouritedStatus2Icon />} type={'primary'}>
click me!
</Button>
<Button icon={<MiddleFavouritedStatus2Icon />} type={'warning'}>
click me!
</Button>
<Button icon={<MiddleFavouritedStatus2Icon />} type={'danger'}>
click me!
</Button>
<Button icon={<MiddleFavouritedStatus2Icon />}></Button>
<Button icon={<MiddleFavouritedStatus2Icon />} shape="round"></Button>
</> </>
); );
}; };
-169
View File
@@ -1,169 +0,0 @@
import React from 'react';
import { Header } from '@/components/header';
import { displayFlex, styled, textEllipsis } from '@/styles';
import { Table, TableCell, TableHead, TableRow, TableBody } from '../ui/table';
import { useConfirm } from '@/providers/confirm-provider';
import { IconButton } from '@/ui/button';
import { MoreVertical_24pxIcon } from '@blocksuite/icons';
import { Menu, MenuItem } from '@/ui/menu';
import { PageMeta, useEditor } from '@/providers/editor-provider';
import { Wrapper } from '@/ui/layout';
import {
MiddleFavouritesIcon,
MiddleFavouritedStatus2Icon,
} from '@blocksuite/icons';
const StyledTableContainer = styled.div(() => {
return {
height: 'calc(100vh - 60px)',
padding: '78px 72px',
overflowY: 'auto',
};
});
const StyledTitleWrapper = styled.div(({ theme }) => {
return {
...displayFlex('flex-start', 'center'),
'favorite-heart': {},
};
});
const StyledTitleContent = styled.div(({ theme }) => {
return {
maxWidth: '90%',
marginRight: '18px',
...textEllipsis(1),
};
});
const StyledFavoriteButton = styled.button<{ favorite: boolean }>(
({ theme, favorite }) => {
return {
width: '32px',
height: '32px',
justifyContent: 'center',
alignItems: 'center',
display: 'none',
color: favorite ? theme.colors.primaryColor : theme.colors.iconColor,
'&:hover': {
color: theme.colors.primaryColor,
},
};
}
);
const StyledTableRow = styled(TableRow)(({ theme }) => {
return {
'&:hover': {
'.favorite-button': {
display: 'flex',
},
},
};
});
const OperationArea = ({ pageMeta }: { pageMeta: PageMeta }) => {
const { id, favorite } = pageMeta;
const { openPage, toggleFavoritePage, toggleDeletePage } = useEditor();
const OperationMenu = (
<>
<MenuItem
onClick={() => {
toggleFavoritePage(id);
}}
>
{favorite ? 'Remove' : 'Add'} to favourites
</MenuItem>
<MenuItem
onClick={() => {
openPage(id);
}}
>
Open in new tab
</MenuItem>
<MenuItem
onClick={() => {
toggleDeletePage(id);
}}
>
Delete
</MenuItem>
</>
);
return (
<Wrapper alignItems="center" justifyContent="center">
<Menu content={OperationMenu} placement="bottom-end" disablePortal={true}>
<IconButton hoverBackground="#E0E6FF">
<MoreVertical_24pxIcon />
</IconButton>
</Menu>
</Wrapper>
);
};
export const AllPage = () => {
const { confirm } = useConfirm();
const { pageList, toggleFavoritePage } = useEditor();
return (
<>
<Header />
<button
onClick={() => {
confirm({
title: 'Permanently delete',
content: "Once deleted, you can't undo this action.Do you confirm?",
confirmText: 'Delete',
confirmType: 'danger',
});
}}
>
click to show confirm
</button>
<StyledTableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell proportion={0.5}>Documents</TableCell>
<TableCell proportion={0.2}>Created</TableCell>
<TableCell proportion={0.2}>Uploaded</TableCell>
<TableCell proportion={0.1}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{pageList.map((pageMeta, index) => {
return (
<StyledTableRow key={`${pageMeta.id}-${index}`}>
<TableCell>
<StyledTitleWrapper>
<StyledTitleContent>
{pageMeta.title || pageMeta.id}
</StyledTitleContent>
<StyledFavoriteButton
className="favorite-button"
favorite={pageMeta.favorite}
onClick={() => {
toggleFavoritePage(pageMeta.id);
}}
>
{pageMeta.favorite ? (
<MiddleFavouritedStatus2Icon />
) : (
<MiddleFavouritesIcon />
)}
</StyledFavoriteButton>
</StyledTitleWrapper>
</TableCell>
<TableCell ellipsis={true}>{pageMeta.createDate}</TableCell>
<TableCell ellipsis={true}>{pageMeta.createDate}</TableCell>
<TableCell>
<OperationArea pageMeta={pageMeta} />
</TableCell>
</StyledTableRow>
);
})}
</TableBody>
</Table>
</StyledTableContainer>
</>
);
};
export default AllPage;
+18
View File
@@ -0,0 +1,18 @@
import { Header } from '@/components/header';
import { useEditor } from '@/providers/editor-provider';
import { PageList } from '@/components/page-list';
export const All = () => {
const { pageList: allPageList } = useEditor();
return (
<>
<Header />
<PageList
pageList={allPageList.filter(p => !p.trash)}
showFavoriteTag={true}
/>
</>
);
};
export default All;
@@ -0,0 +1,15 @@
import { Header } from '@/components/header';
import { useEditor } from '@/providers/editor-provider';
import { PageList } from '@/components/page-list';
export const Favorite = () => {
const { pageList: allPageList } = useEditor();
return (
<>
<Header />
<PageList pageList={allPageList.filter(p => p.favorite && !p.trash)} />
</>
);
};
export default Favorite;
@@ -0,0 +1,15 @@
import { Header } from '@/components/header';
import { useEditor } from '@/providers/editor-provider';
import { PageList } from '@/components/page-list';
export const Trash = () => {
const { pageList: allPageList } = useEditor();
return (
<>
<Header />
<PageList pageList={allPageList.filter(p => p.trash)} isTrash={true} />
</>
);
};
export default Trash;
+28 -16
View File
@@ -3,12 +3,12 @@ import type { PropsWithChildren } from 'react';
import { Confirm, ConfirmProps } from '@/ui/confirm'; import { Confirm, ConfirmProps } from '@/ui/confirm';
type ConfirmContextValue = { type ConfirmContextValue = {
confirm: (props: ConfirmProps) => void; confirm: (props: ConfirmProps) => Promise<boolean>;
}; };
type ConfirmContextProps = PropsWithChildren<{}>; type ConfirmContextProps = PropsWithChildren<{}>;
export const ConfirmContext = createContext<ConfirmContextValue>({ export const ConfirmContext = createContext<ConfirmContextValue>({
confirm: () => {}, confirm: () => Promise.resolve(false),
}); });
export const useConfirm = () => useContext(ConfirmContext); export const useConfirm = () => useContext(ConfirmContext);
@@ -22,21 +22,33 @@ export const ConfirmProvider = ({
return ( return (
<ConfirmContext.Provider <ConfirmContext.Provider
value={{ value={{
confirm: (props: ConfirmProps) => { confirm: ({ onClose, onCancel, onConfirm, ...props }: ConfirmProps) => {
const confirmId = String(Date.now()); return new Promise((resolve, reject) => {
setConfirmRecord(oldConfirmRecord => { const confirmId = String(Date.now());
return { const closeHandler = () => {
...oldConfirmRecord, delete confirmRecord[confirmId];
[confirmId]: ( setConfirmRecord({ ...confirmRecord });
<Confirm
{...props}
onClose={() => {
delete confirmRecord[confirmId];
setConfirmRecord({ ...confirmRecord });
}}
/>
),
}; };
setConfirmRecord(oldConfirmRecord => {
return {
...oldConfirmRecord,
[confirmId]: (
<Confirm
{...props}
onCancel={() => {
closeHandler();
onCancel?.();
resolve(false);
}}
onConfirm={() => {
closeHandler();
onConfirm?.();
resolve(true);
}}
/>
),
};
});
}); });
}, },
}} }}
@@ -39,6 +39,7 @@ type EditorHandlers = {
favoritePage: (pageId: string) => void; favoritePage: (pageId: string) => void;
unFavoritePage: (pageId: string) => void; unFavoritePage: (pageId: string) => void;
toggleFavoritePage: (pageId: string) => void; toggleFavoritePage: (pageId: string) => void;
permanentlyDeletePage: (pageId: string) => void;
}; };
type EditorContextProps = PropsWithChildren<{}>; type EditorContextProps = PropsWithChildren<{}>;
@@ -59,6 +60,7 @@ export const EditorContext = createContext<EditorContextValue>({
favoritePage: () => {}, favoritePage: () => {},
unFavoritePage: () => {}, unFavoritePage: () => {},
toggleFavoritePage: () => {}, toggleFavoritePage: () => {},
permanentlyDeletePage: () => {},
}); });
export const useEditor = () => useContext(EditorContext); export const useEditor = () => useContext(EditorContext);
@@ -108,19 +110,23 @@ export const EditorProvider = ({
}); });
}, },
deletePage: pageId => { deletePage: pageId => {
workspace?.setPage(pageId, { trash: true }); workspace?.setPageMeta(pageId, { trash: true });
}, },
recyclePage: pageId => { recyclePage: pageId => {
workspace?.setPage(pageId, { trash: false }); workspace?.setPageMeta(pageId, { trash: false });
}, },
toggleDeletePage: pageId => { toggleDeletePage: pageId => {
const pageMeta = workspace?.meta.pages.find(p => p.id === pageId); const pageMeta = workspace?.meta.pages.find(p => p.id === pageId);
if (pageMeta) { if (pageMeta) {
workspace?.setPage(pageId, { trash: !pageMeta.trash }); workspace?.setPageMeta(pageId, { trash: !pageMeta.trash });
} }
}, },
favoritePage: pageId => { favoritePage: pageId => {
workspace?.setPage(pageId, { favorite: true }); workspace?.setPageMeta(pageId, { favorite: true });
},
permanentlyDeletePage: pageId => {
// TODO: workspace.meta.removePage or workspace.removePage?
workspace?.meta.removePage(pageId);
}, },
unFavoritePage: pageId => { unFavoritePage: pageId => {
workspace?.setPageMeta(pageId, { favorite: true }); workspace?.setPageMeta(pageId, { favorite: true });
+21 -11
View File
@@ -1,9 +1,13 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { import {
ThemeProvider as EmotionThemeProvider, ThemeProvider as EmotionThemeProvider,
Global, Global,
css, css,
} from '@emotion/react'; } from '@emotion/react';
import { createContext, useContext, useEffect, useState } from 'react'; import {
ThemeProvider as MuiThemeProvider,
createTheme as MuiCreateTheme,
} from '@mui/material/styles';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { import {
Theme, Theme,
@@ -26,6 +30,7 @@ export const ThemeContext = createContext<ThemeProviderValue>({
}); });
export const useTheme = () => useContext(ThemeContext); export const useTheme = () => useContext(ThemeContext);
const muiTheme = MuiCreateTheme();
export const ThemeProvider = ({ export const ThemeProvider = ({
defaultTheme = 'light', defaultTheme = 'light',
@@ -87,16 +92,21 @@ export const ThemeProvider = ({
// }, [mode]); // }, [mode]);
return ( return (
<ThemeContext.Provider value={{ mode, changeMode, theme: themeStyle }}> // Use MuiThemeProvider is just because some Transitions in Mui components need it
<Global <MuiThemeProvider theme={muiTheme}>
styles={css` <ThemeContext.Provider value={{ mode, changeMode, theme: themeStyle }}>
:root { <Global
${globalThemeVariables(mode, themeStyle) as {}} styles={css`
} :root {
`} ${globalThemeVariables(mode, themeStyle) as {}}
/> }
<EmotionThemeProvider theme={themeStyle}>{children}</EmotionThemeProvider> `}
</ThemeContext.Provider> />
<EmotionThemeProvider theme={themeStyle}>
{children}
</EmotionThemeProvider>
</ThemeContext.Provider>
</MuiThemeProvider>
); );
}; };
+61
View File
@@ -0,0 +1,61 @@
import {
HTMLAttributes,
cloneElement,
ReactElement,
Children,
CSSProperties,
forwardRef,
PropsWithChildren,
} from 'react';
import { StyledButton } from './styles';
import { ButtonProps } from './interface';
import { getSize } from './utils';
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
size = 'default',
disabled = false,
hoverBackground,
hoverColor,
hoverStyle,
shape = 'default',
icon,
type = 'default',
children,
bold = false,
...props
},
ref
) => {
const { iconSize } = getSize(size);
return (
<StyledButton
ref={ref}
disabled={disabled}
size={size}
shape={shape}
hoverBackground={hoverBackground}
hoverColor={hoverColor}
hoverStyle={hoverStyle}
// @ts-ignore
type={type}
bold={bold}
{...props}
>
{icon &&
cloneElement(Children.only(icon), {
width: iconSize,
height: iconSize,
className: `affine-button-icon ${icon.props.className ?? ''}`,
})}
{children && <span>{children}</span>}
</StyledButton>
);
}
);
Button.displayName = 'Button';
export default Button;
+1 -1
View File
@@ -11,7 +11,7 @@ import { StyledIconButton } from './styles';
const SIZE_SMALL = 'small' as const; const SIZE_SMALL = 'small' as const;
const SIZE_MIDDLE = 'middle' as const; const SIZE_MIDDLE = 'middle' as const;
const SIZE_NORMAL = 'normal' as const; const SIZE_NORMAL = 'normal' as const;
// TODO: Designer is not sure about the size of the icon button // TODO: IconButton should merge into Button, but it has not been designed yet
const SIZE_CONFIG = { const SIZE_CONFIG = {
[SIZE_SMALL]: { [SIZE_SMALL]: {
iconSize: 16, iconSize: 16,
+1
View File
@@ -1 +1,2 @@
export * from './icon-button'; export * from './icon-button';
export * from './button';
+23
View File
@@ -0,0 +1,23 @@
import {
CSSProperties,
HTMLAttributes,
PropsWithChildren,
ReactElement,
} from 'react';
export const SIZE_SMALL = 'small' as const;
export const SIZE_MIDDLE = 'middle' as const;
export const SIZE_DEFAULT = 'default' as const;
export type ButtonProps = PropsWithChildren &
Omit<HTMLAttributes<HTMLButtonElement>, 'type'> & {
size?: typeof SIZE_SMALL | typeof SIZE_MIDDLE | typeof SIZE_DEFAULT;
disabled?: boolean;
hoverBackground?: CSSProperties['background'];
hoverColor?: CSSProperties['color'];
hoverStyle?: CSSProperties;
icon?: ReactElement;
shape?: 'default' | 'round' | 'circle';
type?: 'primary' | 'warning' | 'danger' | 'default';
bold?: boolean;
};
+66
View File
@@ -1,5 +1,7 @@
import { absoluteCenter, displayInlineFlex, styled } from '@/styles'; import { absoluteCenter, displayInlineFlex, styled } from '@/styles';
import { CSSProperties } from 'react'; import { CSSProperties } from 'react';
import { ButtonProps } from '@/ui/button/interface';
import { getSize, getButtonColors } from './utils';
export const StyledIconButton = styled.button<{ export const StyledIconButton = styled.button<{
width: number; width: number;
@@ -53,3 +55,67 @@ export const StyledIconButton = styled.button<{
}; };
} }
); );
export const StyledButton = styled.button<
Pick<
ButtonProps,
| 'size'
| 'disabled'
| 'hoverBackground'
| 'hoverColor'
| 'hoverStyle'
| 'shape'
| 'type'
| 'bold'
>
>(
({
theme,
size = 'default',
disabled,
hoverBackground,
hoverColor,
hoverStyle,
bold = false,
shape = 'default',
type = 'default',
}) => {
const { fontSize, borderRadius, padding, height } = getSize(size);
return {
height,
paddingLeft: padding,
paddingRight: padding,
border: '1px solid',
...displayInlineFlex('flex-start', 'center'),
position: 'relative',
...(disabled ? { cursor: 'not-allowed', pointerEvents: 'none' } : {}),
transition: 'background .15s',
// TODO: Implement circle shape
borderRadius: shape === 'default' ? borderRadius : height / 2,
fontSize,
fontWeight: bold ? '500' : '400',
'.affine-button-icon': {
color: theme.colors.iconColor,
},
'>span': {
marginLeft: '5px',
},
// @ts-ignore
...getButtonColors(theme, type, {
hoverBackground,
hoverColor,
hoverStyle,
}),
//
// ':hover': {
// color: hoverColor ?? theme.colors.primaryColor,
// background: hoverBackground ?? theme.colors.hoverBackground,
// '.affine-button-icon':{
//
// }
// ...(hoverStyle ?? {}),
// },
};
}
);
+104
View File
@@ -0,0 +1,104 @@
import { AffineTheme } from '@/styles';
import {
SIZE_SMALL,
SIZE_MIDDLE,
SIZE_DEFAULT,
ButtonProps,
} from './interface';
// TODO: Designer is not sure about the size, Now, is just use default size
export const SIZE_CONFIG = {
[SIZE_SMALL]: {
iconSize: 16,
fontSize: 16,
borderRadius: 6,
height: 26,
padding: 24,
},
[SIZE_MIDDLE]: {
iconSize: 20,
fontSize: 16,
borderRadius: 6,
height: 32,
padding: 24,
},
[SIZE_DEFAULT]: {
iconSize: 24,
fontSize: 16,
height: 38,
padding: 24,
borderRadius: 6,
},
} as const;
export const getSize = (
size: typeof SIZE_SMALL | typeof SIZE_MIDDLE | typeof SIZE_DEFAULT
) => {
return SIZE_CONFIG[size];
};
export const getButtonColors = (
theme: AffineTheme,
type: ButtonProps['type'],
extend?: {
hoverBackground: ButtonProps['hoverBackground'];
hoverColor: ButtonProps['hoverColor'];
hoverStyle: ButtonProps['hoverStyle'];
}
) => {
switch (type) {
case 'primary':
return {
background: theme.colors.primaryColor,
color: '#fff',
borderColor: theme.colors.primaryColor,
'.affine-button-icon': {
color: '#fff',
},
};
case 'warning':
return {
background: theme.colors.warningBackground,
color: theme.colors.warningColor,
borderColor: theme.colors.warningBackground,
'.affine-button-icon': {
color: theme.colors.warningColor,
},
':hover': {
borderColor: theme.colors.warningColor,
color: extend?.hoverColor,
background: extend?.hoverBackground,
...extend?.hoverStyle,
},
};
case 'danger':
return {
background: theme.colors.errorBackground,
color: theme.colors.errorColor,
borderColor: theme.colors.errorBackground,
'.affine-button-icon': {
color: theme.colors.errorColor,
},
':hover': {
borderColor: theme.colors.errorColor,
color: extend?.hoverColor,
background: extend?.hoverBackground,
...extend?.hoverStyle,
},
};
default:
return {
color: theme.colors.popoverColor,
borderColor: theme.colors.borderColor,
':hover': {
borderColor: theme.colors.primaryColor,
color: extend?.hoverColor ?? theme.colors.primaryColor,
'.affine-button-icon': {
color: extend?.hoverColor ?? theme.colors.primaryColor,
background: extend?.hoverBackground,
...extend?.hoverStyle,
},
},
};
}
};
+11 -7
View File
@@ -5,9 +5,8 @@ import {
StyledConfirmContent, StyledConfirmContent,
StyledConfirmTitle, StyledConfirmTitle,
StyledModalWrapper, StyledModalWrapper,
StyledButton,
} from '@/ui/confirm/styles'; } from '@/ui/confirm/styles';
import { Button } from '@/ui/button';
export type ConfirmProps = { export type ConfirmProps = {
title?: string; title?: string;
content?: string; content?: string;
@@ -40,23 +39,28 @@ export const Confirm = ({
<StyledConfirmContent>{content}</StyledConfirmContent> <StyledConfirmContent>{content}</StyledConfirmContent>
<StyledButtonWrapper> <StyledButtonWrapper>
<StyledButton <Button
shape="round"
bold={true}
onClick={() => { onClick={() => {
setOpen(false); setOpen(false);
onCancel?.(); onCancel?.();
}} }}
style={{ marginRight: '24px' }}
> >
Cancel Cancel
</StyledButton> </Button>
<StyledButton <Button
confirmType={confirmType} type={confirmType}
shape="round"
bold={true}
onClick={() => { onClick={() => {
setOpen(false); setOpen(false);
onConfirm?.(); onConfirm?.();
}} }}
> >
{confirmText} {confirmText}
</StyledButton> </Button>
</StyledButtonWrapper> </StyledButtonWrapper>
</StyledModalWrapper> </StyledModalWrapper>
</Modal> </Modal>
-18
View File
@@ -78,21 +78,3 @@ const getButtonColors = (
}; };
} }
}; };
export const StyledButton = styled.button<Pick<ConfirmProps, 'confirmType'>>(
({ theme, confirmType }) => {
return {
width: '100px',
height: '38px',
borderRadius: '19px',
border: '1px solid',
...getButtonColors(theme, confirmType),
fontSize: theme.font.sm,
fontWeight: 500,
'&:first-of-type': {
marginRight: '24px',
},
};
}
);
-1
View File
@@ -4,7 +4,6 @@ import { StyledTableCell } from './styles';
export const TableCell = ({ export const TableCell = ({
children, children,
ellipsis,
...props ...props
}: PropsWithChildren< }: PropsWithChildren<
TableCellProps & HTMLAttributes<HTMLTableCellElement> TableCellProps & HTMLAttributes<HTMLTableCellElement>