milestone: publish alpha version (#637)

- document folder
- full-text search
- blob storage
- basic edgeless support

Co-authored-by: tzhangchi <terry.zhangchi@outlook.com>
Co-authored-by: QiShaoXuan <qishaoxuan777@gmail.com>
Co-authored-by: DiamondThree <diamond.shx@gmail.com>
Co-authored-by: MingLiang Wang <mingliangwang0o0@gmail.com>
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
Co-authored-by: Yifeng Wang <doodlewind@toeverything.info>
Co-authored-by: Himself65 <himself65@outlook.com>
Co-authored-by: lawvs <18554747+lawvs@users.noreply.github.com>
Co-authored-by: Qi <474021214@qq.com>
This commit is contained in:
DarkSky
2022-12-30 21:40:15 +08:00
committed by GitHub
parent cc790dcbc2
commit 6c2c7dcd48
296 changed files with 16139 additions and 2072 deletions
@@ -0,0 +1,184 @@
import { InformationIcon, LogOutIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import { Divider } from '@/ui/divider';
import { useAppState } from '@/providers/app-state-provider/context';
import { SelectorPopperContainer } from './styles';
import {
PrivateWorkspaceItem,
WorkspaceItem,
CreateWorkspaceItem,
ListItem,
LoginItem,
} from './WorkspaceItem';
import { WorkspaceSetting } from '@/components/workspace-setting';
import { useCallback, useEffect, useState } from 'react';
import { getWorkspaceDetail, WorkspaceType } from '@affine/data-services';
import { useModal } from '@/providers/global-modal-provider';
export type WorkspaceDetails = Record<
string,
{ memberCount: number; owner: { id: string; name: string } }
>;
type SelectorPopperContentProps = {
isShow: boolean;
};
export const SelectorPopperContent = ({
isShow,
}: SelectorPopperContentProps) => {
const { user, workspacesMeta, workspaces, refreshWorkspacesMeta } =
useAppState();
const [settingWorkspaceId, setSettingWorkspaceId] = useState<string | null>(
null
);
const [workSpaceDetails, setWorkSpaceDetails] = useState<WorkspaceDetails>(
{}
);
const { triggerContactModal } = useModal();
const handleClickSettingWorkspace = (workspaceId: string) => {
setSettingWorkspaceId(workspaceId);
};
const handleCloseWorkSpace = () => {
setSettingWorkspaceId(null);
};
const settingWorkspace = settingWorkspaceId
? workspacesMeta.find(workspace => workspace.id === settingWorkspaceId)
: undefined;
const refreshDetails = useCallback(async () => {
const workspaceDetailList = await Promise.all(
workspacesMeta.map(async ({ id, type }) => {
if (user) {
if (type === WorkspaceType.Private) {
return { id, member_count: 1, owner: user };
} else {
const data = await getWorkspaceDetail({ id });
return { id, ...data } || { id, member_count: 0, owner: user };
}
}
})
);
const workSpaceDetails: WorkspaceDetails = {};
workspaceDetailList.forEach(details => {
if (details) {
const { id, member_count, owner } = details;
if (!owner) return;
workSpaceDetails[id] = {
memberCount: member_count || 1,
owner: {
id: owner.id,
name: owner.name,
},
};
}
});
setWorkSpaceDetails(workSpaceDetails);
}, [user, workspacesMeta]);
useEffect(() => {
if (isShow) {
setSettingWorkspaceId(null);
refreshWorkspacesMeta();
refreshDetails();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isShow]);
return !user ? (
<SelectorPopperContainer placement="bottom-start">
<LoginItem />
<StyledDivider />
<ListItem
icon={<InformationIcon />}
name="About AFFiNE"
onClick={() => triggerContactModal()}
/>
</SelectorPopperContainer>
) : (
<SelectorPopperContainer placement="bottom-start">
<PrivateWorkspaceItem
privateWorkspaceId={
workspacesMeta.find(
workspace => workspace.type === WorkspaceType.Private
)?.id
}
/>
<StyledDivider />
<WorkspaceGroupTitle>Workspace</WorkspaceGroupTitle>
<WorkspaceWrapper>
{workspacesMeta.map(workspace => {
return workspace.type !== WorkspaceType.Private ? (
<WorkspaceItem
type={workspace.type}
key={workspace.id}
id={workspace.id}
icon={
(workspaces[workspace.id]?.meta.avatar &&
`/api/blob/${workspaces[workspace.id]?.meta.avatar}`) ||
`loading...`
}
onClickSetting={handleClickSettingWorkspace}
name={workspaces[workspace.id]?.meta.name || `loading...`}
memberCount={workSpaceDetails[workspace.id]?.memberCount || 1}
/>
) : null;
})}
</WorkspaceWrapper>
<CreateWorkspaceItem />
{settingWorkspace ? (
<WorkspaceSetting
isShow={Boolean(settingWorkspaceId)}
onClose={handleCloseWorkSpace}
workspace={settingWorkspace}
owner={
(settingWorkspaceId &&
workSpaceDetails[settingWorkspaceId]?.owner) || {
id: user.id,
name: user.name,
}
}
/>
) : null}
<StyledDivider />
<ListItem
icon={<InformationIcon />}
name="About AFFiNE"
onClick={() => triggerContactModal()}
/>
<ListItem
icon={<LogOutIcon />}
name="Sign out"
onClick={() => {
console.log('Sign out');
// FIXME: remove token from local storage and reload the page
localStorage.removeItem('affine_token');
window.location.reload();
}}
/>
</SelectorPopperContainer>
);
};
const StyledDivider = styled(Divider)({
margin: '8px 12px',
width: 'calc(100% - 24px)',
});
const WorkspaceGroupTitle = styled('div')(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
lineHeight: '30px',
height: '30px',
padding: '0 12px',
};
});
const WorkspaceWrapper = styled('div')(() => {
return {
maxHeight: '200px',
overflow: 'auto',
};
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { AddIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import {
WorkspaceItemAvatar,
WorkspaceItemWrapper,
WorkspaceItemContent,
} from '../styles';
import { WorkspaceCreate } from './workspace-create';
const name = 'Create new Workspace';
export const CreateWorkspaceItem = () => {
const [open, setOpen] = useState(false);
return (
<>
<WorkspaceItemWrapper onClick={() => setOpen(true)}>
<WorkspaceItemAvatar>
<AddIcon />
</WorkspaceItemAvatar>
<WorkspaceItemContent>
<Name title={name}>{name}</Name>
</WorkspaceItemContent>
</WorkspaceItemWrapper>
<WorkspaceCreate open={open} onClose={() => setOpen(false)} />
</>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.base,
fontWeight: 400,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
@@ -0,0 +1 @@
export * from './CreateWorkspaceItem';
@@ -0,0 +1,63 @@
import { styled } from '@/styles';
import { Button } from '@/ui/button';
export const StyledModalWrapper = styled('div')(({ theme }) => {
return {
position: 'relative',
padding: '0px',
width: '460px',
background: theme.colors.popoverBackground,
borderRadius: '12px',
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
margin: '44px 0px 12px 0px',
width: '460px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
color: theme.colors.popoverColor,
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'center',
};
});
export const StyledInputContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '40px 0 24px 0',
};
});
export const StyledButtonContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '0px 0 32px 0',
};
});
export const StyledButton = styled(Button)(() => {
return {
width: '260px',
justifyContent: 'center',
};
});
@@ -0,0 +1,118 @@
import { createWorkspace, uploadBlob } from '@affine/data-services';
import Modal from '@/ui/modal';
import Input from '@/ui/input';
import {
StyledModalHeader,
StyledTextContent,
StyledModalWrapper,
StyledInputContent,
StyledButtonContent,
StyledButton,
} from './style';
import { useState } from 'react';
import { ModalCloseButton } from '@/ui/modal';
import router from 'next/router';
import { useAppState } from '@/providers/app-state-provider';
interface WorkspaceCreateProps {
open: boolean;
onClose: () => void;
}
const DefaultHeadImgColors = [
['#C6F2F3', '#0C6066'],
['#FFF5AB', '#896406'],
['#FFCCA7', '#8F4500'],
['#FFCECE', '#AF1212'],
['#E3DEFF', '#511AAB'],
];
export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => {
const [workspaceName, setWorkspaceId] = useState<string>('');
const [creating, setCreating] = useState<boolean>(false);
const { refreshWorkspacesMeta } = useAppState();
const handlerInputChange = (workspaceName: string) => {
setWorkspaceId(workspaceName);
};
const createDefaultHeadImg = (workspaceName: string) => {
const canvas = document.createElement('canvas');
canvas.height = 100;
canvas.width = 100;
const ctx = canvas.getContext('2d');
return new Promise<string>((resolve, reject) => {
if (ctx) {
const randomNumber = Math.floor(Math.random() * 5);
const randomColor = DefaultHeadImgColors[randomNumber];
ctx.fillStyle = randomColor[0];
ctx.fillRect(0, 0, 100, 100);
ctx.font = "600 50px 'PingFang SC', 'Microsoft Yahei'";
ctx.fillStyle = randomColor[1];
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(workspaceName[0], 50, 50);
canvas.toBlob(blob => {
if (blob) {
const blobId = uploadBlob({ blob });
resolve(blobId);
} else {
reject();
}
}, 'image/png');
} else {
reject();
}
});
};
const handleCreateWorkspace = async () => {
setCreating(true);
const blobId = await createDefaultHeadImg(workspaceName).catch(() => {
setCreating(false);
});
if (blobId) {
createWorkspace({ name: workspaceName, avatar: blobId })
.then(async data => {
await refreshWorkspacesMeta();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
router.push(`/workspace/${data.id}`);
onClose();
})
.catch(err => {
console.log(err, 'err');
})
.finally(() => {
setCreating(false);
});
}
};
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>Create new Workspace</StyledModalHeader>
<StyledTextContent>
Workspaces are shared environments where teams can collaborate. After
creating a Workspace, you can invite others to join.
</StyledTextContent>
<StyledInputContent>
<Input
onChange={handlerInputChange}
placeholder="Set a Workspace name"
value={workspaceName}
></Input>
</StyledInputContent>
<StyledButtonContent>
<StyledButton
disabled={!workspaceName.length || creating}
onClick={handleCreateWorkspace}
loading={creating}
>
Create
</StyledButton>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};
export default WorkspaceCreate;
@@ -0,0 +1,37 @@
import type { ReactNode } from 'react';
import { styled } from '@/styles';
import { WorkspaceItemWrapper, WorkspaceItemContent } from './styles';
interface ListItemProps {
name: string;
icon: ReactNode;
onClick: () => void;
}
export const ListItem = ({ name, icon, onClick }: ListItemProps) => {
return (
<WorkspaceItemWrapper onClick={onClick}>
<StyledIconWrapper>{icon}</StyledIconWrapper>
<WorkspaceItemContent>
<Name title={name}>{name}</Name>
</WorkspaceItemContent>
</WorkspaceItemWrapper>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.sm,
fontWeight: 400,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
const StyledIconWrapper = styled('div')({
width: '20px',
height: '20px',
fontSize: '20px',
});
@@ -0,0 +1,52 @@
import { useModal } from '@/providers/global-modal-provider';
import { styled } from '@/styles';
import { AffineIcon } from '../../icons/icons';
import {
WorkspaceItemAvatar,
LoginItemWrapper,
WorkspaceItemContent,
} from './styles';
export const LoginItem = () => {
const { triggerLoginModal } = useModal();
return (
<LoginItemWrapper
onClick={() => triggerLoginModal()}
data-testid="open-login-modal"
>
<WorkspaceItemAvatar alt="AFFiNE" src={''}>
<AffineIcon />
</WorkspaceItemAvatar>
<WorkspaceItemContent>
<Name title="AFFiNE">AFFiNE</Name>
<Description
title="Log in to sync with affine"
className="login-description"
>
Log in to sync with affine
</Description>
</WorkspaceItemContent>
</LoginItemWrapper>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.base,
fontWeight: 500,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
const Description = styled('div')(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
@@ -0,0 +1,60 @@
import { styled } from '@/styles';
import { useAppState } from '@/providers/app-state-provider/context';
import {
WorkspaceItemAvatar,
PrivateWorkspaceWrapper,
WorkspaceItemContent,
} from './styles';
import { useRouter } from 'next/router';
type PrivateWorkspaceItemProps = {
privateWorkspaceId?: string;
};
export const PrivateWorkspaceItem = ({
privateWorkspaceId,
}: PrivateWorkspaceItemProps) => {
const { user } = useAppState();
const router = useRouter();
const handleClick = () => {
if (privateWorkspaceId) {
router.push(`/workspace/${privateWorkspaceId}`);
}
};
if (user) {
const Username = user.name;
return (
<PrivateWorkspaceWrapper onClick={handleClick}>
<WorkspaceItemAvatar alt={Username} src={user.avatar_url}>
{Username}
</WorkspaceItemAvatar>
<WorkspaceItemContent>
<Name title={Username}>{Username}</Name>
<Email title={user.email}>{user.email}</Email>
</WorkspaceItemContent>
</PrivateWorkspaceWrapper>
);
}
return null;
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.base,
fontWeight: 500,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
const Email = styled('div')(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
@@ -0,0 +1,32 @@
import { SettingsIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import { IconButton } from '@/ui/button';
import { MouseEventHandler } from 'react';
type SettingProps = {
onClick?: () => void;
};
export const FooterSetting = ({ onClick }: SettingProps) => {
const handleClick: MouseEventHandler<HTMLButtonElement> = e => {
e.stopPropagation();
onClick && onClick();
};
return (
<Wrapper
className="footer-setting"
onClick={e => {
e.stopPropagation();
handleClick(e);
}}
>
<SettingsIcon />
</Wrapper>
);
};
const Wrapper = styled(IconButton)(() => {
return {
fontSize: '20px',
};
});
@@ -0,0 +1,29 @@
import { UsersIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import { IconButton } from '@/ui/button';
type FooterUsersProps = {
memberCount: number;
};
export const FooterUsers = ({ memberCount = 1 }: FooterUsersProps) => {
return (
<Wrapper className="footer-users">
<>
<UsersIcon />
<Tip>{memberCount > 99 ? '99+' : memberCount}</Tip>
</>
</Wrapper>
);
};
const Wrapper = styled(IconButton)({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '16px',
});
const Tip = styled('span')({
fontSize: '12px',
});
@@ -0,0 +1,96 @@
import { useRouter } from 'next/router';
import { styled } from '@/styles';
import {
WorkspaceItemAvatar,
WorkspaceItemWrapper,
WorkspaceItemContent,
} from '../styles';
import { FooterSetting } from './FooterSetting';
import { FooterUsers } from './FooterUsers';
import { WorkspaceType } from '@affine/data-services';
import { useAppState } from '@/providers/app-state-provider';
interface WorkspaceItemProps {
id: string;
name: string;
icon: string;
type: WorkspaceType;
memberCount: number;
onClickSetting?: (workspaceId: string) => void;
}
export const WorkspaceItem = ({
id,
name,
icon,
type,
onClickSetting,
memberCount,
}: WorkspaceItemProps) => {
const router = useRouter();
const { currentWorkspaceId } = useAppState();
const handleClickSetting = async () => {
onClickSetting && onClickSetting(id);
};
return (
<StyledWrapper
onClick={() => {
router.push(`/workspace/${id}`);
}}
canSet={
type !== WorkspaceType.Private && currentWorkspaceId === String(id)
}
>
<WorkspaceItemAvatar alt={name} src={icon}>
{name.charAt(0)}
</WorkspaceItemAvatar>
<WorkspaceItemContent>
<Name title={name}>{name}</Name>
</WorkspaceItemContent>
<Footer>
<FooterUsers memberCount={memberCount} />
<FooterSetting onClick={handleClickSetting} />
</Footer>
</StyledWrapper>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.sm,
fontWeight: 400,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
const StyledWrapper = styled(WorkspaceItemWrapper)<{ canSet: boolean }>(
({ canSet }) => {
return {
'& .footer-setting': {
display: 'none',
},
':hover .footer-users': {
display: canSet ? 'none' : '',
},
':hover .footer-setting': {
display: canSet ? 'block' : 'none',
},
};
}
);
const Footer = styled('div')({
width: '42px',
flex: '0 42px',
fontSize: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginLeft: '12px',
});
@@ -0,0 +1 @@
export * from './WorkspaceItem';
@@ -0,0 +1,5 @@
export * from './PrivateWorkspaceItem';
export * from './WorkspaceItem';
export * from './CreateWorkspaceItem';
export * from './ListItem';
export * from './LoginItem';
@@ -0,0 +1,41 @@
import MuiAvatar from '@mui/material/Avatar';
import { styled } from '@/styles';
export const WorkspaceItemWrapper = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
cursor: 'pointer',
borderRadius: '5px',
padding: '6px 12px',
':hover': {
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
},
}));
export const PrivateWorkspaceWrapper = styled(WorkspaceItemWrapper)({
padding: '10px 12px',
});
export const LoginItemWrapper = styled(WorkspaceItemWrapper)(({ theme }) => {
return {
padding: '10px 12px',
':hover .login-description': {
color: theme.colors.primaryColor,
},
};
});
export const WorkspaceItemAvatar = styled(MuiAvatar)({
height: '40px',
width: '40px',
});
export const WorkspaceItemContent = styled('div')({
minWidth: 0,
marginLeft: '12px',
flexGrow: 1,
});
@@ -0,0 +1,53 @@
import { Popper } from '@/ui/popper';
import { Avatar, WorkspaceName, SelectorWrapper } from './styles';
import { SelectorPopperContent } from './SelectorPopperContent';
import { useState } from 'react';
import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceType } from '@affine/data-services';
import { AffineIcon } from '../icons/icons';
export const WorkspaceSelector = () => {
const [isShow, setIsShow] = useState(false);
const { currentWorkspace, workspacesMeta, currentWorkspaceId, user } =
useAppState();
const workspaceMeta = workspacesMeta.find(
meta => String(meta.id) === String(currentWorkspaceId)
);
return (
<Popper
content={<SelectorPopperContent isShow={isShow} />}
zIndex={1000}
placement="bottom-start"
trigger="click"
onVisibleChange={setIsShow}
>
<SelectorWrapper data-testid="current-workspace">
<Avatar
alt="Affine"
data-testid="workspace-avatar"
src={
workspaceMeta?.type === WorkspaceType.Private
? user
? user.avatar_url
: ''
: currentWorkspace?.meta.avatar &&
`/api/blob/${currentWorkspace?.meta.avatar}`
}
>
{workspaceMeta?.type === WorkspaceType.Private && user ? (
user?.name[0]
) : (
<AffineIcon />
)}
</Avatar>
<WorkspaceName data-testid="workspace-name">
{workspaceMeta?.type === WorkspaceType.Private
? user
? user.name
: 'AFFiNE'
: currentWorkspace?.meta.name || 'AFFiNE'}
</WorkspaceName>
</SelectorWrapper>
</Popper>
);
};
@@ -0,0 +1 @@
export * from './WorkspaceSelector';
@@ -0,0 +1,41 @@
import MuiAvatar from '@mui/material/Avatar';
import { styled } from '@/styles';
import { StyledPopperContainer } from '@/ui/shared/Container';
export const SelectorWrapper = styled('div')({
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
':hover': {
cursor: 'pointer',
},
});
export const Avatar = styled(MuiAvatar)({
height: '28px',
width: '28px',
});
export const WorkspaceName = styled('span')(({ theme }) => {
return {
marginLeft: '12px',
lineHeight: 1,
fontSize: '18px',
fontWeight: 500,
color: theme.colors.iconColor,
};
});
export const SelectorPopperContainer = styled(StyledPopperContainer)(
({ theme }) => {
return {
width: '334px',
boxShadow: theme.shadow.tooltip,
padding: '24px 12px',
backgroundColor: theme.colors.pageBackground,
fontSize: theme.font.xs,
};
}
);
@@ -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
fill-rule="evenodd"
clip-rule="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,188 @@
import React, { useState } from 'react';
import { useRouter } from 'next/router';
import {
StyledArrowButton,
StyledLink,
StyledListItem,
// StyledListItemForWorkspace,
StyledNewPageButton,
StyledQuickSearch,
StyledSliderBar,
StyledSliderBarWrapper,
StyledSubListItem,
} from './style';
import { Arrow } from './icons';
import Collapse from '@mui/material/Collapse';
import {
ArrowDownIcon,
SearchIcon,
AllPagesIcon,
FavouritesIcon,
ImportIcon,
TrashIcon,
AddIcon,
} from '@blocksuite/icons';
import Link from 'next/link';
import { Tooltip } from '@/ui/tooltip';
import { useModal } from '@/providers/global-modal-provider';
import { useAppState } from '@/providers/app-state-provider/context';
import { IconButton } from '@/ui/button';
// import { WorkspaceSelector } from './WorkspaceSelector';
import useLocalStorage from '@/hooks/use-local-storage';
import usePageMetaList from '@/hooks/use-page-meta-list';
import { usePageHelper } from '@/hooks/use-page-helper';
import { getUaHelper } from '@/utils';
const isMac = () => {
return getUaHelper().isMacOs;
};
const FavoriteList = ({ showList }: { showList: boolean }) => {
const { openPage } = usePageHelper();
const pageList = usePageMetaList();
const router = useRouter();
const favoriteList = pageList.filter(p => p.favorite && !p.trash);
return (
<Collapse 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}>No item</StyledSubListItem>
)}
</Collapse>
);
};
export const WorkSpaceSliderBar = () => {
const { triggerQuickSearchModal } = useModal();
const [showSubFavorite, setShowSubFavorite] = useState(true);
const { currentWorkspaceId } = useAppState();
const { openPage, createPage } = usePageHelper();
const router = useRouter();
const [showTip, setShowTip] = useState(false);
const [show, setShow] = useLocalStorage('AFFiNE_SLIDE_BAR', false, true);
const paths = {
all: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/all` : '',
favorite: currentWorkspaceId
? `/workspace/${currentWorkspaceId}/favorite`
: '',
trash: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/trash` : '',
};
return (
<>
<StyledSliderBar show={show}>
<Tooltip
content={show ? 'Collapse sidebar' : 'Expand sidebar'}
placement="right"
visible={showTip}
>
<StyledArrowButton
data-testid="sliderBar-arrowButton"
isShow={show}
onClick={() => {
setShow(!show);
setShowTip(false);
}}
onMouseEnter={() => {
setShowTip(true);
}}
onMouseLeave={() => {
setShowTip(false);
}}
>
<Arrow />
</StyledArrowButton>
</Tooltip>
<StyledSliderBarWrapper data-testid="sliderBar">
{/* <StyledListItemForWorkspace>
<WorkspaceSelector />
</StyledListItemForWorkspace> */}
<StyledQuickSearch
data-testid="sliderBar-quickSearchButton"
style={{ cursor: 'pointer' }}
onClick={() => {
triggerQuickSearchModal();
}}
>
<SearchIcon />
Quick search
<span>{isMac() ? '⌘ + K' : 'Ctrl + K'}</span>
</StyledQuickSearch>
<Link href={{ pathname: paths.all }}>
<StyledListItem active={router.asPath === paths.all}>
<AllPagesIcon /> <span>All pages</span>
</StyledListItem>
</Link>
<StyledListItem active={router.asPath === paths.favorite}>
<StyledLink href={{ pathname: paths.favorite }}>
<FavouritesIcon />
Favourites
</StyledLink>
<IconButton
darker={true}
onClick={() => {
setShowSubFavorite(!showSubFavorite);
}}
>
<ArrowDownIcon
style={{
transform: `rotate(${showSubFavorite ? '180' : '0'}deg)`,
}}
/>
</IconButton>
</StyledListItem>
<FavoriteList showList={showSubFavorite} />
<Tooltip content="Coming soon" placement="right-start" zIndex={9999}>
<StyledListItem
disabled={true}
onClick={() => {
// triggerImportModal();
}}
>
<ImportIcon /> Import
</StyledListItem>
</Tooltip>
<Link href={{ pathname: paths.trash }}>
<StyledListItem active={router.asPath === paths.trash}>
<TrashIcon /> Trash
</StyledListItem>
</Link>
<StyledNewPageButton
onClick={async () => {
const pageId = await createPage();
if (pageId) {
openPage(pageId);
}
}}
>
<AddIcon /> New Page
</StyledNewPageButton>
</StyledSliderBarWrapper>
</StyledSliderBar>
</>
);
};
export default WorkSpaceSliderBar;
@@ -0,0 +1,174 @@
import { displayFlex, styled, textEllipsis } from '@/styles';
import Link from 'next/link';
export const StyledSliderBar = styled.div<{ show: boolean }>(
({ theme, show }) => {
return {
width: show ? '320px' : '0',
height: '100vh',
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
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',
};
});
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 {
width: '296px',
height: '32px',
marginTop: '12px',
fontSize: theme.font.sm,
color: active ? theme.colors.primaryColor : theme.colors.popoverColor,
paddingLeft: '12px',
borderRadius: '5px',
...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 StyledListItemForWorkspace = styled(StyledListItem)({
height: '52px',
});
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: '296px',
height: '32px',
marginTop: '4px',
fontSize: theme.font.sm,
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',
lineHeight: '32px',
textAlign: 'start',
...textEllipsis(1),
':hover': disable
? {}
: {
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
},
};
});
export const StyledQuickSearch = styled.div(({ theme }) => {
return {
width: '296px',
height: '32px',
marginTop: '12px',
fontSize: theme.font.sm,
backgroundColor: theme.colors.hoverBackground,
color: theme.colors.popoverColor,
paddingLeft: '12px',
borderRadius: '5px',
...displayFlex('flex-start', 'center'),
'>svg': {
fontSize: '20px',
marginRight: '12px',
},
'>span': {
fontSize: theme.font.xs,
margin: 'auto',
marginRight: '12px',
color: theme.colors.hoverBackground,
transition: 'all .15s',
},
':hover': {
color: theme.colors.popoverColor,
'>span': {
color: theme.colors.popoverColor,
},
},
};
});