mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-23 21:38:44 +08:00
refactor!: next generation AFFiNE code structure (#1176)
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import { RemWorkspace } from '../../../../shared';
|
||||
import { WorkspaceAvatar } from '../../workspace-avatar';
|
||||
import { SelectorWrapper, WorkspaceName } from './styles';
|
||||
|
||||
export type WorkspaceSelectorProps = {
|
||||
currentWorkspace: RemWorkspace | null;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const WorkspaceSelector: React.FC<WorkspaceSelectorProps> = ({
|
||||
currentWorkspace,
|
||||
onClick,
|
||||
}) => {
|
||||
let name = 'Untitled Workspace';
|
||||
if (currentWorkspace) {
|
||||
if (currentWorkspace.flavour === 'affine') {
|
||||
if (currentWorkspace.firstBinarySynced) {
|
||||
name = currentWorkspace.blockSuiteWorkspace.meta.name;
|
||||
}
|
||||
} else if (currentWorkspace.flavour === 'local') {
|
||||
name = currentWorkspace.blockSuiteWorkspace.meta.name;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SelectorWrapper onClick={onClick} data-testid="current-workspace">
|
||||
<WorkspaceAvatar
|
||||
data-testid="workspace-avatar"
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
size={32}
|
||||
workspace={currentWorkspace}
|
||||
/>
|
||||
<WorkspaceName data-testid="workspace-name">{name}</WorkspaceName>
|
||||
</SelectorWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WorkspaceSelector';
|
||||
@@ -0,0 +1,32 @@
|
||||
import { MuiAvatar, textEllipsis } from '@affine/component';
|
||||
import { styled } from '@affine/component';
|
||||
export const SelectorWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
height: '42px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 12px',
|
||||
borderRadius: '5px',
|
||||
color: theme.colors.textColor,
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
background: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const Avatar = styled(MuiAvatar)({
|
||||
height: '28px',
|
||||
width: '28px',
|
||||
});
|
||||
|
||||
export const WorkspaceName = styled('span')(({ theme }) => {
|
||||
return {
|
||||
marginLeft: '12px',
|
||||
fontSize: theme.font.h6,
|
||||
fontWeight: 500,
|
||||
marginTop: '4px',
|
||||
flexGrow: 1,
|
||||
...textEllipsis(1),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
export const Arrow = () => {
|
||||
return (
|
||||
<svg
|
||||
width="6"
|
||||
height="10"
|
||||
viewBox="0 0 6 10"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M0.354 9.22997C0.201333 9.0773 0.125 8.91764 0.125 8.75097C0.125 8.5843 0.201333 8.42464 0.354 8.27197L3.625 5.00097L0.354 1.72997C0.201333 1.5773 0.125 1.41764 0.125 1.25097C0.125 1.0843 0.201333 0.924636 0.354 0.771969C0.506667 0.619302 0.666333 0.542969 0.833 0.542969C0.999667 0.542969 1.15933 0.619302 1.312 0.771969L4.979 4.43897C5.06233 4.52164 5.125 4.61164 5.167 4.70897C5.20833 4.8063 5.229 4.90364 5.229 5.00097C5.229 5.0983 5.20833 5.19564 5.167 5.29297C5.125 5.3903 5.06233 5.4803 4.979 5.56297L1.312 9.22997C1.15933 9.38264 0.999667 9.45897 0.833 9.45897C0.666333 9.45897 0.506667 9.38264 0.354 9.22997Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
export const AffineIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
width="39"
|
||||
height="39"
|
||||
rx="19.5"
|
||||
stroke="#6880FF"
|
||||
fill="#FFF"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M18.6303 8.79688L11.2559 29.8393H15.5752L20.2661 15.2858L24.959 29.8393H29.2637L21.8881 8.79688H18.6303Z"
|
||||
fill="#6880FF"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,273 @@
|
||||
import { MuiCollapse } from '@affine/component';
|
||||
import { Tooltip } from '@affine/component';
|
||||
import { IconButton } from '@affine/component';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
DeleteTemporarilyIcon,
|
||||
FavoriteIcon,
|
||||
FolderIcon,
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
SettingsIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { PageMeta } from '@blocksuite/store';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { usePageMeta } from '../../../hooks/use-page-meta';
|
||||
import { RemWorkspace } from '../../../shared';
|
||||
import { Arrow } from './icons';
|
||||
import {
|
||||
StyledArrowButton,
|
||||
StyledLink,
|
||||
StyledListItem,
|
||||
StyledNewPageButton,
|
||||
StyledSliderBar,
|
||||
StyledSliderBarWrapper,
|
||||
StyledSubListItem,
|
||||
} from './style';
|
||||
import { WorkspaceSelector } from './WorkspaceSelector';
|
||||
|
||||
export type FavoriteListProps = {
|
||||
currentPageId: string | null;
|
||||
openPage: (pageId: string) => void;
|
||||
showList: boolean;
|
||||
pageMeta: PageMeta[];
|
||||
};
|
||||
|
||||
const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
pageMeta,
|
||||
openPage,
|
||||
showList,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const favoriteList = useMemo(
|
||||
() => pageMeta.filter(p => p.favorite && !p.trash),
|
||||
[pageMeta]
|
||||
);
|
||||
return (
|
||||
<MuiCollapse in={showList}>
|
||||
{favoriteList.map((pageMeta, index) => {
|
||||
const active = router.query.pageId === pageMeta.id;
|
||||
return (
|
||||
<StyledSubListItem
|
||||
data-testid={`favorite-list-item-${pageMeta.id}`}
|
||||
active={active}
|
||||
key={`${pageMeta}-${index}`}
|
||||
onClick={() => {
|
||||
if (active) {
|
||||
return;
|
||||
}
|
||||
openPage(pageMeta.id);
|
||||
}}
|
||||
>
|
||||
{pageMeta.title || 'Untitled'}
|
||||
</StyledSubListItem>
|
||||
);
|
||||
})}
|
||||
{favoriteList.length === 0 && (
|
||||
<StyledSubListItem disable={true}>{t('No item')}</StyledSubListItem>
|
||||
)}
|
||||
</MuiCollapse>
|
||||
);
|
||||
};
|
||||
|
||||
export type WorkSpaceSliderBarProps = {
|
||||
isPublicWorkspace: boolean;
|
||||
onOpenQuickSearchModal: () => void;
|
||||
onOpenWorkspaceListModal: () => void;
|
||||
currentWorkspace: RemWorkspace | null;
|
||||
currentPageId: string | null;
|
||||
openPage: (pageId: string) => void;
|
||||
createPage: () => Promise<string>;
|
||||
show: boolean;
|
||||
setShow: (show: boolean) => void;
|
||||
currentPath: string;
|
||||
paths: {
|
||||
all: (workspaceId: string) => string;
|
||||
favorite: (workspaceId: string) => string;
|
||||
trash: (workspaceId: string) => string;
|
||||
setting: (workspaceId: string) => string;
|
||||
};
|
||||
};
|
||||
|
||||
export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
||||
isPublicWorkspace,
|
||||
currentWorkspace,
|
||||
currentPageId,
|
||||
openPage,
|
||||
createPage,
|
||||
show,
|
||||
setShow,
|
||||
currentPath,
|
||||
paths,
|
||||
onOpenQuickSearchModal,
|
||||
onOpenWorkspaceListModal,
|
||||
}) => {
|
||||
const currentWorkspaceId = currentWorkspace?.id || null;
|
||||
const [showSubFavorite, setShowSubFavorite] = useState(true);
|
||||
const [showTip, setShowTip] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const pageMeta = usePageMeta(currentWorkspace?.blockSuiteWorkspace ?? null);
|
||||
const onClickNewPage = useCallback(async () => {
|
||||
const pageId = await createPage();
|
||||
if (pageId) {
|
||||
openPage(pageId);
|
||||
}
|
||||
}, [createPage, openPage]);
|
||||
return (
|
||||
<>
|
||||
<StyledSliderBar show={isPublicWorkspace ? false : show}>
|
||||
<Tooltip
|
||||
content={show ? t('Collapse sidebar') : t('Expand sidebar')}
|
||||
placement="right"
|
||||
visible={showTip}
|
||||
>
|
||||
<StyledArrowButton
|
||||
data-testid="sliderBar-arrowButton"
|
||||
isShow={show}
|
||||
style={{
|
||||
visibility: isPublicWorkspace ? 'hidden' : 'visible',
|
||||
}}
|
||||
onClick={useCallback(() => {
|
||||
setShow(!show);
|
||||
setShowTip(false);
|
||||
}, [setShow, show])}
|
||||
onMouseEnter={useCallback(() => {
|
||||
setShowTip(true);
|
||||
}, [])}
|
||||
onMouseLeave={useCallback(() => {
|
||||
setShowTip(false);
|
||||
}, [])}
|
||||
>
|
||||
<Arrow />
|
||||
</StyledArrowButton>
|
||||
</Tooltip>
|
||||
|
||||
<StyledSliderBarWrapper data-testid="sliderBar">
|
||||
<WorkspaceSelector
|
||||
currentWorkspace={currentWorkspace}
|
||||
onClick={onOpenWorkspaceListModal}
|
||||
/>
|
||||
|
||||
<StyledListItem
|
||||
data-testid="slider-bar-quick-search-button"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={useCallback(() => {
|
||||
onOpenQuickSearchModal();
|
||||
}, [onOpenQuickSearchModal])}
|
||||
>
|
||||
<SearchIcon />
|
||||
{t('Quick search')}
|
||||
</StyledListItem>
|
||||
<Link
|
||||
href={{
|
||||
pathname: currentWorkspaceId && paths.all(currentWorkspaceId),
|
||||
}}
|
||||
>
|
||||
<StyledListItem
|
||||
active={
|
||||
currentPath ===
|
||||
(currentWorkspaceId && paths.all(currentWorkspaceId))
|
||||
}
|
||||
>
|
||||
<FolderIcon />
|
||||
<span data-testid="all-pages">{t('All pages')}</span>
|
||||
</StyledListItem>
|
||||
</Link>
|
||||
<StyledListItem
|
||||
active={
|
||||
currentPath ===
|
||||
(currentWorkspaceId && paths.favorite(currentWorkspaceId))
|
||||
}
|
||||
>
|
||||
<StyledLink
|
||||
href={{
|
||||
pathname:
|
||||
currentWorkspaceId && paths.favorite(currentWorkspaceId),
|
||||
}}
|
||||
>
|
||||
<FavoriteIcon />
|
||||
{t('Favorites')}
|
||||
</StyledLink>
|
||||
<IconButton
|
||||
darker={true}
|
||||
onClick={useCallback(() => {
|
||||
setShowSubFavorite(!showSubFavorite);
|
||||
}, [showSubFavorite])}
|
||||
>
|
||||
<ArrowDownSmallIcon
|
||||
style={{
|
||||
transform: `rotate(${showSubFavorite ? '180' : '0'}deg)`,
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</StyledListItem>
|
||||
<FavoriteList
|
||||
currentPageId={currentPageId}
|
||||
showList={showSubFavorite}
|
||||
openPage={openPage}
|
||||
pageMeta={pageMeta}
|
||||
/>
|
||||
<StyledListItem
|
||||
active={
|
||||
currentPath ===
|
||||
(currentWorkspaceId && paths.setting(currentWorkspaceId))
|
||||
}
|
||||
>
|
||||
<StyledLink
|
||||
href={{
|
||||
pathname:
|
||||
currentWorkspaceId && paths.setting(currentWorkspaceId),
|
||||
}}
|
||||
>
|
||||
<SettingsIcon />
|
||||
{t('Workspace Settings')}
|
||||
</StyledLink>
|
||||
</StyledListItem>
|
||||
|
||||
{/* <WorkspaceSetting
|
||||
isShow={showWorkspaceSetting}
|
||||
onClose={() => {
|
||||
setShowWorkspaceSetting(false);
|
||||
}}
|
||||
/> */}
|
||||
{/* TODO: will finish the feature next version */}
|
||||
{/* <StyledListItem
|
||||
onClick={() => {
|
||||
triggerImportModal();
|
||||
}}
|
||||
>
|
||||
<ImportIcon /> {t('Import')}
|
||||
</StyledListItem> */}
|
||||
|
||||
<Link
|
||||
href={{
|
||||
pathname: currentWorkspaceId && paths.trash(currentWorkspaceId),
|
||||
}}
|
||||
>
|
||||
<StyledListItem
|
||||
active={
|
||||
currentPath ===
|
||||
(currentWorkspaceId && paths.trash(currentWorkspaceId))
|
||||
}
|
||||
>
|
||||
<DeleteTemporarilyIcon /> {t('Trash')}
|
||||
</StyledListItem>
|
||||
</Link>
|
||||
<StyledNewPageButton
|
||||
data-testid="new-page-button"
|
||||
onClick={onClickNewPage}
|
||||
>
|
||||
<PlusIcon /> {t('New Page')}
|
||||
</StyledNewPageButton>
|
||||
</StyledSliderBarWrapper>
|
||||
</StyledSliderBar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkSpaceSliderBar;
|
||||
@@ -0,0 +1,139 @@
|
||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const StyledSliderBar = styled.div<{ show: boolean }>(
|
||||
({ theme, show }) => {
|
||||
return {
|
||||
width: show ? '256px' : '0',
|
||||
height: '100vh',
|
||||
minHeight: '450px',
|
||||
background: theme.colors.hubBackground,
|
||||
boxShadow: theme.shadow.modal,
|
||||
transition: 'width .15s, padding .15s',
|
||||
position: 'relative',
|
||||
zIndex: theme.zIndex.modal,
|
||||
padding: show ? '24px 12px' : '24px 0',
|
||||
flexShrink: 0,
|
||||
};
|
||||
}
|
||||
);
|
||||
export const StyledSliderBarWrapper = styled.div(() => {
|
||||
return {
|
||||
height: '100%',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
position: 'relative',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledArrowButton = styled.button<{ isShow: boolean }>(
|
||||
({ theme, isShow }) => {
|
||||
return {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
...displayFlex('center', 'center'),
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
borderRadius: '50%',
|
||||
transition: 'all .15s',
|
||||
position: 'absolute',
|
||||
top: '34px',
|
||||
right: '-20px',
|
||||
zIndex: theme.zIndex.modal,
|
||||
svg: {
|
||||
transform: isShow ? 'rotate(180deg)' : 'unset',
|
||||
},
|
||||
':hover': {
|
||||
color: '#fff',
|
||||
backgroundColor: theme.colors.primaryColor,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledListItem = styled.div<{
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
}>(({ theme, active, disabled }) => {
|
||||
return {
|
||||
height: '32px',
|
||||
marginTop: '12px',
|
||||
color: active ? theme.colors.primaryColor : theme.colors.textColor,
|
||||
paddingLeft: '12px',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
...(disabled
|
||||
? {
|
||||
cursor: 'not-allowed',
|
||||
color: theme.colors.borderColor,
|
||||
}
|
||||
: {}),
|
||||
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
':hover:not([disabled])': {
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledLink = styled(Link)(() => {
|
||||
return {
|
||||
flexGrow: 1,
|
||||
textAlign: 'left',
|
||||
color: 'inherit',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
':visited': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledNewPageButton = styled(StyledListItem)(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
bottom: '24px',
|
||||
left: '0',
|
||||
right: '0',
|
||||
margin: 'auto',
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSubListItem = styled.button<{
|
||||
disable?: boolean;
|
||||
active?: boolean;
|
||||
}>(({ theme, disable, active }) => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '32px',
|
||||
marginTop: '4px',
|
||||
color: disable
|
||||
? theme.colors.disableColor
|
||||
: active
|
||||
? theme.colors.primaryColor
|
||||
: theme.colors.popoverColor,
|
||||
backgroundColor: active ? theme.colors.hoverBackground : 'unset',
|
||||
|
||||
cursor: disable ? 'not-allowed' : 'pointer',
|
||||
paddingLeft: '45px',
|
||||
lineHeight: '32px',
|
||||
textAlign: 'start',
|
||||
...textEllipsis(1),
|
||||
':hover': disable
|
||||
? {}
|
||||
: {
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user