feat(component): init app sidebar (#2135)

This commit is contained in:
Himself65
2023-04-27 16:46:08 -05:00
committed by GitHub
parent f3cbe54625
commit c1a65b6b76
23 changed files with 668 additions and 451 deletions
@@ -1,6 +1,8 @@
import { Tooltip } from '@affine/component';
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
import { getEnvironment } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAtom } from 'jotai';
import { useCallback, useEffect, useState } from 'react';
import {
@@ -8,7 +10,6 @@ import {
useGuideHiddenUntilNextUpdate,
useUpdateTipsOnVersionChange,
} from '../../../hooks/use-is-first-load';
import { useSidebarStatus } from '../../../hooks/use-sidebar-status';
import { SidebarSwitchIcon } from './icons';
import { StyledSidebarSwitch } from './style';
type SidebarSwitchProps = {
@@ -24,7 +25,7 @@ export const SidebarSwitch = ({
...props
}: SidebarSwitchProps) => {
useUpdateTipsOnVersionChange();
const [open, setOpen] = useSidebarStatus();
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const [tooltipVisible, setTooltipVisible] = useState(false);
const [guideHidden, setGuideHidden] = useGuideHidden();
const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] =
@@ -56,9 +57,9 @@ export const SidebarSwitch = ({
visible={visible}
disabled={!visible}
onClick={useCallback(() => {
setOpen(!open);
setOpen(open => !open);
setTooltipVisible(false);
if (guideHiddenUntilNextUpdate['quickSearchTips'] === false) {
if (!guideHiddenUntilNextUpdate['quickSearchTips']) {
setGuideHiddenUntilNextUpdate({
...guideHiddenUntilNextUpdate,
quickSearchTips: true,
@@ -70,7 +71,6 @@ export const SidebarSwitch = ({
}, [
guideHidden,
guideHiddenUntilNextUpdate,
open,
setGuideHidden,
setGuideHiddenUntilNextUpdate,
setOpen,
@@ -1,7 +1,9 @@
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
import { useTranslation } from '@affine/i18n';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { CloseIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { useAtom } from 'jotai';
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
import {
forwardRef,
@@ -12,10 +14,6 @@ import {
useState,
} from 'react';
import {
useSidebarFloating,
useSidebarStatus,
} from '../../../hooks/use-sidebar-status';
import type { AffineOfficialWorkspace } from '../../../shared';
import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
import EditPage from './header-right-items/EditPage';
@@ -142,17 +140,11 @@ export const Header = forwardRef<
useEffect(() => {
setShowWarning(shouldShowWarning());
}, []);
const [open] = useSidebarStatus();
const sidebarFloating = useSidebarFloating();
const [open] = useAtom(appSidebarOpenAtom);
const { t } = useTranslation();
return (
<StyledHeaderContainer
sidebarFloating={sidebarFloating && open}
ref={ref}
hasWarning={showWarning}
{...props}
>
<StyledHeaderContainer ref={ref} hasWarning={showWarning} {...props}>
<BrowserWarning
show={showWarning}
onClose={() => {
@@ -7,8 +7,7 @@ import {
export const StyledHeaderContainer = styled('div')<{
hasWarning: boolean;
sidebarFloating: boolean;
}>(({ theme, hasWarning, sidebarFloating }) => {
}>(({ hasWarning }) => {
return {
height: hasWarning ? '96px' : '52px',
flexShrink: 0,
@@ -16,10 +15,6 @@ export const StyledHeaderContainer = styled('div')<{
top: 0,
background: 'var(--affine-background-primary-color)',
zIndex: 1,
WebkitAppRegion: sidebarFloating ? '' : 'drag',
button: {
WebkitAppRegion: 'no-drag',
},
};
});
export const StyledHeader = styled('div')<{ hasWarning: boolean }>(
@@ -1,47 +1,6 @@
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { WorkspaceFlavour } from '@affine/workspace/type';
import {
DeleteTemporarilyIcon,
FolderIcon,
PlusIcon,
SearchIcon,
SettingsIcon,
ShareIcon,
} from '@blocksuite/icons';
import type { Page, PageMeta } from '@blocksuite/store';
import type React from 'react';
import type { UIEvent } from 'react';
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
import {
useSidebarFloating,
useSidebarResizing,
useSidebarStatus,
useSidebarWidth,
} from '../../../hooks/use-sidebar-status';
import type { AllWorkspace } from '../../../shared';
import { ChangeLog } from './changeLog';
import Favorite from './favorite';
import { RouteNavigation } from './RouteNavigation';
import { StyledListItem } from './shared-styles';
import {
StyledLink,
StyledNewPageButton,
StyledScrollWrapper,
StyledSidebarHeader,
StyledSliderBar,
StyledSliderBarInnerWrapper,
StyledSliderBarWrapper,
StyledSliderModalBackground,
} from './style';
import { WorkspaceSelector } from './WorkspaceSelector';
const SidebarSwitch = lazy(() =>
import('../../affine/sidebar-switch').then(module => ({
default: module.SidebarSwitch,
}))
);
export type FavoriteListProps = {
currentPageId: string | null;
@@ -67,204 +26,3 @@ export type WorkSpaceSliderBarProps = {
shared: (workspaceId: string) => string;
};
};
export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
isPublicWorkspace,
currentWorkspace,
currentPageId,
openPage,
createPage,
currentPath,
paths,
onOpenQuickSearchModal,
onOpenWorkspaceListModal,
}) => {
const currentWorkspaceId = currentWorkspace?.id || null;
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const { t } = useTranslation();
const [sidebarOpen, setSidebarOpen] = useSidebarStatus();
const onClickNewPage = useCallback(async () => {
const page = await createPage();
openPage(page.id);
}, [createPage, openPage]);
const floatingSlider = useSidebarFloating();
const [sliderWidth] = useSidebarWidth();
const [isResizing] = useSidebarResizing();
const [isScrollAtTop, setIsScrollAtTop] = useState(true);
const show = isPublicWorkspace ? false : sidebarOpen;
const actualWidth = floatingSlider ? 'calc(10vw + 400px)' : sliderWidth;
useEffect(() => {
if (environment.isDesktop) {
window.apis?.onSidebarVisibilityChange(sidebarOpen);
}
}, [sidebarOpen]);
useEffect(() => {
const keydown = (e: KeyboardEvent) => {
if ((e.key === '/' && e.metaKey) || (e.key === '/' && e.ctrlKey)) {
setSidebarOpen(!sidebarOpen);
}
};
document.addEventListener('keydown', keydown, { capture: true });
return () =>
document.removeEventListener('keydown', keydown, { capture: true });
}, [sidebarOpen, setSidebarOpen]);
return (
<>
<StyledSliderBarWrapper
resizing={isResizing}
floating={floatingSlider}
show={show}
style={{ width: actualWidth }}
data-testid="sliderBar-root"
>
<StyledSliderBar>
<StyledSidebarHeader>
<RouteNavigation />
<Suspense>
<SidebarSwitch
visible={sidebarOpen}
tooltipContent={t('Collapse sidebar')}
data-testid="sliderBar-arrowButton-collapse"
/>
</Suspense>
</StyledSidebarHeader>
<StyledSliderBarInnerWrapper data-testid="sliderBar-inner">
<WorkspaceSelector
currentWorkspace={currentWorkspace}
onClick={onOpenWorkspaceListModal}
/>
<ChangeLog />
<StyledListItem
data-testid="slider-bar-quick-search-button"
onClick={useCallback(() => {
onOpenQuickSearchModal();
}, [onOpenQuickSearchModal])}
>
<SearchIcon />
{t('Quick search')}
</StyledListItem>
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.setting(currentWorkspaceId))
}
data-testid="slider-bar-workspace-setting-button"
style={{
marginBottom: '16px',
}}
>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.setting(currentWorkspaceId),
}}
>
<SettingsIcon />
<div>{t('Workspace Settings')}</div>
</StyledLink>
</StyledListItem>
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.all(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname: currentWorkspaceId && paths.all(currentWorkspaceId),
}}
>
<FolderIcon />
<span data-testid="all-pages">{t('All pages')}</span>
</StyledLink>
</StyledListItem>
<StyledScrollWrapper
showTopBorder={!isScrollAtTop}
onScroll={(e: UIEvent<HTMLDivElement>) => {
(e.target as HTMLDivElement).scrollTop === 0
? setIsScrollAtTop(true)
: setIsScrollAtTop(false);
}}
>
{blockSuiteWorkspace && (
<Favorite
currentPath={currentPath}
paths={paths}
currentPageId={currentPageId}
openPage={openPage}
currentWorkspace={currentWorkspace}
/>
)}
</StyledScrollWrapper>
<div style={{ height: 16 }}></div>
{config.enableLegacyCloud &&
(currentWorkspace?.flavour === WorkspaceFlavour.AFFINE &&
currentWorkspace.public ? (
<StyledListItem>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.setting(currentWorkspaceId),
}}
>
<ShareIcon />
<span data-testid="Published-to-web">Published to web</span>
</StyledLink>
</StyledListItem>
) : (
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.shared(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.shared(currentWorkspaceId),
}}
>
<ShareIcon />
<span data-testid="shared-pages">{t('Shared Pages')}</span>
</StyledLink>
</StyledListItem>
))}
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.trash(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.trash(currentWorkspaceId),
}}
>
<DeleteTemporarilyIcon /> {t('Trash')}
</StyledLink>
</StyledListItem>
</StyledSliderBarInnerWrapper>
<StyledNewPageButton
data-testid="new-page-button"
onClick={onClickNewPage}
>
<PlusIcon /> {t('New Page')}
</StyledNewPageButton>
</StyledSliderBar>
</StyledSliderBarWrapper>
<StyledSliderModalBackground
data-testid="sliderBar-modalBackground"
active={floatingSlider && sidebarOpen}
onClick={() => setSidebarOpen(false)}
/>
</>
);
};
export default WorkSpaceSliderBar;
@@ -2,60 +2,10 @@ import { displayFlex, styled, textEllipsis } from '@affine/component';
import { baseTheme } from '@toeverything/theme';
import Link from 'next/link';
const macosElectron = environment.isDesktop && environment.isMacOs;
export const StyledSliderBarWrapper = styled('div')<{
show: boolean;
floating: boolean;
resizing: boolean;
}>(({ theme, show, floating, resizing }) => {
return {
height: '100%',
position: 'absolute',
'button, a': {
userSelect: 'none',
},
zIndex: 'var(--affine-z-index-modal)',
transition: resizing ? '' : 'transform .3s, width .3s, max-width .3s',
transform: show ? 'translateX(0)' : 'translateX(-100%)',
maxWidth: floating ? 'calc(10vw + 400px)' : 'calc(100vw - 698px)',
background:
!floating && macosElectron
? 'transparent'
: 'var(--affine-background-secondary-color)',
borderRight: macosElectron ? '' : '1px solid',
borderColor: 'var(--affine-border-color)',
};
});
export const StyledSliderBar = styled('div')(() => {
return {
whiteSpace: 'nowrap',
width: '100%',
height: '100%',
padding: '0 4px',
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
};
});
export const StyledSidebarHeader = styled('div')(() => {
return {
height: '52px',
flexShrink: 0,
padding: '0 16px 0 10px',
WebkitAppRegion: 'drag',
button: {
WebkitAppRegion: 'no-drag',
},
...displayFlex(macosElectron ? 'flex-end' : 'space-between', 'center'),
};
});
export const StyledSliderBarInnerWrapper = styled('div')(() => {
return {
flexGrow: 1,
// overflowX: 'hidden',
// overflowY: 'auto',
margin: '0 2px',
position: 'relative',
height: 'calc(100% - 52px * 2)',
display: 'flex',
@@ -91,8 +41,6 @@ export const StyledNewPageButton = styled('button')(({ theme }) => {
return {
height: '52px',
...displayFlex('flex-start', 'center'),
borderTop: '1px solid',
borderColor: 'var(--affine-border-color)',
padding: '0 8px 0 16px',
svg: {
fontSize: '20px',
@@ -0,0 +1,216 @@
import {
AppSidebar,
appSidebarOpenAtom,
ResizeIndicator,
} from '@affine/component/app-sidebar';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { WorkspaceFlavour } from '@affine/workspace/type';
import {
DeleteTemporarilyIcon,
FolderIcon,
PlusIcon,
SearchIcon,
SettingsIcon,
ShareIcon,
} from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import type { ReactElement, UIEvent } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AllWorkspace } from '../../shared';
import ChangeLog from '../pure/workspace-slider-bar/changeLog';
import Favorite from '../pure/workspace-slider-bar/favorite';
import { StyledListItem } from '../pure/workspace-slider-bar/shared-styles';
import {
StyledLink,
StyledNewPageButton,
StyledScrollWrapper,
StyledSliderBarInnerWrapper,
} from '../pure/workspace-slider-bar/style';
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
export type RootAppSidebarProps = {
isPublicWorkspace: boolean;
onOpenQuickSearchModal: () => void;
onOpenWorkspaceListModal: () => void;
currentWorkspace: AllWorkspace | null;
currentPageId: string | null;
openPage: (pageId: string) => void;
createPage: () => Page;
currentPath: string;
paths: {
all: (workspaceId: string) => string;
favorite: (workspaceId: string) => string;
trash: (workspaceId: string) => string;
setting: (workspaceId: string) => string;
shared: (workspaceId: string) => string;
};
};
/**
* This is for the whole affine app sidebar.
* This component wraps the app sidebar in `@affine/component` with logic and data.
*
* @todo(himself65): rewrite all styled component into @vanilla-extract/css
*/
export const RootAppSidebar = ({
currentWorkspace,
currentPageId,
openPage,
createPage,
currentPath,
paths,
onOpenQuickSearchModal,
onOpenWorkspaceListModal,
}: RootAppSidebarProps): ReactElement => {
const currentWorkspaceId = currentWorkspace?.id || null;
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const { t } = useTranslation();
const [isScrollAtTop, setIsScrollAtTop] = useState(true);
const onClickNewPage = useCallback(async () => {
const page = await createPage();
openPage(page.id);
}, [createPage, openPage]);
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
useEffect(() => {
if (environment.isDesktop) {
window.apis?.onSidebarVisibilityChange(sidebarOpen);
}
}, [sidebarOpen]);
const ref = useRef<HTMLElement>(null);
return (
<>
<AppSidebar
ref={ref}
footer={
<StyledNewPageButton
data-testid="new-page-button"
onClick={onClickNewPage}
>
<PlusIcon /> {t('New Page')}
</StyledNewPageButton>
}
>
<StyledSliderBarInnerWrapper data-testid="sliderBar-inner">
<WorkspaceSelector
currentWorkspace={currentWorkspace}
onClick={onOpenWorkspaceListModal}
/>
<ChangeLog />
<StyledListItem
data-testid="slider-bar-quick-search-button"
onClick={useCallback(() => {
onOpenQuickSearchModal();
}, [onOpenQuickSearchModal])}
>
<SearchIcon />
{t('Quick search')}
</StyledListItem>
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.setting(currentWorkspaceId))
}
data-testid="slider-bar-workspace-setting-button"
style={{
marginBottom: '16px',
}}
>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.setting(currentWorkspaceId),
}}
>
<SettingsIcon />
<div>{t('Workspace Settings')}</div>
</StyledLink>
</StyledListItem>
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.all(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname: currentWorkspaceId && paths.all(currentWorkspaceId),
}}
>
<FolderIcon />
<span data-testid="all-pages">{t('All pages')}</span>
</StyledLink>
</StyledListItem>
<StyledScrollWrapper
showTopBorder={!isScrollAtTop}
onScroll={(e: UIEvent<HTMLDivElement>) => {
(e.target as HTMLDivElement).scrollTop === 0
? setIsScrollAtTop(true)
: setIsScrollAtTop(false);
}}
>
{blockSuiteWorkspace && (
<Favorite
currentPath={currentPath}
paths={paths}
currentPageId={currentPageId}
openPage={openPage}
currentWorkspace={currentWorkspace}
/>
)}
</StyledScrollWrapper>
<div style={{ height: 16 }}></div>
{config.enableLegacyCloud &&
(currentWorkspace?.flavour === WorkspaceFlavour.AFFINE &&
currentWorkspace.public ? (
<StyledListItem>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.setting(currentWorkspaceId),
}}
>
<ShareIcon />
<span data-testid="Published-to-web">Published to web</span>
</StyledLink>
</StyledListItem>
) : (
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.shared(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname:
currentWorkspaceId && paths.shared(currentWorkspaceId),
}}
>
<ShareIcon />
<span data-testid="shared-pages">{t('Shared Pages')}</span>
</StyledLink>
</StyledListItem>
))}
<StyledListItem
active={
currentPath ===
(currentWorkspaceId && paths.trash(currentWorkspaceId))
}
>
<StyledLink
href={{
pathname: currentWorkspaceId && paths.trash(currentWorkspaceId),
}}
>
<DeleteTemporarilyIcon /> {t('Trash')}
</StyledLink>
</StyledListItem>
</StyledSliderBarInnerWrapper>
</AppSidebar>
<ResizeIndicator targetElement={ref} />
</>
);
};