Merge branch 'master'

Conflicts:
	.vscode/settings.json
	package.json
	packages/data-center/package.json
	pnpm-lock.yaml
This commit is contained in:
linonetwo
2023-02-09 10:13:15 +08:00
164 changed files with 3027 additions and 5273 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ export const NotfoundPage = () => {
const router = useRouter();
return (
<PageContainer>
<NotFoundTitle>
<NotFoundTitle data-testid="notFound">
{t('404 - Page Not Found')}
<p>
<Button
@@ -15,7 +15,7 @@ export const NotfoundPage = () => {
router.push('/workspace');
}}
>
Back Home
{t('Back Home')}
</Button>
</p>
</NotFoundTitle>
@@ -91,8 +91,6 @@ export const ContactModal = ({
<span>Alpha</span>
</StyledModalHeaderLeft>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
@@ -53,13 +53,12 @@ export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => {
</Header>
<Content>
<ContentTitle>{t('New Workspace')}</ContentTitle>
<p>
Workspace is your virtual space to capture, create and plan as
just one person or together as a team.
</p>
<p>{t('Workspace description')}</p>
<Input
onKeyDown={handleKeyDown}
placeholder={'Set a Workspace name'}
placeholder={t('Set a Workspace name')}
maxLength={15}
minLength={0}
onChange={value => {
setWorkspaceName(value);
}}
@@ -29,8 +29,6 @@ export const DeleteModal = ({
<ModalWrapper width={620} height={334}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
@@ -14,8 +14,8 @@ import {
UndoIcon,
RedoIcon,
} from './Icons';
import { MuiSlide } from '@/ui/mui';
import { Tooltip } from '@/ui/tooltip';
import Slide from '@mui/material/Slide';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { useAppState } from '@/providers/app-state-provider';
import useHistoryUpdated from '@/hooks/use-history-update';
@@ -127,7 +127,7 @@ export const EdgelessToolbar = () => {
const { mode } = useCurrentPageMeta() || {};
return (
<Slide
<MuiSlide
direction="right"
in={mode === 'edgeless'}
mountOnEnter
@@ -155,7 +155,7 @@ export const EdgelessToolbar = () => {
</StyledToolbarWrapper>
<UndoRedo />
</StyledEdgelessToolbar>
</Slide>
</MuiSlide>
);
};
+10 -11
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react';
import type { Page, Workspace } from '@blocksuite/store';
import '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor';
import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-Downhill.md';
import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-Downhills.md';
import { styled } from '@/styles';
const StyledEditorContainer = styled('div')(() => {
@@ -39,22 +39,21 @@ export const Editor = ({ page, workspace, setEditor }: Props) => {
const title = metaTitle
? metaTitle
: isFirstPage
? 'Welcome to AFFiNE Alpha "Downhill"'
? 'Welcome to AFFiNE Alpha "Downhills"'
: '';
workspace?.setPageMeta(page.id, { title });
const pageId = page.addBlock({
flavour: 'affine:page',
title,
});
page.addBlock({ flavour: 'affine:surface' });
const frameId = page.addBlock({ flavour: 'affine:frame' }, pageId);
page.addBlock({ flavour: 'affine:frame' }, pageId);
const pageBlockId = page.addBlockByFlavour('affine:page', { title });
page.addBlockByFlavour('affine:surface', {}, null);
// Add frame block inside page block
const frameId = page.addBlockByFlavour('affine:frame', {}, pageBlockId);
// Add paragraph block inside frame block
// If this is a first page in workspace, init an introduction markdown
if (isFirstPage) {
editor.clipboard.importMarkdown(exampleMarkdown, `${frameId}`);
editor.clipboard.importMarkdown(exampleMarkdown, frameId);
workspace.setPageMeta(page.id, { title });
page.resetHistory();
} else {
page.addBlockByFlavour('affine:paragraph', {}, frameId);
}
page.resetHistory();
}
@@ -0,0 +1,111 @@
import { styled } from '@/styles';
import { Modal, ModalWrapper } from '@/ui/modal';
import { Button, IconButton } from '@/ui/button';
import { useTranslation } from '@affine/i18n';
import { useAppState } from '@/providers/app-state-provider';
import { useState } from 'react';
import router from 'next/router';
import { toast } from '@/ui/toast';
import { CloseIcon } from '@blocksuite/icons';
interface EnableWorkspaceModalProps {
open: boolean;
onClose: () => void;
}
export const EnableWorkspaceModal = ({
open,
onClose,
}: EnableWorkspaceModalProps) => {
const { t } = useTranslation();
const { user, dataCenter, login, currentWorkspace } = useAppState();
const [loading, setLoading] = useState(false);
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={560} height={292}>
<Header>
<IconButton
onClick={() => {
onClose();
}}
>
<CloseIcon />
</IconButton>
</Header>
<Content>
<ContentTitle>{t('Enable AFFiNE Cloud')}?</ContentTitle>
<StyleTips>{t('Enable AFFiNE Cloud Description')}</StyleTips>
{/* <StyleTips>{t('Retain local cached data')}</StyleTips> */}
<div>
<StyleButton
shape="round"
type="primary"
loading={loading}
onClick={async () => {
setLoading(true);
if (!user) {
await login();
}
if (currentWorkspace) {
const workspace = await dataCenter.enableWorkspaceCloud(
currentWorkspace
);
workspace &&
router.push(`/workspace/${workspace.id}/setting`);
toast(t('Enabled success'));
}
}}
>
{user ? t('Enable') : t('Sign in and Enable')}
</StyleButton>
<StyleButton
shape="round"
onClick={() => {
onClose();
}}
>
{t('Not now')}
</StyleButton>
</div>
</Content>
</ModalWrapper>
</Modal>
);
};
const Header = styled('div')({
height: '44px',
display: 'flex',
flexDirection: 'row-reverse',
paddingRight: '10px',
paddingTop: '10px',
});
const Content = styled('div')({
textAlign: 'center',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
});
const StyleTips = styled('div')(() => {
return {
userSelect: 'none',
width: '400px',
margin: 'auto',
marginBottom: '32px',
marginTop: '12px',
};
});
const StyleButton = styled(Button)(() => {
return {
width: '284px',
display: 'block',
margin: 'auto',
marginTop: '16px',
};
});
@@ -1,24 +1,28 @@
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { Button } from '@/ui/button';
import { useTranslation } from '@affine/i18n';
import { useState } from 'react';
import { EnableWorkspaceModal } from './EnableWorkspaceModal';
export const EnableWorkspaceButton = () => {
const { t } = useTranslation();
const { enableWorkspace } = useWorkspaceHelper();
const [loading, setLoading] = useState(false);
const [enableModalOpen, setEnableModalOpen] = useState(false);
return (
<Button
type="primary"
shape="circle"
loading={loading}
onClick={async () => {
setLoading(true);
await enableWorkspace();
setLoading(false);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
<>
<Button
type="light"
shape="circle"
onClick={async () => {
setEnableModalOpen(true);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
<EnableWorkspaceModal
open={enableModalOpen}
onClose={() => {
setEnableModalOpen(false);
}}
></EnableWorkspaceModal>
</>
);
};
@@ -6,7 +6,7 @@ import {
StyledBrowserWarning,
StyledCloseButton,
} from './styles';
import CloseIcon from '@mui/icons-material/Close';
import { CloseIcon } from '@blocksuite/icons';
import { useWarningMessage, shouldShowWarning } from './utils';
import EditorOptionMenu from './header-right-items/EditorOptionMenu';
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
@@ -1,5 +1,6 @@
import { PropsWithChildren, ReactNode } from 'react';
import Header from './Header';
import QuickSearchButton from './QuickSearchButton';
import { StyledPageListTittleWrapper } from './styles';
// import QuickSearchButton from './QuickSearchButton';
@@ -12,7 +13,7 @@ export const PageListHeader = ({ icon, children }: PageListHeaderProps) => {
<StyledPageListTittleWrapper>
{icon}
{children}
{/* <QuickSearchButton style={{ marginLeft: '5px' }} /> */}
<QuickSearchButton />
</StyledPageListTittleWrapper>
</Header>
);
@@ -1,15 +1,41 @@
import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons';
import { CloudUnsyncedIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/GlobalModalProvider';
import { useAppState } from '@/providers/app-state-provider';
import { IconButton } from '@/ui/button';
// Temporary solution to use this component, since the @blocksuite/icons has not been published yet
const DefaultSyncIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 13.6493C3 16.6044 5.41766 19 8.4 19L16.5 19C18.9853 19 21 16.9839 21 14.4969C21 12.6503 19.8893 10.9449 18.3 10.25C18.1317 7.32251 15.684 5 12.6893 5C10.3514 5 8.34694 6.48637 7.5 8.5C4.8 8.9375 3 11.2001 3 13.6493Z"
stroke="#888A9E"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.4571 9L9 16H10.4392L12.0021 11.1586L13.5657 16H15L12.5425 9H11.4571Z"
fill="#888A9E"
/>
</svg>
);
};
export const SyncUser = () => {
const { triggerLoginModal } = useModal();
const appState = useAppState();
return appState.user ? (
<IconButton iconSize="middle" disabled>
<CloudInsyncIcon />
<DefaultSyncIcon />
</IconButton>
) : (
<IconButton
+1 -1
View File
@@ -103,7 +103,7 @@ export const StyledSearchArrowWrapper = styled.div(() => {
export const StyledPageListTittleWrapper = styled(StyledTitle)(({ theme }) => {
return {
fontSize: theme.font.sm,
fontSize: theme.font.base,
color: theme.colors.textColor,
'>svg': {
fontSize: '20px',
@@ -6,7 +6,7 @@ import {
StyledTransformIcon,
} from './style';
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
import Grow from '@mui/material/Grow';
import { MuiGrow } from '@/ui/mui';
import { Tooltip } from '@/ui/tooltip';
import { useTranslation } from '@affine/i18n';
import { useModal } from '@/providers/GlobalModalProvider';
@@ -35,7 +35,7 @@ export const HelpIsland = ({
setShowContent(false);
}}
>
<Grow in={showContent}>
<MuiGrow in={showContent}>
<StyledIslandWrapper>
{showList.includes('contact') && (
<Tooltip content={t('Contact Us')} placement="left-end">
@@ -66,7 +66,7 @@ export const HelpIsland = ({
</Tooltip>
)}
</StyledIslandWrapper>
</Grow>
</MuiGrow>
<div style={{ position: 'relative' }}>
<StyledIconWrapper
+3 -3
View File
@@ -1,7 +1,7 @@
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { StyledButtonWrapper, StyledTitle } from './styles';
import { Button } from '@/ui/button';
import { Wrapper, Content } from '@/ui/layout';
import { Content, FlexWrapper } from '@/ui/layout';
import Loading from '@/components/loading';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useAppState } from '@/providers/app-state-provider';
@@ -116,7 +116,7 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => {
)}
{status === 'importing' && (
<Wrapper
<FlexWrapper
wrap={true}
justifyContent="center"
style={{ marginTop: 22, paddingBottom: '32px' }}
@@ -126,7 +126,7 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => {
OOOOPS! Sorry forgot to remind you that we are working on the
import function
</Content>
</Wrapper>
</FlexWrapper>
)}
</ModalWrapper>
</Modal>
@@ -0,0 +1,28 @@
export const GoogleIcon = () => {
return (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.327 16.3285 15.1115 18 12.5 18C9.1865 18 6.5 15.3135 6.5 12C6.5 8.6865 9.1865 6 12.5 6C14.0295 6 15.421 6.577 16.4805 7.5195L19.309 4.691C17.523 3.0265 15.134 2 12.5 2C6.9775 2 2.5 6.4775 2.5 12C2.5 17.5225 6.9775 22 12.5 22C18.0225 22 22.5 17.5225 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z"
fill="#FFC107"
/>
<path
d="M3.65234 7.3455L6.93784 9.755C7.82684 7.554 9.97984 6 12.4993 6C14.0288 6 15.4203 6.577 16.4798 7.5195L19.3083 4.691C17.5223 3.0265 15.1333 2 12.4993 2C8.65834 2 5.32734 4.1685 3.65234 7.3455Z"
fill="#FF3D00"
/>
<path
d="M12.5002 22.0003C15.0832 22.0003 17.4302 21.0118 19.2047 19.4043L16.1097 16.7853C15.0719 17.5745 13.8039 18.0014 12.5002 18.0003C9.89916 18.0003 7.69066 16.3418 6.85866 14.0273L3.59766 16.5398C5.25266 19.7783 8.61366 22.0003 12.5002 22.0003Z"
fill="#4CAF50"
/>
<path
d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.7571 15.1082 17.0467 16.0766 16.108 16.7855L16.1095 16.7845L19.2045 19.4035C18.9855 19.6025 22.5 17 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z"
fill="#1976D2"
/>
</svg>
);
};
@@ -1,40 +0,0 @@
import { CloudUnsyncedIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import GoogleSvg from './google.svg';
export const GoogleIcon = () => {
return (
<GoogleIconWrapper>
<picture>
<img src={GoogleSvg.src} alt="Google" />
</picture>
</GoogleIconWrapper>
);
};
const GoogleIconWrapper = styled('div')(({ theme }) => ({
background: theme.colors.pageBackground,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
export const StayLogOutIcon = () => {
return (
<StayLogOutWrapper>
<CloudUnsyncedIcon />
</StayLogOutWrapper>
);
};
const StayLogOutWrapper = styled('div')(({ theme }) => {
return {
width: '48px',
height: '48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
background: theme.colors.hoverBackground,
};
});
@@ -1,99 +0,0 @@
import { styled } from '@/styles';
import { Button } from '@/ui/button';
import { GoogleIcon, StayLogOutIcon } from './Icons';
import { useTranslation } from '@affine/i18n';
export const GoogleLoginButton = () => {
const { t } = useTranslation();
return (
<StyledGoogleButton>
<ButtonWrapper>
<IconWrapper>
<GoogleIcon />
</IconWrapper>
<TextWrapper>{t('Continue with Google')}</TextWrapper>
</ButtonWrapper>
</StyledGoogleButton>
);
};
export const StayLogOutButton = () => {
const { t } = useTranslation();
return (
<StyledStayLogOutButton>
<ButtonWrapper>
<IconWrapper>
<StayLogOutIcon />
</IconWrapper>
<TextWrapper>
<Title>{t('Stay logged out')}</Title>
<Description>{t('All changes are saved locally')}</Description>
</TextWrapper>
</ButtonWrapper>
</StyledStayLogOutButton>
);
};
const StyledGoogleButton = styled('div')(({ theme }) => {
return {
width: '284px',
height: '40px',
marginTop: '30px',
fontSize: '16px',
cursor: 'pointer',
borderRadius: '40px',
border: `1px solid ${theme.colors.iconColor}`,
overflow: 'hidden',
':hover': {
border: `1px solid ${theme.colors.primaryColor}`,
},
};
});
const StyledStayLogOutButton = styled(Button)(() => {
return {
width: '361px',
height: '56px',
padding: '4px',
':hover': {
borderColor: '#6880FF',
},
};
});
const ButtonWrapper = styled('div')({
display: 'flex',
flexDirection: 'row',
width: '100%',
});
const IconWrapper = styled('div')({
flex: '0 48px',
borderRadius: '5px',
overflow: 'hidden',
marginRight: '12px',
marginTop: '8px',
});
const TextWrapper = styled('div')({
flex: 1,
textAlign: 'left',
height: '40px',
lineHeight: '40px',
});
const Title = styled('h1')(() => {
return {
fontSize: '18px',
lineHeight: '26px',
fontWeight: 500,
};
});
const Description = styled('p')(() => {
return {
fontSize: '16px',
lineHeight: '22px',
fontWeight: 400,
};
});
@@ -1,6 +0,0 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.327 16.3285 15.1115 18 12.5 18C9.1865 18 6.5 15.3135 6.5 12C6.5 8.6865 9.1865 6 12.5 6C14.0295 6 15.421 6.577 16.4805 7.5195L19.309 4.691C17.523 3.0265 15.134 2 12.5 2C6.9775 2 2.5 6.4775 2.5 12C2.5 17.5225 6.9775 22 12.5 22C18.0225 22 22.5 17.5225 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z" fill="#FFC107"/>
<path d="M3.65234 7.3455L6.93784 9.755C7.82684 7.554 9.97984 6 12.4993 6C14.0288 6 15.4203 6.577 16.4798 7.5195L19.3083 4.691C17.5223 3.0265 15.1333 2 12.4993 2C8.65834 2 5.32734 4.1685 3.65234 7.3455Z" fill="#FF3D00"/>
<path d="M12.5002 22.0003C15.0832 22.0003 17.4302 21.0118 19.2047 19.4043L16.1097 16.7853C15.0719 17.5745 13.8039 18.0014 12.5002 18.0003C9.89916 18.0003 7.69066 16.3418 6.85866 14.0273L3.59766 16.5398C5.25266 19.7783 8.61366 22.0003 12.5002 22.0003Z" fill="#4CAF50"/>
<path d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.7571 15.1082 17.0467 16.0766 16.108 16.7855L16.1095 16.7845L19.2045 19.4035C18.9855 19.6025 22.5 17 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z" fill="#1976D2"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,7 +1,9 @@
import { styled } from '@/styles';
import { positionAbsolute, styled } from '@/styles';
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { GoogleLoginButton } from './LoginOptionButton';
import { Button } from '@/ui/button';
import { useAppState } from '@/providers/app-state-provider';
import { useTranslation } from '@affine/i18n';
import { GoogleIcon } from './GoogleIcon';
interface LoginModalProps {
open: boolean;
onClose: () => void;
@@ -9,38 +11,44 @@ interface LoginModalProps {
export const LoginModal = ({ open, onClose }: LoginModalProps) => {
const { login } = useAppState();
const { t } = useTranslation();
return (
<Modal open={open} onClose={onClose} data-testid="login-modal">
<ModalWrapper width={560} height={292}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
/>
</Header>
<ModalWrapper width={560} height={292} style={{ paddingTop: '44px' }}>
<ModalCloseButton
onClick={() => {
onClose();
}}
/>
<Content>
<ContentTitle>{'Sign in'}</ContentTitle>
<SignDes>Set up an AFFINE account to sync data</SignDes>
<span
<ContentTitle>{t('Sign in')}</ContentTitle>
<SignDes>{t('Set up an AFFiNE account to sync data')}</SignDes>
<StyledLoginButton
shape="round"
onClick={async () => {
await login();
onClose();
}}
>
<GoogleLoginButton />
</span>
<GoogleIcon />
{t('Continue with Google')}
</StyledLoginButton>
</Content>
</ModalWrapper>
</Modal>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
const StyledLoginButton = styled(Button)(() => {
return {
width: '284px',
marginTop: '30px',
position: 'relative',
svg: {
...positionAbsolute({ left: '18px', top: '0', bottom: '0' }),
margin: 'auto',
},
};
});
const Content = styled('div')({
@@ -8,7 +8,7 @@ export const Check = () => {
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_9266_16831)">
<g clipPath="url(#clip0_9266_16831)">
<path
fillRule="evenodd"
clipRule="evenodd"
@@ -3,28 +3,34 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import { Check, UnCheck } from './icon';
import { useState } from 'react';
import { useTranslation } from '@affine/i18n';
import { useAppState } from '@/providers/app-state-provider';
interface LoginModalProps {
open: boolean;
onClose: (wait: boolean) => void;
}
export const LogoutModal = ({ open, onClose }: LoginModalProps) => {
const [localCache, setLocalCache] = useState(false);
const [localCache, setLocalCache] = useState(true);
const { blobDataSynced } = useAppState();
const { t } = useTranslation();
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={560} height={292}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose(true);
}}
/>
</Header>
<Content>
<ContentTitle>{'Sign out?'}</ContentTitle>
<SignDes>Set up an AFFINE account to sync data</SignDes>
<ContentTitle>{t('Sign out')}?</ContentTitle>
<SignDes>
{blobDataSynced
? t('Set up an AFFiNE account to sync data')
: 'All data has been stored in the cloud'}
</SignDes>
<StyleTips>
{localCache ? (
<StyleCheck
@@ -43,28 +49,51 @@ export const LogoutModal = ({ open, onClose }: LoginModalProps) => {
<UnCheck></UnCheck>
</StyleCheck>
)}
Retain local cached data
{t('Retain local cached data')}
</StyleTips>
<div>
<Button
style={{ marginRight: '16px' }}
shape="round"
onClick={() => {
onClose(true);
}}
>
Wait for Sync
</Button>
<Button
type="danger"
shape="round"
onClick={() => {
onClose(false);
}}
>
Force Sign Out
</Button>
</div>
{blobDataSynced ? (
<div>
<Button
type="danger"
shape="round"
style={{ marginRight: '16px' }}
onClick={async () => {
onClose(false);
}}
>
{t('Force Sign Out')}
</Button>
<Button
shape="round"
onClick={() => {
onClose(true);
}}
>
{t('Wait for Sync')}
</Button>
</div>
) : (
<div>
<Button
type="primary"
style={{ marginRight: '16px' }}
shape="round"
onClick={() => {
onClose(true);
}}
>
{t('Cancel')}
</Button>
<Button
shape="round"
onClick={() => {
onClose(false);
}}
>
{t('Sign out')}
</Button>
</div>
)}
</Content>
</ModalWrapper>
</Modal>
@@ -14,7 +14,6 @@ export const PageListEmpty = (props: { listType?: string }) => {
{listType === 'all' && <p>{t('emptyAllPages')}</p>}
{listType === 'favorite' && <p>{t('emptyFavourite')}</p>}
{listType === 'trash' && <p>{t('emptyTrash')}</p>}
<p>{t('still designed')}</p>
</div>
);
};
@@ -1,7 +1,7 @@
import { useConfirm } from '@/providers/ConfirmProvider';
import { PageMeta } from '@/providers/app-state-provider';
import { Menu, MenuItem } from '@/ui/menu';
import { Wrapper } from '@/ui/layout';
import { FlexWrapper } from '@/ui/layout';
import { IconButton } from '@/ui/button';
import {
MoreVerticalIcon,
@@ -63,13 +63,13 @@ export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
</>
);
return (
<Wrapper alignItems="center" justifyContent="center">
<FlexWrapper alignItems="center" justifyContent="center">
<Menu content={OperationMenu} placement="bottom-end" disablePortal={true}>
<IconButton darker={true}>
<MoreVerticalIcon />
</IconButton>
</Menu>
</Wrapper>
</FlexWrapper>
);
};
@@ -80,7 +80,7 @@ export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
const { confirm } = useConfirm();
const { t } = useTranslation();
return (
<Wrapper>
<FlexWrapper>
<IconButton
darker={true}
style={{ marginRight: '12px' }}
@@ -108,6 +108,6 @@ export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
>
<DeleteIcon />
</IconButton>
</Wrapper>
</FlexWrapper>
);
};
@@ -1,26 +1,23 @@
import React from 'react';
import { AddIcon } from '@blocksuite/icons';
import { StyledModalFooterContent } from './style';
import { useModal } from '@/providers/GlobalModalProvider';
import { Command } from 'cmdk';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from '@affine/i18n';
export const Footer = (props: { query: string }) => {
const { triggerQuickSearchModal } = useModal();
export const Footer = (props: { query: string; onClose: () => void }) => {
const { openPage, createPage } = usePageHelper();
const { t } = useTranslation();
const query = props.query;
const { query, onClose } = props;
return (
<Command.Item
data-testid="quickSearch-addNewPage"
onSelect={async () => {
onClose();
const pageId = await createPage({ title: query });
if (pageId) {
openPage(pageId);
}
triggerQuickSearchModal();
}}
>
<StyledModalFooterContent>
@@ -8,32 +8,41 @@ import React, {
import { SearchIcon } from '@blocksuite/icons';
import { StyledInputContent, StyledLabel } from './style';
import { Command } from 'cmdk';
import { useAppState } from '@/providers/app-state-provider';
import { useTranslation } from '@affine/i18n';
export const Input = (props: {
open: boolean;
query: string;
setQuery: Dispatch<SetStateAction<string>>;
setLoading: Dispatch<SetStateAction<boolean>>;
isPublic: boolean;
publishWorkspaceName: string | undefined;
}) => {
const { open, query, setQuery, setLoading, isPublic, publishWorkspaceName } =
props;
const [isComposition, setIsComposition] = useState(false);
const [inputValue, setInputValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const { currentWorkspace } = useAppState();
const { t } = useTranslation();
useEffect(() => {
inputRef.current?.addEventListener(
'blur',
() => {
inputRef.current?.focus();
},
true
);
return inputRef.current?.focus();
}, [inputRef]);
if (open) {
const inputElement = inputRef.current;
return inputElement?.focus();
}
}, [open]);
useEffect(() => {
return setInputValue(props.query);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const inputElement = inputRef.current;
if (!open) {
return;
}
const handleFocus = () => {
inputElement?.focus();
};
inputElement?.addEventListener('blur', handleFocus, true);
return () => inputElement?.removeEventListener('blur', handleFocus, true);
}, [inputRef, open]);
useEffect(() => {
setInputValue(query);
}, [query]);
return (
<StyledInputContent>
<StyledLabel htmlFor=":r5:">
@@ -46,18 +55,18 @@ export const Input = (props: {
setIsComposition(true);
}}
onCompositionEnd={e => {
props.setQuery(e.data);
setQuery(e.data);
setIsComposition(false);
if (!props.query) {
props.setLoading(true);
if (!query) {
setLoading(true);
}
}}
onValueChange={str => {
setInputValue(str);
if (!isComposition) {
props.setQuery(str);
if (!props.query) {
props.setLoading(true);
setQuery(str);
if (!query) {
setLoading(true);
}
}
}}
@@ -78,9 +87,9 @@ export const Input = (props: {
}
}}
placeholder={
currentWorkspace?.isPublish
isPublic
? t('Quick search placeholder2', {
workspace: currentWorkspace?.blocksuiteWorkspace?.meta.name,
workspace: publishWorkspaceName,
})
: t('Quick search placeholder')
}
@@ -0,0 +1,95 @@
import { Command } from 'cmdk';
import { StyledListItem, StyledNotFound } from './style';
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useAppState, PageMeta } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
import { NoResultSVG } from './NoResultSVG';
import { useTranslation } from '@affine/i18n';
import usePageHelper from '@/hooks/use-page-helper';
import { Workspace } from '@blocksuite/store';
export const PublishedResults = (props: {
query: string;
loading: boolean;
setLoading: Dispatch<SetStateAction<boolean>>;
setPublishWorkspaceName: Dispatch<SetStateAction<string>>;
onClose: () => void;
}) => {
const [workspace, setWorkspace] = useState<Workspace>();
const { query, loading, setLoading, onClose, setPublishWorkspaceName } =
props;
const { search } = usePageHelper();
const [results, setResults] = useState(new Map<string, string | undefined>());
const { dataCenter } = useAppState();
const router = useRouter();
const [pageList, setPageList] = useState<PageMeta[]>([]);
useEffect(() => {
dataCenter
.loadPublicWorkspace(router.query.workspaceId as string)
.then(data => {
setPageList(data.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]);
if (data.blocksuiteWorkspace) {
setWorkspace(data.blocksuiteWorkspace);
setPublishWorkspaceName(data.blocksuiteWorkspace.meta.name);
}
})
.catch(() => {
router.push('/404');
});
}, [router, dataCenter, setPublishWorkspaceName]);
const { t } = useTranslation();
useEffect(() => {
setResults(search(query, workspace));
setLoading(false);
//Save the Map<BlockId, PageId> obtained from the search as state
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query, setResults, setLoading]);
const pageIds = [...results.values()];
const resultsPageMeta = pageList.filter(
page => pageIds.indexOf(page.id) > -1 && !page.trash
);
return loading ? null : (
<>
{query ? (
resultsPageMeta.length ? (
<Command.Group
heading={t('Find results', { number: resultsPageMeta.length })}
>
{resultsPageMeta.map(result => {
return (
<Command.Item
key={result.id}
onSelect={() => {
router.push(
`/public-workspace/${router.query.workspaceId}/${result.id}`
);
onClose();
}}
value={result.id}
>
<StyledListItem>
{result.mode === 'edgeless' ? (
<EdgelessIcon />
) : (
<PaperIcon />
)}
<span>{result.title}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
) : (
<StyledNotFound>
<span>{t('Find 0 result')}</span>
<NoResultSVG />
</StyledNotFound>
)
) : (
<></>
)}
</>
);
};
@@ -1,6 +1,5 @@
import { Command } from 'cmdk';
import { StyledListItem, StyledNotFound } from './style';
import { useModal } from '@/providers/GlobalModalProvider';
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useAppState } from '@/providers/app-state-provider';
@@ -12,14 +11,11 @@ import usePageHelper from '@/hooks/use-page-helper';
export const Results = (props: {
query: string;
loading: boolean;
onClose: () => void;
setLoading: Dispatch<SetStateAction<boolean>>;
setShowCreatePage: Dispatch<SetStateAction<boolean>>;
}) => {
const query = props.query;
const loading = props.loading;
const setLoading = props.setLoading;
const setShowCreatePage = props.setShowCreatePage;
const { triggerQuickSearchModal } = useModal();
const { query, loading, setLoading, setShowCreatePage, onClose } = props;
const { openPage } = usePageHelper();
const router = useRouter();
const { currentWorkspace, pageList } = useAppState();
@@ -55,8 +51,8 @@ export const Results = (props: {
<Command.Item
key={result.id}
onSelect={() => {
onClose();
openPage(result.id);
triggerQuickSearchModal();
}}
value={result.id}
>
@@ -86,8 +82,8 @@ export const Results = (props: {
key={link.title}
value={link.title}
onSelect={() => {
onClose();
router.push(link.href);
triggerQuickSearchModal();
}}
>
<StyledListItem>
@@ -1,5 +1,10 @@
import { FC, SVGProps } from 'react';
import { AllPagesIcon, FavouritesIcon, TrashIcon } from '@blocksuite/icons';
import {
AllPagesIcon,
FavouritesIcon,
TrashIcon,
SettingsIcon,
} from '@blocksuite/icons';
import { useTranslation } from '@affine/i18n';
export const useSwitchToConfig = (
@@ -23,6 +28,13 @@ export const useSwitchToConfig = (
: '',
icon: FavouritesIcon,
},
{
title: t('Settings'),
href: currentWorkspaceId
? `/workspace/${currentWorkspaceId}/setting`
: '',
icon: SettingsIcon,
},
{
title: t('Trash'),
href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/trash` : '',
@@ -13,7 +13,8 @@ import { Command } from 'cmdk';
import { useEffect, useState } from 'react';
import { useModal } from '@/providers/GlobalModalProvider';
import { getUaHelper } from '@/utils';
import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
import { PublishedResults } from './PublishedResults';
type TransitionsModalProps = {
open: boolean;
onClose: () => void;
@@ -22,18 +23,30 @@ const isMac = () => {
return getUaHelper().isMacOs;
};
export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
const { currentWorkspace } = useAppState();
const router = useRouter();
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(true);
const [isPublic, setIsPublic] = useState(false);
const [publishWorkspaceName, setPublishWorkspaceName] = useState('');
const [showCreatePage, setShowCreatePage] = useState(true);
const { triggerQuickSearchModal } = useModal();
const isPublicAndNoQuery = () => {
return isPublic && query.length === 0;
};
const handleClose = () => {
setQuery('');
onClose();
};
// Add ‘⌘+K shortcut keys as switches
useEffect(() => {
if (router.pathname.startsWith('/404')) {
triggerQuickSearchModal(false);
return;
}
const down = (e: KeyboardEvent) => {
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
const selection = window.getSelection();
setQuery('');
if (selection?.toString()) {
triggerQuickSearchModal(false);
return;
@@ -43,18 +56,23 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
}
}
};
if (!open) {
setQuery('');
}
document.addEventListener('keydown', down, { capture: true });
return () =>
document.removeEventListener('keydown', down, { capture: true });
}, [open, triggerQuickSearchModal]);
}, [open, router.pathname, triggerQuickSearchModal]);
useEffect(() => {
if (router.pathname.startsWith('/public-workspace')) {
return setIsPublic(true);
} else {
return setIsPublic(false);
}
}, [router]);
return (
<Modal
open={open}
onClose={onClose}
onClose={handleClose}
wrapperPosition={['top', 'center']}
data-testid="quickSearch"
>
@@ -62,7 +80,7 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
width={620}
style={{
maxHeight: '80vh',
minHeight: '350px',
minHeight: isPublicAndNoQuery() ? '72px' : '350px',
top: '12vh',
}}
>
@@ -81,28 +99,51 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
}}
>
<StyledModalHeader>
<Input query={query} setQuery={setQuery} setLoading={setLoading} />
<Input
open={open}
query={query}
setQuery={setQuery}
setLoading={setLoading}
isPublic={isPublic}
publishWorkspaceName={publishWorkspaceName}
/>
<StyledShortcut>{isMac() ? '⌘ + K' : 'Ctrl + K'}</StyledShortcut>
</StyledModalHeader>
<StyledModalDivider />
<StyledModalDivider
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
/>
<Command.List>
<StyledContent>
<Results
query={query}
loading={loading}
setLoading={setLoading}
setShowCreatePage={setShowCreatePage}
/>
<StyledContent
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
>
{!isPublic ? (
<Results
query={query}
loading={loading}
setLoading={setLoading}
onClose={handleClose}
setShowCreatePage={setShowCreatePage}
/>
) : (
<PublishedResults
query={query}
loading={loading}
setLoading={setLoading}
onClose={handleClose}
setPublishWorkspaceName={setPublishWorkspaceName}
data-testid="publishedSearchResults"
/>
)}
</StyledContent>
{currentWorkspace?.published ? (
<></>
) : showCreatePage ? (
<>
<StyledModalDivider />
<StyledModalFooter>
<Footer query={query} />
</StyledModalFooter>
</>
{!isPublic ? (
showCreatePage ? (
<>
<StyledModalDivider />
<StyledModalFooter>
<Footer query={query} onClose={handleClose} />
</StyledModalFooter>
</>
) : null
) : null}
</Command.List>
</Command>
@@ -9,6 +9,7 @@ export const StyledContent = styled('div')(({ theme }) => {
marginBottom: '10px',
...displayFlex('center', 'flex-start'),
color: theme.colors.popoverColor,
transition: 'all 0.15s',
letterSpacing: '0.06em',
'[cmdk-group-heading]': {
margin: '5px 16px',
@@ -16,7 +17,6 @@ export const StyledContent = styled('div')(({ theme }) => {
fontWeight: '500',
},
'[aria-selected="true"]': {
transition: 'background .15s, color .15s',
borderRadius: '5px',
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
@@ -103,6 +103,7 @@ export const StyledModalDivider = styled('div')(({ theme }) => {
margin: '6px 16px 6.5px 16px',
position: 'relative',
borderTop: `0.5px solid ${theme.colors.placeHolderColor}`,
transition: 'all 0.15s',
};
});
@@ -13,7 +13,7 @@ import {
useWindowsKeyboardShortcuts,
useWinMarkdownShortcuts,
} from '@/components/shortcuts-modal/config';
import Slide from '@mui/material/Slide';
import { MuiSlide } from '@/ui/mui';
import { ModalCloseButton } from '@/ui/modal';
import { getUaHelper } from '@/utils';
import { useTranslation } from '@affine/i18n';
@@ -40,7 +40,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
: windowsKeyboardShortcuts;
return createPortal(
<Slide direction="left" in={open} mountOnEnter unmountOnExit>
<MuiSlide direction="left" in={open} mountOnEnter unmountOnExit>
<StyledShortcutsModal data-testid="shortcuts-modal">
<>
<StyledModalHeader>
@@ -81,7 +81,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
})}
</>
</StyledShortcutsModal>
</Slide>,
</MuiSlide>,
document.body
);
};
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import type { WorkspaceUnit } from '@affine/datacenter';
import { WorkspaceAvatar as Avatar } from './Avatar';
const useAvatar = (workspaceUnit?: WorkspaceUnit) => {
const useAvatar = (workspaceUnit: WorkspaceUnit | null) => {
const [avatarUrl, setAvatarUrl] = useState('');
const avatarId =
workspaceUnit?.avatar || workspaceUnit?.blocksuiteWorkspace?.meta.avatar;
@@ -23,7 +23,7 @@ const useAvatar = (workspaceUnit?: WorkspaceUnit) => {
export const WorkspaceUnitAvatar = ({
size = 20,
name,
workspaceUnit,
workspaceUnit = null,
style,
}: {
size?: number;
@@ -31,7 +31,7 @@ export const WorkspaceUnitAvatar = ({
workspaceUnit?: WorkspaceUnit | null;
style?: React.CSSProperties;
}) => {
const avatarUrl = useAvatar(workspaceUnit || undefined);
const avatarUrl = useAvatar(workspaceUnit);
return (
<Avatar
size={size}
@@ -0,0 +1,64 @@
import { CloudInsyncIcon, LogOutIcon } from '@blocksuite/icons';
import { FlexWrapper } from '@/ui/layout';
import { WorkspaceAvatar } from '@/components/workspace-avatar';
import { IconButton } from '@/ui/button';
import { useAppState } from '@/providers/app-state-provider';
import { StyledFooter, StyleUserInfo, StyledSignInButton } from './styles';
import { useTranslation } from '@affine/i18n';
import { Tooltip } from '@/ui/tooltip';
export const Footer = ({
onLogin,
onLogout,
}: {
onLogin: () => void;
onLogout: () => void;
}) => {
const { user } = useAppState();
const { t } = useTranslation();
return (
<StyledFooter>
{user && (
<>
<FlexWrapper>
<WorkspaceAvatar
size={40}
name={user.name}
avatar={user.avatar}
></WorkspaceAvatar>
<StyleUserInfo>
<p>{user.name}</p>
<p>{user.email}</p>
</StyleUserInfo>
</FlexWrapper>
<Tooltip content={t('Sign out')} disablePortal={true}>
<IconButton
onClick={() => {
onLogout();
}}
>
<LogOutIcon />
</IconButton>
</Tooltip>
</>
)}
{!user && (
<StyledSignInButton
noBorder
bold
icon={
<div className="circle">
<CloudInsyncIcon fontSize={16} />
</div>
}
onClick={async () => {
onLogin();
}}
>
{t('Sign in')}
</StyledSignInButton>
)}
</StyledFooter>
);
};
@@ -0,0 +1,62 @@
import { LOCALES } from '@affine/i18n';
import { styled } from '@/styles';
import { useTranslation } from '@affine/i18n';
import { ArrowDownIcon } from '@blocksuite/icons';
import { Button } from '@/ui/button';
import { Menu, MenuItem } from '@/ui/menu';
const LanguageMenuContent = () => {
const { i18n } = useTranslation();
const changeLanguage = (event: string) => {
i18n.changeLanguage(event);
};
return (
<>
{LOCALES.map(option => {
return (
<ListItem
key={option.name}
title={option.name}
onClick={() => {
changeLanguage(option.tag);
}}
>
{option.originalName}
</ListItem>
);
})}
</>
);
};
export const LanguageMenu = () => {
const { i18n } = useTranslation();
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
return (
<Menu
content={<LanguageMenuContent />}
placement="bottom"
trigger="click"
disablePortal={true}
>
<Button
icon={<ArrowDownIcon />}
iconPosition="end"
noBorder={true}
style={{ textTransform: 'capitalize' }}
data-testid="language-menu-button"
>
{currentLanguage?.originalName}
</Button>
</Menu>
);
};
const ListItem = styled(MenuItem)(({ theme }) => ({
height: '38px',
color: theme.colors.popoverColor,
fontSize: theme.font.sm,
textTransform: 'capitalize',
padding: '0 24px',
}));
@@ -0,0 +1,83 @@
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import {
CloudIcon,
LocalIcon,
OfflineIcon,
PublishedIcon,
} from '@/components/workspace-modal/icons';
import { UsersIcon } from '@blocksuite/icons';
import { WorkspaceUnit } from '@affine/datacenter';
import { useAppState } from '@/providers/app-state-provider';
import { StyleWorkspaceInfo, StyleWorkspaceTitle, StyledCard } from './styles';
import { useTranslation } from '@affine/i18n';
import { FlexWrapper } from '@/ui/layout';
const WorkspaceType = ({ workspaceData }: { workspaceData: WorkspaceUnit }) => {
const { user } = useAppState();
const { t } = useTranslation();
const isOwner = user?.id === workspaceData.owner?.id;
if (workspaceData.provider === 'local') {
return (
<p>
<LocalIcon />
{t('Local Workspace')}
</p>
);
}
return isOwner ? (
<p>
<CloudIcon />
{t('Cloud Workspace')}
</p>
) : (
<p>
<UsersIcon fontSize={20} color={'#FF646B'} />
{t('Joined Workspace')}
</p>
);
};
export const WorkspaceCard = ({
workspaceData,
onClick,
}: {
workspaceData: WorkspaceUnit;
onClick: (data: WorkspaceUnit) => void;
}) => {
const { currentWorkspace } = useAppState();
const { t } = useTranslation();
return (
<StyledCard
data-testid="workspace-card"
onClick={() => {
onClick(workspaceData);
}}
active={workspaceData.id === currentWorkspace?.id}
>
<FlexWrapper>
<WorkspaceUnitAvatar size={58} workspaceUnit={workspaceData} />
</FlexWrapper>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>
{workspaceData.name || 'AFFiNE'}
</StyleWorkspaceTitle>
<WorkspaceType workspaceData={workspaceData} />
{workspaceData.provider === 'local' && (
<p>
<OfflineIcon />
{t('Available Offline')}
</p>
)}
{workspaceData.published && (
<p>
<PublishedIcon />
{t('Published to Web')}
</p>
)}
</StyleWorkspaceInfo>
</StyledCard>
);
};
@@ -70,7 +70,7 @@ export const CloudIcon = () => {
<path
d="M2.5 11.3744C2.5 13.837 4.51472 15.8333 7 15.8333L13.75 15.8333C15.8211 15.8333 17.5 14.1532 17.5 12.0807C17.5 10.5419 16.5744 9.12069 15.25 8.54163C15.1098 6.10205 13.07 4.16663 10.5744 4.16663C8.62616 4.16663 6.95578 5.40527 6.25 7.08329C4 7.44788 2.5 9.33336 2.5 11.3744Z"
stroke="#60A5FA"
stroke-width="1.25"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
@@ -1,28 +1,34 @@
import { styled } from '@/styles';
import { Modal, ModalWrapper } from '@/ui/modal';
import { Button, IconButton } from '@/ui/button';
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { FlexWrapper } from '@/ui/layout';
import { useState } from 'react';
import { CreateWorkspaceModal } from '../create-workspace';
import {
UsersIcon,
AddIcon,
LogOutIcon,
CloudInsyncIcon,
PublishIcon,
CloseIcon,
} from '@blocksuite/icons';
import {
WorkspaceAvatar,
WorkspaceUnitAvatar,
} from '@/components/workspace-avatar';
import { Tooltip } from '@/ui/tooltip';
import { AddIcon, HelpCenterIcon } from '@blocksuite/icons';
import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
import { useTranslation } from '@affine/i18n';
import { LanguageMenu } from './languageMenu';
import { LanguageMenu } from './SelectLanguageMenu';
import { CloudIcon, LineIcon, LocalIcon, OfflineIcon } from './icons';
import { LoginModal } from '../login-modal';
import { LogoutModal } from '../logout-modal';
import {
StyledCard,
StyledSplitLine,
StyleWorkspaceInfo,
StyleWorkspaceTitle,
StyledModalHeaderLeft,
StyledModalTitle,
StyledHelperContainer,
StyledModalContent,
StyledOperationWrapper,
StyleWorkspaceAdd,
StyledModalHeader,
} from './styles';
import { WorkspaceCard } from './WorkspaceCard';
import { Footer } from './Footer';
interface WorkspaceModalProps {
open: boolean;
onClose: () => void;
@@ -30,337 +36,117 @@ interface WorkspaceModalProps {
export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => {
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
const { workspaceList, currentWorkspace, user, logout, isOwner } =
useAppState();
const { logout, dataCenter } = useAppState();
const router = useRouter();
const { t } = useTranslation();
const [loginOpen, setLoginOpen] = useState(false);
const [logoutOpen, setLogoutOpen] = useState(false);
return (
<div>
<>
<Modal open={open} onClose={onClose}>
<ModalWrapper
width={720}
height={690}
style={{
padding: '24px 40px',
display: 'flex',
flexDirection: 'column',
}}
>
<Header>
<ContentTitle>{t('My Workspaces')}</ContentTitle>
<HeaderOption>
<LanguageMenu />
<div
style={{
display: 'inline-block',
border: 'none',
margin: '2px 16px',
height: '24px',
position: 'relative',
top: '4px',
}}
<StyledModalHeader>
<StyledModalHeaderLeft>
<StyledModalTitle>{t('My Workspaces')}</StyledModalTitle>
<Tooltip
content={t('Workspace description')}
placement="top-start"
disablePortal={true}
>
<LineIcon></LineIcon>
</div>
<StyledHelperContainer>
<HelpCenterIcon />
</StyledHelperContainer>
</Tooltip>
</StyledModalHeaderLeft>
<Button
style={{ border: 'none', padding: 0 }}
<StyledOperationWrapper>
<LanguageMenu />
<StyledSplitLine />
<ModalCloseButton
onClick={() => {
onClose();
}}
>
<CloseIcon></CloseIcon>
</Button>
</HeaderOption>
</Header>
<Content>
<WorkspaceList>
{workspaceList.map((item, index) => {
return (
<WorkspaceItem
onClick={() => {
router.replace(`/workspace/${item.id}`);
onClose();
}}
active={item.id === currentWorkspace?.id}
key={index}
>
<div>
<WorkspaceUnitAvatar size={58} workspaceUnit={item} />
</div>
absolute={false}
/>
</StyledOperationWrapper>
</StyledModalHeader>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>
{item.name || 'AFFiNE'}
</StyleWorkspaceTitle>
{isOwner ? (
item.provider === 'local' ? (
<p>
<LocalIcon />
Local Workspace
</p>
) : (
<p>
<CloudIcon />
Cloud Workspace
</p>
)
) : (
<p>
<UsersIcon fontSize={20} color={'#FF646B'} />
Joined Workspace
</p>
)}
{item.provider === 'local' && (
<p>
<OfflineIcon />
All data can be accessed offline
</p>
)}
{item.published && (
<p>
<PublishIcon fontSize={16} /> Published to Web
</p>
)}
</StyleWorkspaceInfo>
</WorkspaceItem>
);
})}
<WorkspaceItem
onClick={() => {
setCreateWorkspaceOpen(true);
}}
>
<div>
<StyleWorkspaceAdd className="add-icon">
<AddIcon fontSize={18} />
</StyleWorkspaceAdd>
</div>
<StyledModalContent>
{dataCenter.workspaces.map((item, index) => {
return (
<WorkspaceCard
workspaceData={item}
onClick={workspaceData => {
router.replace(`/workspace/${workspaceData.id}`);
onClose();
}}
key={index}
></WorkspaceCard>
);
})}
<StyledCard
onClick={() => {
setCreateWorkspaceOpen(true);
}}
>
<FlexWrapper>
<StyleWorkspaceAdd className="add-icon">
<AddIcon fontSize={18} />
</StyleWorkspaceAdd>
</FlexWrapper>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>New workspace</StyleWorkspaceTitle>
<p>Crete or import</p>
</StyleWorkspaceInfo>
</WorkspaceItem>
</WorkspaceList>
{/* <p style={{ fontSize: '14px', color: '#ccc', margin: '12px 0' }}>
{t('Tips')}
{t('Workspace description')}
</p> */}
</Content>
<LoginModal
open={loginOpen}
onClose={() => {
setLoginOpen(false);
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>{t('New Workspace')}</StyleWorkspaceTitle>
<p>{t('Create Or Import')}</p>
</StyleWorkspaceInfo>
</StyledCard>
</StyledModalContent>
<Footer
onLogin={() => {
setLoginOpen(true);
}}
></LoginModal>
<Footer>
{!user ? (
<StyleSignIn
onClick={async () => {
setLoginOpen(true);
}}
>
<span>
<CloudInsyncIcon fontSize={16} />
</span>
Sign in to sync with AFFINE Cloud
</StyleSignIn>
) : (
<div style={{ display: 'flex' }}>
<div style={{ paddingTop: '20px' }}>
<WorkspaceAvatar
size={40}
name={user.name}
avatar={user.avatar}
></WorkspaceAvatar>
</div>
<StyleUserInfo style={{}}>
<p>{user.name}</p>
<p>{user.email}</p>
</StyleUserInfo>
<div style={{ paddingTop: '25px' }}>
<IconButton
onClick={() => {
setLogoutOpen(true);
// confirm({
// title: 'Sign out?',
// content: `All data has been stored in the cloud. `,
// confirmText: 'Sign out',
// cancelText: 'Cancel',
// }).then(async confirm => {
// // if (confirm) {
// // if (user) {
// // await logout();
// // router.replace(`/workspace`);
// // toast('Enabled success');
// // }
// // }
// });
}}
>
<LogOutIcon></LogOutIcon>
</IconButton>
</div>
</div>
)}
</Footer>
<CreateWorkspaceModal
open={createWorkspaceOpen}
onClose={() => {
setCreateWorkspaceOpen(false);
onLogout={() => {
setLogoutOpen(true);
}}
></CreateWorkspaceModal>
<LogoutModal
open={logoutOpen}
onClose={async wait => {
if (!wait) {
await logout();
router.replace(`/workspace`);
}
setLogoutOpen(false);
}}
></LogoutModal>
/>
</ModalWrapper>
</Modal>
</div>
<LoginModal
open={loginOpen}
onClose={() => {
setLoginOpen(false);
}}
/>
<LogoutModal
open={logoutOpen}
onClose={async wait => {
if (!wait) {
await logout();
if (dataCenter.workspaces.length === 0) {
router.push(`/workspace`);
} else {
router.push(`/workspace/${dataCenter.workspaces[0].id}`);
}
}
setLogoutOpen(false);
}}
/>
<CreateWorkspaceModal
open={createWorkspaceOpen}
onClose={() => {
setCreateWorkspaceOpen(false);
}}
/>
</>
);
};
const Header = styled('div')({
display: 'flex',
});
const Content = styled('div')({
flexDirection: 'column',
alignItems: 'center',
gap: '16px',
flex: 1,
});
const HeaderOption = styled.div(() => {
return {
marginLeft: '16px',
};
});
const ContentTitle = styled('div')({
fontSize: '20px',
lineHeight: '24px',
fontWeight: 600,
textAlign: 'left',
flex: 1,
});
const WorkspaceList = styled('div')({
maxHeight: '500px',
overflow: 'auto',
display: 'grid',
gridRowGap: '24px',
gridColumnGap: '24px',
fontSize: '16px',
marginTop: '36px',
gridTemplateColumns: 'repeat(2, 1fr)',
});
export const WorkspaceItem = styled.div<{
active?: boolean;
}>(({ theme, active }) => {
const borderColor = active ? theme.colors.primaryColor : 'transparent';
return {
cursor: 'pointer',
padding: '16px',
height: '124px',
boxShadow: theme.shadow.modal,
display: 'flex',
borderRadius: '12px',
border: `1px solid ${borderColor}`,
':hover': {
background: theme.colors.hoverBackground,
'.add-icon': {
border: `1.5px dashed ${theme.colors.primaryColor}`,
svg: {
fill: theme.colors.primaryColor,
},
},
},
};
});
const StyleWorkspaceInfo = styled.div(({ theme }) => {
return {
marginLeft: '16px',
p: {
fontSize: theme.font.xs,
lineHeight: '16px',
},
svg: {
verticalAlign: 'text-bottom',
marginRight: '8px',
},
};
});
const StyleWorkspaceTitle = styled.div(({ theme }) => {
return {
fontSize: theme.font.base,
fontWeight: 600,
lineHeight: '24px',
marginBottom: '8px',
};
});
const StyleWorkspaceAdd = styled.div(() => {
return {
width: '58px',
height: '58px',
borderRadius: '100%',
textAlign: 'center',
background: '#f4f5fa',
border: '1.5px dashed #f4f5fa',
lineHeight: '58px',
marginTop: '2px',
};
});
const Footer = styled('div')({
paddingTop: '16px',
});
const StyleUserInfo = styled.div(({ theme }) => {
return {
textAlign: 'left',
marginLeft: '16px',
marginTop: '16px',
flex: 1,
p: {
lineHeight: '24px',
color: theme.colors.iconColor,
},
'p:nth-child(1)': {
color: theme.colors.textColor,
fontWeight: 600,
},
};
});
const StyleSignIn = styled.div(({ theme }) => {
return {
cursor: 'pointer',
fontSize: '16px',
fontWeight: 700,
color: theme.colors.iconColor,
span: {
display: 'inline-block',
width: '40px',
height: '40px',
borderRadius: '40px',
backgroundColor: theme.colors.innerHoverBackground,
textAlign: 'center',
lineHeight: '44px',
marginRight: '16px',
svg: {
fill: theme.colors.primaryColor,
},
},
};
});
@@ -1,93 +0,0 @@
import { LOCALES } from '@affine/i18n';
import { TooltipProps } from '@mui/material';
import { styled } from '@/styles';
import { Tooltip } from '@mui/material';
import { useState } from 'react';
import { useTranslation } from '@affine/i18n';
import { ArrowDownIcon } from '@blocksuite/icons';
import { Button } from '@/ui/button';
export const LanguageMenu = () => {
const { i18n } = useTranslation();
const changeLanguage = (event: string) => {
i18n.changeLanguage(event);
};
const [show, setShow] = useState(false);
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
const [languageName, setLanguageName] = useState(
currentLanguage?.originalName
);
return (
<StyledTooltip
title={
<>
{LOCALES.map(option => {
return (
<ListItem
style={{ border: 'none' }}
key={option.name}
title={option.name}
onClick={() => {
changeLanguage(option.tag);
setShow(false);
setLanguageName(option.originalName);
}}
>
{option.originalName}
</ListItem>
);
})}
</>
}
open={show}
>
<StyledTitleButton
style={{ border: 'none', padding: '0px' }}
onClick={() => {
setShow(!show);
}}
>
<StyledContainer>
<StyledText>{languageName}</StyledText>
<ArrowDownIcon fontSize={18} />
</StyledContainer>
</StyledTitleButton>
</StyledTooltip>
);
};
const StyledContainer = styled('div')(() => ({
display: 'flex',
alignItems: 'center',
}));
const StyledText = styled('span')(({ theme }) => ({
marginRight: '4px',
marginLeft: '16px',
fontSize: theme.font.sm,
fontWeight: '500',
textTransform: 'capitalize',
}));
const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
zIndex: theme.zIndex.modal,
'& .MuiTooltip-tooltip': {
backgroundColor: theme.colors.popoverBackground,
boxShadow: theme.shadow.modal,
color: theme.colors.popoverColor,
},
}));
const ListItem = styled(Button)(({ theme }) => ({
display: 'block',
width: '100%',
color: theme.colors.popoverColor,
fontSize: theme.font.sm,
textTransform: 'capitalize',
}));
const StyledTitleButton = styled(Button)(({ theme }) => ({
color: theme.colors.popoverColor,
fontSize: theme.font.sm,
}));
@@ -1,38 +1,159 @@
import { displayFlex, styled } from '@/styles';
import { displayFlex, displayInlineFlex, styled, textEllipsis } from '@/styles';
import { Button } from '@/ui/button';
export const StyledTitle = styled.div(() => {
export const StyledSplitLine = styled.div(({ theme }) => {
return {
...displayFlex('center', 'center'),
fontSize: '20px',
fontWeight: 500,
marginTop: '60px',
lineHeight: 1,
width: '1px',
height: '20px',
background: theme.colors.iconColor,
marginRight: '24px',
};
});
export const StyledContent = styled.div(() => {
export const StyleWorkspaceInfo = styled.div(({ theme }) => {
return {
padding: '0 40px',
marginTop: '32px',
fontSize: '18px',
lineHeight: '25px',
'p:not(last-of-type)': {
marginBottom: '10px',
marginLeft: '15px',
p: {
height: '20px',
fontSize: theme.font.xs,
...displayFlex('flex-start', 'center'),
},
svg: {
marginRight: '10px',
},
};
});
export const StyledButton = styled.div(({ theme }) => {
export const StyleWorkspaceTitle = styled.div(({ theme }) => {
return {
width: '146px',
height: '42px',
background: theme.colors.primaryColor,
color: '#FFFFFF',
fontSize: '18px',
fontWeight: 500,
borderRadius: '21px',
margin: '52px auto 0',
fontSize: theme.font.base,
fontWeight: 600,
lineHeight: '24px',
marginBottom: '10px',
maxWidth: '200px',
...textEllipsis(1),
};
});
export const StyledCard = styled.div<{
active?: boolean;
}>(({ theme, active }) => {
const borderColor = active ? theme.colors.primaryColor : 'transparent';
return {
width: '310px',
height: '124px',
cursor: 'pointer',
padding: '16px',
boxShadow: '0px 0px 8px rgba(0, 0, 0, 0.1)',
borderRadius: '12px',
border: `1px solid ${borderColor}`,
...displayFlex('flex-start', 'flex-start'),
marginBottom: '24px',
':hover': {
background: theme.colors.hoverBackground,
'.add-icon': {
border: `1.5px dashed ${theme.colors.primaryColor}`,
svg: {
fill: theme.colors.primaryColor,
},
},
},
};
});
export const StyledFooter = styled('div')({
height: '84px',
padding: '0 40px',
...displayFlex('space-between', 'center'),
});
export const StyleUserInfo = styled.div(({ theme }) => {
return {
textAlign: 'left',
marginLeft: '16px',
flex: 1,
p: {
lineHeight: '24px',
color: theme.colors.iconColor,
},
'p:nth-child(1)': {
color: theme.colors.textColor,
fontWeight: 600,
},
};
});
export const StyledModalHeaderLeft = styled.div(() => {
return { ...displayFlex('flex-start', 'center') };
});
export const StyledModalTitle = styled.div(({ theme }) => {
return {
fontWeight: 600,
fontSize: theme.font.h6,
};
});
export const StyledHelperContainer = styled.div(({ theme }) => {
return {
color: theme.colors.iconColor,
marginLeft: '15px',
fontWeight: 400,
...displayFlex('center', 'center'),
};
});
export const StyledModalContent = styled('div')({
height: '534px',
padding: '8px 40px',
marginTop: '72px',
overflow: 'auto',
...displayFlex('space-between', 'flex-start', 'flex-start'),
flexWrap: 'wrap',
});
export const StyledOperationWrapper = styled.div(() => {
return {
...displayFlex('flex-end', 'center'),
};
});
export const StyleWorkspaceAdd = styled.div(() => {
return {
width: '58px',
height: '58px',
borderRadius: '100%',
textAlign: 'center',
background: '#f4f5fa',
border: '1.5px dashed #f4f5fa',
lineHeight: '58px',
marginTop: '2px',
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
width: '100%',
height: '72px',
position: 'absolute',
left: 0,
top: 0,
background: theme.colors.pageBackground,
borderRadius: '24px 24px 0 0',
padding: '0 40px',
...displayFlex('space-between', 'center'),
};
});
export const StyledSignInButton = styled(Button)(({ theme }) => {
return {
fontWeight: 700,
paddingLeft: 0,
'.circle': {
width: '40px',
height: '40px',
borderRadius: '20px',
backgroundColor: theme.colors.innerHoverBackground,
flexShrink: 0,
marginRight: '16px',
...displayInlineFlex('center', 'center'),
},
};
});
@@ -1,24 +1,16 @@
import { styled } from '@/styles';
import { WorkspaceUnit } from '@affine/datacenter';
import { Trans } from '@affine/i18n';
export const ExportPageTitleContainer = styled('div')(() => {
return {
display: 'flex',
fontWeight: '500',
flex: 1,
};
});
import { useTranslation } from '@affine/i18n';
import { Wrapper } from '@/ui/layout';
import { Button } from '@/ui/button';
export const ExportPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { t } = useTranslation();
console.log(workspace);
return (
<ExportPageTitleContainer>
<Trans i18nKey="Export Workspace">
Export Workspace
<code style={{ margin: '0 10px' }}>
{{ workspace: workspace.name }}
</code>
Is Comming
</Trans>
</ExportPageTitleContainer>
<>
<Wrapper marginBottom="32px"> {t('Export Description')}</Wrapper>
<Button type="light" shape="circle" disabled>
{t('Export AFFINE backup file')}
</Button>
</>
);
};
@@ -1,20 +1,12 @@
import {
StyledCopyButtonContainer,
StyledPublishContent,
StyledPublishCopyContainer,
StyledPublishExplanation,
StyledSettingH2,
StyledStopPublishContainer,
} from './style';
import { useState } from 'react';
import { Button } from '@/ui/button';
import Input from '@/ui/input';
import { toast } from '@/ui/toast';
// import { useAppState } from '@/providers/app-state-provider3';
import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { useTranslation } from '@affine/i18n';
import { EnableWorkspaceButton } from '../enable-workspace';
import { Wrapper, Content, FlexWrapper } from '@/ui/layout';
export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const shareUrl = window.location.host + '/public-workspace/' + workspace.id;
const { publishWorkspace } = useWorkspaceHelper();
@@ -23,95 +15,75 @@ export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const togglePublic = async (flag: boolean) => {
try {
await publishWorkspace(workspace.id.toString(), flag);
setLoaded(false);
} catch (e) {
toast('Failed to publish workspace');
toast(t('Failed to publish workspace'));
}
};
const copyUrl = () => {
navigator.clipboard.writeText(shareUrl);
toast('Copied url to clipboard');
toast(t('Copied link to clipboard'));
};
if (workspace.provider === 'affine') {
if (workspace.published) {
return (
<>
<Wrapper marginBottom="32px">{t('Published Description')}</Wrapper>
<Wrapper marginBottom="12px">
<Content weight="500">{t('Share with link')}</Content>
</Wrapper>
<FlexWrapper>
<Input width={582} value={shareUrl} disabled={true}></Input>
<Button
onClick={copyUrl}
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Copy Link')}
</Button>
</FlexWrapper>
<Button
onClick={async () => {
setLoaded(true);
await togglePublic(false);
}}
loading={false}
type="danger"
shape="circle"
style={{ marginTop: '38px' }}
>
{t('Stop publishing')}
</Button>
</>
);
}
return (
<>
<Wrapper marginBottom="32px">{t('Publishing Description')}</Wrapper>
<Button
onClick={async () => {
setLoaded(true);
await togglePublic(true);
}}
loading={loaded}
type="light"
shape="circle"
>
{t('Publish to web')}
</Button>
</>
);
}
return (
<>
{workspace.provider === 'affine' ? (
<div
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
>
<StyledPublishContent>
{workspace.published ? (
<>
<StyledPublishExplanation>
The current workspace has been published to the web, everyone
can view the contents of this workspace through the link.
</StyledPublishExplanation>
<StyledPublishCopyContainer>
<StyledSettingH2 marginBottom={16}>
{t('Share with link')}
</StyledSettingH2>
<Input width={500} value={shareUrl} disabled={true}></Input>
<StyledCopyButtonContainer>
<Button onClick={copyUrl} type="primary" shape="circle">
{t('Copy Link')}
</Button>
</StyledCopyButtonContainer>
</StyledPublishCopyContainer>
</>
) : (
<StyledPublishExplanation>
{t('Publishing Description')}
<div style={{ marginTop: '64px' }}>
<Button
onClick={async () => {
setLoaded(true);
await togglePublic(true);
setLoaded(false);
}}
loading={loaded}
type="primary"
shape="circle"
>
{t('Publish to web')}
</Button>
</div>
</StyledPublishExplanation>
)}
</StyledPublishContent>
{workspace.published ? (
<StyledStopPublishContainer>
<Button
onClick={async () => {
setLoaded(true);
await togglePublic(false);
setLoaded(true);
}}
loading={false}
type="danger"
shape="circle"
>
{t('Stop publishing')}
</Button>
</StyledStopPublishContainer>
) : (
<></>
)}
</div>
) : (
<StyledPublishContent>
<>
<StyledPublishExplanation>
Publishing to web requires AFFiNE Cloud service.
</StyledPublishExplanation>
<div style={{ marginTop: '72px' }}>
<EnableWorkspaceButton></EnableWorkspaceButton>
</div>
</>
</StyledPublishContent>
)}
<Wrapper marginBottom="32px">{t('Publishing')}</Wrapper>
<EnableWorkspaceButton />
</>
);
};
@@ -1,84 +1,106 @@
import {
StyleAsync,
StyledPublishContent,
StyledPublishExplanation,
StyledWorkspaceName,
StyledWorkspaceType,
StyledEmail,
// StyledDownloadCard,
// StyledDownloadCardDes,
} from './style';
import { DownloadIcon } from '@blocksuite/icons';
import { Button } from '@/ui/button';
import { Menu, MenuItem } from '@/ui/menu';
import { WorkspaceUnit } from '@affine/datacenter';
import { Trans, useTranslation } from '@affine/i18n';
import { useTranslation, Trans } from '@affine/i18n';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { EnableWorkspaceButton } from '../enable-workspace';
import { useAppState } from '@/providers/app-state-provider';
import { FlexWrapper, Content, Wrapper } from '@/ui/layout';
// // FIXME: Temporary solution, since the @blocksuite/icons is broken
// const ActiveIcon = () => {
// return (
// <svg
// width="24"
// height="24"
// viewBox="0 0 24 24"
// fill="none"
// xmlns="http://www.w3.org/2000/svg"
// >
// <path
// fillRule="evenodd"
// clipRule="evenodd"
// d="M2.25 12C2.25 6.61522 6.61522 2.25 12 2.25C17.3848 2.25 21.75 6.61522 21.75 12C21.75 17.3848 17.3848 21.75 12 21.75C6.61522 21.75 2.25 17.3848 2.25 12Z"
// fill="#6880FF"
// />
// <path
// fillRule="evenodd"
// clipRule="evenodd"
// d="M16.5068 8.44714C16.8121 8.72703 16.8328 9.20146 16.5529 9.5068L11.0529 15.5068C10.9146 15.6576 10.7208 15.7454 10.5163 15.7498C10.3118 15.7543 10.1143 15.675 9.96967 15.5303L7.46967 13.0303C7.17678 12.7374 7.17678 12.2626 7.46967 11.9697C7.76256 11.6768 8.23744 11.6768 8.53033 11.9697L10.4764 13.9158L15.4471 8.49321C15.727 8.18787 16.2015 8.16724 16.5068 8.44714Z"
// fill="white"
// />
// </svg>
// );
// };
export const SyncPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { t } = useTranslation();
const { user } = useAppState();
if (workspace.provider === 'local') {
return (
<>
<FlexWrapper alignItems="center" style={{ marginBottom: '12px' }}>
<WorkspaceUnitAvatar
size={32}
name={workspace.name}
workspaceUnit={workspace}
style={{ marginRight: '12px' }}
/>
<StyledWorkspaceName>{workspace.name}&nbsp;</StyledWorkspaceName>
<Content weight={500}>{t('is a Local Workspace')}</Content>
</FlexWrapper>
<p>{t('Local Workspace Description')}</p>
<Wrapper marginTop="32px">
<EnableWorkspaceButton />
</Wrapper>
</>
);
}
return (
<div>
<StyledPublishContent>
{workspace.provider === 'local' ? (
<>
<StyledPublishExplanation>
<WorkspaceUnitAvatar
size={32}
name={workspace.name}
workspaceUnit={workspace}
style={{ marginRight: '12px' }}
/>
<StyledWorkspaceName>{workspace.name};</StyledWorkspaceName>
<StyledWorkspaceType>is a Local Workspace.</StyledWorkspaceType>
</StyledPublishExplanation>
<StyledWorkspaceType>
All data is stored on the current device. You can enable AFFiNE
Cloud for this workspace to keep data in sync with the cloud.
</StyledWorkspaceType>
<StyleAsync>
<EnableWorkspaceButton></EnableWorkspaceButton>
</StyleAsync>
</>
) : (
<>
<StyledPublishExplanation>
<Trans i18nKey="Sync Description2">
<code>{{ workspaceName: workspace.name ?? 'Affine' }}</code>
is Cloud Workspace. All data will be synchronised and saved to
the AFFiNE
</Trans>
</StyledPublishExplanation>
<StyleAsync>
<Menu
content={
<>
<MenuItem
onClick={() => {
// deleteMember(workspace.id, 0);
}}
icon={<DownloadIcon />}
>
{t('Download data to device', { CoreOrAll: 'core' })}
</MenuItem>
<MenuItem
onClick={() => {
// deleteMember(workspace.id, 0);
}}
icon={<DownloadIcon />}
>
{t('Download data to device', { CoreOrAll: 'all' })}
</MenuItem>
</>
}
placement="bottom-end"
disablePortal={true}
>
<Button type="primary">
{t('Download data to device', { CoreOrAll: 'all' })}
</Button>
</Menu>
</StyleAsync>
</>
)}
</StyledPublishContent>
</div>
<>
<FlexWrapper alignItems="center" style={{ marginBottom: '12px' }}>
<WorkspaceUnitAvatar
size={32}
name={workspace.name}
workspaceUnit={workspace}
style={{ marginRight: '12px' }}
/>
<StyledWorkspaceName>{workspace.name}&nbsp;</StyledWorkspaceName>
<Content weight={500}>{t('is a Cloud Workspace')}</Content>
</FlexWrapper>
<Trans i18nKey="Cloud Workspace Description">
All data will be synchronised and saved to the AFFiNE account
<StyledEmail>
{{
email: '{' + user?.email + '}.',
}}
</StyledEmail>
</Trans>
{/*<Wrapper marginBottom="12px" marginTop="32px">*/}
{/* <Content weight="500">{t('Data sync mode')}</Content>*/}
{/*</Wrapper>*/}
{/*<FlexWrapper>*/}
{/* <StyledDownloadCard>*/}
{/* <ActiveIcon />*/}
{/* <Wrapper>{t('Download all data')}</Wrapper>*/}
{/* <StyledDownloadCardDes>*/}
{/* {t('It takes up more space on your device.')}*/}
{/* </StyledDownloadCardDes>*/}
{/* </StyledDownloadCard>*/}
{/* <StyledDownloadCard active>*/}
{/* <ActiveIcon />*/}
{/* <Wrapper>{t('Download core data')}</Wrapper>*/}
{/* <StyledDownloadCardDes>*/}
{/* {t('It takes up little space on your device.')}*/}
{/* </StyledDownloadCardDes>*/}
{/* </StyledDownloadCard>*/}
{/*</FlexWrapper>*/}
</>
);
};
@@ -1,23 +1,13 @@
import {
StyledDeleteButtonContainer,
// StyledSettingAvatar,
StyledSettingAvatarContent,
StyledSettingInputContainer,
StyledDoneButtonContainer,
StyledInput,
StyledProviderInfo,
StyleGeneral,
StyledAvatar,
} from './style';
import { StyledSettingH2 } from '../style';
import { StyledInput, StyledProviderInfo, StyledAvatar } from './style';
import { StyledSettingKey, StyledRow } from '../style';
import { FlexWrapper, Content } from '@/ui/layout';
import { useState } from 'react';
import { Button } from '@/ui/button';
import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceDelete } from './delete';
import { WorkspaceLeave } from './leave';
import { DoneIcon, UsersIcon } from '@blocksuite/icons';
// import { Upload } from '@/components/file-upload';
import { UsersIcon } from '@blocksuite/icons';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
@@ -33,9 +23,6 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { updateWorkspace } = useWorkspaceHelper();
const { t } = useTranslation();
const handleChangeWorkSpaceName = (newName: string) => {
setWorkspaceName(newName);
};
const handleUpdateWorkspaceName = () => {
currentWorkspace &&
updateWorkspace({ name: workspaceName }, currentWorkspace);
@@ -47,83 +34,105 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
(await updateWorkspace({ avatarBlob: blob }, currentWorkspace));
};
return workspace ? (
<StyleGeneral>
<div style={{ flex: 1, overflow: 'auto' }}>
<StyledSettingH2>Workspace Avatar</StyledSettingH2>
<StyledSettingAvatarContent>
<StyledAvatar>
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange}
>
<>
<div className="camera-icon">
<CameraIcon></CameraIcon>
</div>
<WorkspaceUnitAvatar
size={60}
name={workspace.name}
workspaceUnit={workspace}
/>
</>
</Upload>
</StyledAvatar>
{/* TODO: Wait for image sync to complete */}
{/* <Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange}
>
<Button loading={uploading}>{t('Upload')}</Button>
</Upload> */}
{/* TODO: add upload logic */}
</StyledSettingAvatarContent>
<StyledSettingH2 marginTop={20}>{t('Workspace Name')}</StyledSettingH2>
<StyledSettingInputContainer>
if (!workspace) {
return null;
}
return (
<>
<StyledRow>
<StyledSettingKey>{t('Workspace Avatar')}</StyledSettingKey>
<StyledAvatar>
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange}
>
<>
<div className="camera-icon">
<CameraIcon></CameraIcon>
</div>
<WorkspaceUnitAvatar
size={72}
name={workspace.name}
workspaceUnit={workspace}
/>
</>
</Upload>
</StyledAvatar>
</StyledRow>
<StyledRow>
<StyledSettingKey>{t('Workspace Name')}</StyledSettingKey>
<FlexWrapper>
<StyledInput
width={284}
height={32}
height={38}
value={workspaceName}
placeholder={t('Workspace Name')}
maxLength={14}
minLength={1}
maxLength={15}
minLength={0}
disabled={!isOwner}
onChange={handleChangeWorkSpaceName}
onChange={newName => {
setWorkspaceName(newName);
}}
></StyledInput>
{isOwner ? (
<StyledDoneButtonContainer
onClick={() => {
handleUpdateWorkspaceName();
}}
>
<DoneIcon />
</StyledDoneButtonContainer>
) : null}
</StyledSettingInputContainer>
<StyledSettingH2 marginTop={20}>{t('Workspace Type')}</StyledSettingH2>
<StyledSettingInputContainer>
{isOwner && (
<>
<Button
type="default"
shape="circle"
style={{ marginLeft: '24px' }}
onClick={() => {
setWorkspaceName(workspace.name);
}}
>
{t('Cancel')}
</Button>
<Button
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
onClick={() => {
handleUpdateWorkspaceName();
}}
>
{t('Confirm')}
</Button>
</>
)}
</FlexWrapper>
</StyledRow>
<StyledRow>
<StyledSettingKey>{t('Workspace Type')}</StyledSettingKey>
<FlexWrapper>
{isOwner ? (
currentWorkspace?.provider === 'local' ? (
<StyledProviderInfo>
<FlexWrapper alignItems="center">
<LocalIcon />
Local Workspace
</StyledProviderInfo>
<Content style={{ marginLeft: '15px' }}>
{t('Local Workspace')}
</Content>
</FlexWrapper>
) : (
<StyledProviderInfo>
<FlexWrapper alignItems="center">
<CloudIcon />
All data can be accessed offline
</StyledProviderInfo>
<Content style={{ marginLeft: '15px' }}>
{t('Available Offline')}
</Content>
</FlexWrapper>
)
) : (
<StyledProviderInfo>
<UsersIcon fontSize={20} color={'#FF646B'} />
Joined Workspace
{t('Joined Workspace')}
</StyledProviderInfo>
)}
</StyledSettingInputContainer>
</div>
</FlexWrapper>
</StyledRow>
<StyledDeleteButtonContainer>
<StyledRow>
<StyledSettingKey> {t('Delete Workspace')}</StyledSettingKey>
{isOwner ? (
<>
<Button
@@ -163,7 +172,7 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
/>
</>
)}
</StyledDeleteButtonContainer>
</StyleGeneral>
) : null;
</StyledRow>
</>
);
};
@@ -1,3 +1,4 @@
import { useRouter } from 'next/router';
import Modal from '@/ui/modal';
import Input from '@/ui/input';
import {
@@ -11,7 +12,6 @@ import {
import { useState } from 'react';
import { ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import { useRouter } from 'next/router';
import { WorkspaceUnit } from '@affine/datacenter';
import { Trans, useTranslation } from '@affine/i18n';
@@ -1,40 +1,6 @@
import { displayFlex, styled } from '@/styles';
import MuiAvatar from '@mui/material/Avatar';
import IconButton from '@/ui/button/IconButton';
import { styled } from '@/styles';
import Input from '@/ui/input';
export const StyledSettingInputContainer = styled('div')(() => {
return {
marginTop: '12px',
width: '100%',
...displayFlex('flex-start', 'center'),
};
});
export const StyledDeleteButtonContainer = styled('div')(() => {
return {
textAlign: 'center',
};
});
export const StyleGeneral = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'column',
height: '100%',
};
});
export const StyledDoneButtonContainer = styled(IconButton)(({ theme }) => {
return {
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: '10px',
height: '32px',
overflow: 'hidden',
marginLeft: '20px',
':hover': {
borderColor: theme.colors.primaryColor,
},
};
});
export const StyledInput = styled(Input)(({ theme }) => {
return {
border: `1px solid ${theme.colors.borderColor}`,
@@ -43,24 +9,9 @@ export const StyledInput = styled(Input)(({ theme }) => {
};
});
export const StyledSettingAvatarContent = styled('div')(() => {
return {
marginTop: '12px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: '72px',
};
});
export const StyledSettingAvatar = styled(MuiAvatar)(() => {
return { height: '72px', width: '72px', marginRight: '24px' };
});
export const StyledProviderInfo = styled('p')(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
svg: {
verticalAlign: 'sub',
marginRight: '10px',
@@ -71,7 +22,6 @@ export const StyledProviderInfo = styled('p')(({ theme }) => {
export const StyledAvatar = styled('div')(() => {
return {
position: 'relative',
marginRight: '20px',
cursor: 'pointer',
':hover': {
'.camera-icon': {
@@ -4,7 +4,7 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import Input from '@/ui/input';
import { useState } from 'react';
import { Avatar } from '@mui/material';
import { MuiAvatar } from '@/ui/mui';
import useMembers from '@/hooks/use-members';
import { User } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n';
@@ -76,10 +76,9 @@ export const InviteMemberModal = ({
<ModalWrapper width={460} height={236}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
setEmail('');
}}
/>
</Header>
@@ -102,7 +101,7 @@ export const InviteMemberModal = ({
) : (
<Member>
{userData?.avatar ? (
<Avatar src={userData?.avatar}></Avatar>
<MuiAvatar src={userData?.avatar}></MuiAvatar>
) : (
<MemberIcon>
<EmailIcon></EmailIcon>
@@ -125,6 +124,7 @@ export const InviteMemberModal = ({
style={{ width: '364px', height: '38px', borderRadius: '40px' }}
onClick={async () => {
await inviteMember(email);
setEmail('');
onInviteSuccess();
}}
>
@@ -10,10 +10,9 @@ import {
StyledMemberRoleContainer,
StyledMemberTitleContainer,
StyledMoreVerticalButton,
StyledPublishExplanation,
StyledMemberWarp,
StyledMemberContainer,
} from './style';
import { Wrapper } from '@/ui/layout';
import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons';
import { useState } from 'react';
import { Button, IconButton } from '@/ui/button';
@@ -25,14 +24,13 @@ import { useConfirm } from '@/providers/ConfirmProvider';
import { toast } from '@/ui/toast';
import useMembers from '@/hooks/use-members';
import Loading from '@/components/loading';
import { Wrapper } from '@/ui/layout';
import { FlexWrapper } from '@/ui/layout';
import { useTranslation } from '@affine/i18n';
import { EnableWorkspaceButton } from '@/components/enable-workspace';
export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const { members, removeMember, loaded } = useMembers();
const { t } = useTranslation();
const { confirm } = useConfirm();
@@ -41,9 +39,9 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
<StyledMemberContainer>
<StyledMemberListContainer>
{!loaded && (
<Wrapper justifyContent="center">
<FlexWrapper justifyContent="center">
<Loading size={25} />
</Wrapper>
</FlexWrapper>
)}
{loaded && members.length === 0 && (
<Empty width={648} sx={{ marginTop: '60px' }} height={300} />
@@ -89,42 +87,50 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
<StyledMemberRoleContainer>
{member.accepted
? member.type !== 99
? 'Member'
: 'Workspace Owner'
: 'Pending'}
? t('Member')
: t('Owner')
: t('Pending')}
</StyledMemberRoleContainer>
<StyledMoreVerticalButton>
<Menu
content={
<>
<MenuItem
onClick={async () => {
const confirmRemove = await confirm({
title: 'Delete Member?',
content: `will delete member`,
confirmText: 'Delete',
confirmType: 'danger',
});
{member.type === 99 ? (
<></>
) : (
<Menu
content={
<>
<MenuItem
onClick={async () => {
const confirmRemove = await confirm({
title: t('Delete Member?'),
content: t('will delete member'),
confirmText: t('Delete'),
confirmType: 'danger',
});
if (!confirmRemove) {
return;
}
await removeMember(member.id);
toast(`${user.name} has been removed`);
}}
icon={<TrashIcon />}
>
Delete
</MenuItem>
</>
}
placement="bottom-end"
disablePortal={true}
>
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
if (!confirmRemove) {
return;
}
await removeMember(member.id);
toast(
t('Member has been removed', {
name: user.name,
})
);
}}
icon={<TrashIcon />}
>
{t('Delete')}
</MenuItem>
</>
}
placement="bottom-end"
disablePortal={true}
>
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
)}
</StyledMoreVerticalButton>
</StyledMemberListItem>
);
@@ -159,11 +165,14 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
}
return (
<StyledMemberWarp>
{t('Collaboration Description')}
<StyledPublishExplanation>
<EnableWorkspaceButton></EnableWorkspaceButton>
</StyledPublishExplanation>
</StyledMemberWarp>
<Wrapper
style={{
fontWeight: '500',
fontSize: '18px',
}}
>
<Wrapper marginBottom="32px">{t('Collaboration Description')}</Wrapper>
<EnableWorkspaceButton />
</Wrapper>
);
};
@@ -1,5 +1,5 @@
import { styled } from '@/styles';
import MuiAvatar from '@mui/material/Avatar';
import { MuiAvatar } from '@/ui/mui';
export const StyledMemberTitleContainer = styled('li')(() => {
return {
@@ -97,24 +97,3 @@ export const StyledMoreVerticalButton = styled('button')(() => {
paddingRight: '48px',
};
});
export const StyledPublishExplanation = styled('div')(() => {
return {
paddingRight: '48px',
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
flex: 1,
marginTop: '64px',
};
});
export const StyledMemberWarp = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'column',
padding: '0 0 48px 0',
fontWeight: '500',
fontSize: '18px',
};
});
@@ -1,5 +1,5 @@
import { styled } from '@/styles';
import { FlexWrapper } from '@/ui/layout';
export const StyledSettingContainer = styled('div')(() => {
return {
display: 'flex',
@@ -13,41 +13,19 @@ export const StyledSettingContainer = styled('div')(() => {
export const StyledSettingSidebar = styled('div')(() => {
{
return {
// height: '48px',
marginTop: '50px',
marginTop: '48px',
};
}
});
export const StyledSettingContent = styled('div')(() => {
return {
overflow: 'hidden',
overflow: 'auto',
flex: 1,
paddingTop: '48px',
};
});
export const StyledSetting = styled('div')(({ theme }) => {
{
return {
width: '236px',
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
};
}
});
export const StyledSettingSidebarHeader = styled('div')(() => {
{
return {
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'left',
marginTop: '37px',
};
}
});
export const StyledSettingTabContainer = styled('ul')(() => {
{
return {
@@ -65,8 +43,8 @@ export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
height: '34px',
color: isActive ? theme.colors.primaryColor : theme.colors.textColor,
fontWeight: '500',
fontSize: theme.font.base,
lineHeight: theme.font.lineHeightBase,
fontSize: theme.font.h6,
lineHeight: theme.font.lineHeight,
cursor: 'pointer',
transition: 'all 0.15s ease',
borderBottom: `2px solid ${
@@ -78,79 +56,60 @@ export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
}
);
export const StyledSettingTagIconContainer = styled('div')(() => {
export const StyledSettingKey = styled.div(({ theme }) => {
return {
display: 'flex',
alignItems: 'center',
};
});
export const StyledSettingH2 = styled('h2')<{
marginTop?: number;
marginBottom?: number;
}>(({ marginTop, marginBottom, theme }) => {
return {
fontWeight: '500',
width: '140px',
textAlign: 'right',
fontSize: theme.font.base,
lineHeight: theme.font.lineHeightBase,
marginTop: marginTop ? `${marginTop}px` : '0px',
marginBottom: marginBottom ? `${marginBottom}px` : '0px',
fontWeight: 500,
marginRight: '56px',
flexShrink: 0,
};
});
export const StyledRow = styled(FlexWrapper)(() => {
return {
marginBottom: '32px',
};
});
export const StyledPublishExplanation = styled('div')(() => {
return {
paddingRight: '48px',
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
marginBottom: '22px',
};
});
export const StyledWorkspaceName = styled('span')(() => {
export const StyledWorkspaceName = styled('span')(({ theme }) => {
return {
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
};
});
export const StyledWorkspaceType = styled('span')(() => {
return {
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
fontSize: theme.font.h6,
};
});
export const StyledPublishCopyContainer = styled('div')(() => {
export const StyledEmail = styled('span')(() => {
return {
marginTop: '12px',
marginBottom: '20px',
paddingTop: '20px',
flex: 1,
};
});
export const StyledStopPublishContainer = styled('div')(() => {
return {
textAlign: 'center',
color: '#E8178A',
};
});
export const StyledCopyButtonContainer = styled('div')(() => {
return {
marginTop: '64px',
};
});
export const StyledPublishContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'column',
};
});
export const StyleAsync = styled('div')(() => {
return {
marginTop: '64px',
};
});
// export const StyledDownloadCard = styled.div<{ active?: boolean }>(
// ({ theme, active }) => {
// return {
// width: '240px',
// height: '86px',
// border: '1px solid',
// borderColor: active
// ? theme.colors.primaryColor
// : theme.colors.borderColor,
// borderRadius: '10px',
// padding: '8px 12px',
// position: 'relative',
// ':not(:last-of-type)': {
// marginRight: '24px',
// },
// svg: {
// display: active ? 'block' : 'none',
// ...positionAbsolute({ top: '-12px', right: '-12px' }),
// },
// };
// }
// );
// export const StyledDownloadCardDes = styled.div(({ theme }) => {
// return {
// fontSize: theme.font.sm,
// color: theme.colors.iconColor,
// };
// });
@@ -1,4 +1,4 @@
import MuiAvatar from '@mui/material/Avatar';
import { MuiAvatar } from '@/ui/mui';
import { styled } from '@/styles';
export const WorkspaceItemWrapper = styled('div')(({ theme }) => ({
@@ -32,13 +32,13 @@ export const WorkspaceSelector = () => {
>
<WorkspaceUnitAvatar
size={28}
name={currentWorkspace?.name ?? 'AFFiNE'}
name={currentWorkspace?.name ?? 'AFFiNE Test'}
workspaceUnit={currentWorkspace}
/>
</div>
</Avatar>
<WorkspaceName data-testid="workspace-name">
{currentWorkspace?.name ?? 'AFFiNE'}
{currentWorkspace?.name ?? 'AFFiNE Test'}
</WorkspaceName>
</SelectorWrapper>
<WorkspaceModal
@@ -1,4 +1,4 @@
import MuiAvatar from '@mui/material/Avatar';
import { MuiAvatar } from '@/ui/mui';
import { styled } from '@/styles';
import { StyledPopperContainer } from '@/ui/shared/Container';
@@ -11,18 +11,17 @@ import {
StyledSubListItem,
} from './style';
import { Arrow } from './icons';
import Collapse from '@mui/material/Collapse';
import {
ArrowDownIcon,
SearchIcon,
AllPagesIcon,
FavouritesIcon,
ImportIcon,
TrashIcon,
AddIcon,
SettingsIcon,
} from '@blocksuite/icons';
import Link from 'next/link';
import { MuiCollapse } from '@/ui/mui';
import { Tooltip } from '@/ui/tooltip';
import { useModal } from '@/providers/GlobalModalProvider';
import { useAppState } from '@/providers/app-state-provider';
@@ -39,7 +38,7 @@ const FavoriteList = ({ showList }: { showList: boolean }) => {
const { t } = useTranslation();
const favoriteList = pageList.filter(p => p.favorite && !p.trash);
return (
<Collapse in={showList}>
<MuiCollapse in={showList}>
{favoriteList.map((pageMeta, index) => {
const active = router.query.pageId === pageMeta.id;
return (
@@ -61,11 +60,11 @@ const FavoriteList = ({ showList }: { showList: boolean }) => {
{favoriteList.length === 0 && (
<StyledSubListItem disable={true}>{t('No item')}</StyledSubListItem>
)}
</Collapse>
</MuiCollapse>
);
};
export const WorkSpaceSliderBar = () => {
const { triggerQuickSearchModal, triggerImportModal } = useModal();
const { triggerQuickSearchModal } = useModal();
const [showSubFavorite, setShowSubFavorite] = useState(true);
const { currentWorkspace } = useAppState();
const { openPage, createPage } = usePageHelper();
@@ -161,14 +160,14 @@ export const WorkSpaceSliderBar = () => {
setShowWorkspaceSetting(false);
}}
/> */}
<StyledListItem
{/* TODO: will finish the feature next version */}
{/* <StyledListItem
onClick={() => {
triggerImportModal();
}}
>
<ImportIcon /> {t('Import')}
</StyledListItem>
</StyledListItem> */}
<Link href={{ pathname: paths.trash }}>
<StyledListItem active={router.asPath === paths.trash}>
@@ -4,9 +4,9 @@ import Link from 'next/link';
export const StyledSliderBar = styled.div<{ show: boolean }>(
({ theme, show }) => {
return {
width: show ? '320px' : '0',
width: show ? '256px' : '0',
height: '100vh',
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
background: theme.mode === 'dark' ? '#272727' : '#F9F9FB',
boxShadow: theme.shadow.modal,
transition: 'width .15s, padding .15s',
position: 'relative',
@@ -58,7 +58,6 @@ export const StyledListItem = styled.div<{
width: '296px',
height: '32px',
marginTop: '12px',
fontSize: theme.font.sm,
color: active ? theme.colors.primaryColor : theme.colors.popoverColor,
paddingLeft: '12px',
borderRadius: '5px',
@@ -122,9 +121,8 @@ export const StyledSubListItem = styled.button<{
width: '296px',
height: '32px',
marginTop: '4px',
fontSize: theme.font.sm,
color: disable
? theme.colors.iconColor
? theme.colors.disableColor
: active
? theme.colors.primaryColor
: theme.colors.popoverColor,
@@ -38,7 +38,7 @@ export const useEnsureWorkspace = () => {
// }
const workspaceId =
(router.query.workspaceId as string) || workspaceList[0]?.id;
loadWorkspace(workspaceId).finally(() => {
loadWorkspace.current(workspaceId).finally(() => {
setWorkspaceLoaded(true);
setActiveWorkspaceId(activeWorkspaceId);
});
+15 -5
View File
@@ -1,4 +1,4 @@
import { uuidv4 } from '@blocksuite/store';
import { uuidv4, Workspace } from '@blocksuite/store';
import { QueryContent } from '@blocksuite/store/dist/workspace/search';
import { PageMeta, useAppState } from '@/providers/app-state-provider';
import { EditorContainer } from '@blocksuite/editor';
@@ -20,7 +20,10 @@ export type EditorHandlers = {
toggleDeletePage: (pageId: string) => Promise<boolean>;
toggleFavoritePage: (pageId: string) => Promise<boolean>;
permanentlyDeletePage: (pageId: string) => void;
search: (query: QueryContent) => Map<string, string | undefined>;
search: (
query: QueryContent,
workspace?: Workspace
) => Map<string, string | undefined>;
// changeEditorMode: (pageId: string) => void;
changePageMode: (
pageId: string,
@@ -83,9 +86,16 @@ export const usePageHelper = (): EditorHandlers => {
});
return trash;
},
search: (query: QueryContent) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return currentWorkspace!.blocksuiteWorkspace!.search(query);
search: (query: QueryContent, workspace?: Workspace) => {
if (workspace) {
return workspace.search(query);
}
if (currentWorkspace) {
if (currentWorkspace.blocksuiteWorkspace) {
return currentWorkspace.blocksuiteWorkspace.search(query);
}
}
return new Map();
},
changePageMode: async (pageId, mode) => {
const pageMeta = getPageMeta(currentWorkspace, pageId);
+3 -30
View File
@@ -1,12 +1,8 @@
import { useAppState } from '@/providers/app-state-provider';
import { useConfirm } from '@/providers/ConfirmProvider';
import { toast } from '@/ui/toast';
import { WorkspaceUnit } from '@affine/datacenter';
import router from 'next/router';
export const useWorkspaceHelper = () => {
const { confirm } = useConfirm();
const { dataCenter, currentWorkspace, user, login } = useAppState();
const { dataCenter, currentWorkspace } = useAppState();
const createWorkspace = async (name: string) => {
const workspaceInfo = await dataCenter.createWorkspace({
name: name,
@@ -31,33 +27,11 @@ export const useWorkspaceHelper = () => {
await dataCenter.updateWorkspaceMeta({ name }, workspace);
}
if (avatarBlob) {
// const blobId = await dataCenter.setBlob(workspace, avatarBlob);
// await dataCenter.updateWorkspaceMeta({ avatar: blobId }, workspace);
const blobId = await dataCenter.setBlob(workspace, avatarBlob);
await dataCenter.updateWorkspaceMeta({ avatar: blobId }, workspace);
}
};
const enableWorkspace = async () => {
confirm({
title: 'Enable AFFiNE Cloud?',
content: `If enabled, the data in this workspace will be backed up and synchronized via AFFiNE Cloud.`,
confirmText: user ? 'Enable' : 'Sign in and Enable',
cancelText: 'Skip',
confirmType: 'primary',
buttonDirection: 'column',
}).then(async confirm => {
if (confirm && currentWorkspace) {
if (!user) {
await login();
}
const workspace = await dataCenter.enableWorkspaceCloud(
currentWorkspace
);
workspace && router.push(`/workspace/${workspace.id}/setting`);
toast('Enabled success');
}
});
};
const deleteWorkSpace = async () => {
currentWorkspace && (await dataCenter.deleteWorkspace(currentWorkspace.id));
};
@@ -77,7 +51,6 @@ export const useWorkspaceHelper = () => {
createWorkspace,
publishWorkspace,
updateWorkspace,
enableWorkspace,
deleteWorkSpace,
leaveWorkSpace,
acceptInvite,
@@ -1,10 +0,0 @@
export const AlreadyJoined = () => {
return (
<div>
<p>
You are already a member of <code>Workspace name</code>
</p>
<button>Open Workspace</button>
</div>
);
};
@@ -1,11 +0,0 @@
export const Confirm = () => {
return (
<div>
<p>Inviter name</p>
<p>
invite you to join in <code>Workspace name</code>
</p>
<button>Join</button>
</div>
);
};
@@ -1,8 +0,0 @@
export const LinkExpired = () => {
return (
<div>
<h1>The current invitation link has expired.</h1>
<a href={location.origin}>Back to home</a>
</div>
);
};
@@ -1,21 +0,0 @@
import { useRouter } from 'next/router';
import { AlreadyJoined } from './AlreadyJoined';
import { Confirm } from './Confirm';
import { LinkExpired } from './LinkExpired';
export const ConfirmInvitation = () => {
const router = useRouter();
// Temporary code. The code should be returned by request.
const { code } = router.query;
const Component = {
'-1': LinkExpired,
0: Confirm,
1: AlreadyJoined,
}[code as string];
return (
<div>
<h1>Confirm Invitation</h1>
{Component ? <Component /> : null}
</div>
);
};
+7 -2
View File
@@ -67,15 +67,20 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => {
};
const AppDefender = ({ children }: PropsWithChildren) => {
const { synced } = useAppState();
const router = useRouter();
const { synced } = useAppState();
useEffect(() => {
if (router.pathname === '/') {
if (router.asPath === '/') {
router.replace('/workspace');
}
}, [router]);
// if you visit /404, you will see the children directly
if (router.route === '/404') {
return <div>{children}</div>;
}
return <div>{synced ? children : <PageLoading />}</div>;
};
+3 -2
View File
@@ -36,7 +36,6 @@ const Affine = () => {
<div>hi</div>
</Modal>
<Loading />
<Button
icon={<FavouritedIcon />}
onClick={() => {
@@ -48,13 +47,15 @@ const Affine = () => {
<Button icon={<FavouritedIcon />} type={'primary'}>
click me!
</Button>
<Button icon={<FavouritedIcon />} type={'light'}>
click me!
</Button>
<Button icon={<FavouritedIcon />} type={'warning'}>
click me!
</Button>
<Button icon={<FavouritedIcon />} type={'danger'}>
click me!
</Button>
<Button icon={<FavouritedIcon />}></Button>
<Button icon={<FavouritedIcon />} shape="round"></Button>
<Button loading={true}></Button>
@@ -1,8 +0,0 @@
import type { NextPage } from 'next';
import { ConfirmInvitation } from '../pages-content/ConfirmInvitationPage';
const ConfirmInvitationPage: NextPage = () => {
return <ConfirmInvitation />;
};
export default ConfirmInvitationPage;
@@ -1,9 +1,9 @@
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { styled } from '@/styles';
import { Empty } from '@/ui/empty';
// import { Avatar } from '@mui/material';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { PageLoading } from '@/components/loading';
// const User = ({ name, avatar }: { name: string; avatar?: string }) => {
// return (
@@ -19,19 +19,29 @@ import { useEffect, useState } from 'react';
// };
export default function DevPage() {
const [loading, setLoading] = useState(true);
const router = useRouter();
const [successInvited, setSuccessInvited] = useState<boolean>(false);
const { acceptInvite } = useWorkspaceHelper();
useEffect(() => {
router.query.invite_code &&
acceptInvite(router.query.invite_code as string).then(data => {
if (data && data.accepted) {
setSuccessInvited(true);
}
});
if (router.query.invite_code) {
acceptInvite(router.query.invite_code as string)
.then(data => {
if (data && data.accepted) {
setSuccessInvited(true);
}
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
}, [router, acceptInvite]);
return (
return loading ? (
<PageLoading />
) : (
<Invited>
<div>
<Empty width={310} height={310}></Empty>
@@ -1,11 +1,17 @@
import { ReactElement, useEffect, useState } from 'react';
import { useAppState } from '@/providers/app-state-provider';
import type { NextPageWithLayout } from '../..//_app';
import { styled } from '@/styles';
import { displayFlex, styled } from '@/styles';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import { Page as PageStore, Workspace } from '@blocksuite/store';
import { PageLoading } from '@/components/loading';
import { Breadcrumbs } from '@/ui/breadcrumbs';
import { IconButton } from '@/ui/button';
import NextLink from 'next/link';
import { PaperIcon, SearchIcon } from '@blocksuite/icons';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { useModal } from '@/providers/GlobalModalProvider';
const DynamicBlocksuite = dynamic(() => import('@/components/editor'), {
ssr: false,
@@ -16,12 +22,16 @@ const Page: NextPageWithLayout = () => {
const { dataCenter } = useAppState();
const router = useRouter();
const [loaded, setLoaded] = useState(false);
const [workspaceName, setWorkspaceName] = useState('');
const [pageTitle, setPageTitle] = useState('');
const { triggerQuickSearchModal } = useModal();
useEffect(() => {
dataCenter
.loadPublicWorkspace(router.query.workspaceId as string)
.then(data => {
if (data && data.blocksuiteWorkspace) {
setWorkspaceName(data.blocksuiteWorkspace?.meta.name as string);
if (data.blocksuiteWorkspace) {
setWorkspace(data.blocksuiteWorkspace);
if (
router.query.pageId &&
@@ -32,6 +42,7 @@ const Page: NextPageWithLayout = () => {
const page = data.blocksuiteWorkspace.getPage(
router.query.pageId as string
);
page && setPageTitle(page.meta.title);
page && setPage(page);
} else {
router.push('/404');
@@ -46,6 +57,30 @@ const Page: NextPageWithLayout = () => {
<>
{!loaded && <PageLoading />}
<PageContainer>
<NavContainer>
<Breadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${router.query.workspaceId}`}
>
<WorkspaceUnitAvatar size={24} name={workspaceName} />
<span>{workspaceName}</span>
</StyledBreadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${router.query.workspaceId}/${router.query.pageId}`}
>
<PaperIcon fontSize={24} />
<span>{pageTitle ? pageTitle : 'Untitled'}</span>
</StyledBreadcrumbs>
</Breadcrumbs>
<SearchButton
onClick={() => {
triggerQuickSearchModal();
}}
>
<SearchIcon />
</SearchButton>
</NavContainer>
{workspace && page && (
<DynamicBlocksuite
page={page}
@@ -69,9 +104,43 @@ export default Page;
export const PageContainer = styled.div(({ theme }) => {
return {
height: 'calc(100vh)',
padding: '78px 72px',
height: '100vh',
overflowY: 'auto',
backgroundColor: theme.colors.pageBackground,
};
});
export const NavContainer = styled.div(({ theme }) => {
return {
width: '100vw',
padding: '0 12px',
height: '60px',
...displayFlex('start', 'center'),
backgroundColor: theme.colors.pageBackground,
};
});
export const StyledBreadcrumbs = styled(NextLink)(({ theme }) => {
return {
flex: 1,
...displayFlex('center', 'center'),
paddingLeft: '12px',
span: {
padding: '0 12px',
fontSize: theme.font.base,
lineHeight: theme.font.lineHeight,
},
':hover': { color: theme.colors.primaryColor },
transition: 'all .15s',
':visited': {
color: theme.colors.popoverColor,
':hover': { color: theme.colors.primaryColor },
},
};
});
export const SearchButton = styled(IconButton)(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: '24px',
marginLeft: 'auto',
padding: '0 24px',
};
});
@@ -2,16 +2,28 @@ import { PageList } from '@/components/page-list';
import { ReactElement, useEffect, useState } from 'react';
import { PageMeta, useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
import { PageContainer } from './[pageId]';
import {
PageContainer,
NavContainer,
StyledBreadcrumbs,
SearchButton,
} from './[pageId]';
import { Breadcrumbs } from '@/ui/breadcrumbs';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { SearchIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/GlobalModalProvider';
const All = () => {
const { dataCenter } = useAppState();
const router = useRouter();
const [pageList, setPageList] = useState<PageMeta[]>([]);
const [workspaceName, setWorkspaceName] = useState('');
const { triggerQuickSearchModal } = useModal();
useEffect(() => {
dataCenter
.loadPublicWorkspace(router.query.workspaceId as string)
.then(data => {
setPageList(data.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]);
setWorkspaceName(data.blocksuiteWorkspace?.meta.name as string);
})
.catch(() => {
router.push('/404');
@@ -20,6 +32,23 @@ const All = () => {
return (
<PageContainer>
<NavContainer>
<Breadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${router.query.workspaceId}`}
>
<WorkspaceUnitAvatar size={24} name={workspaceName} />
<span>{workspaceName}</span>
</StyledBreadcrumbs>
</Breadcrumbs>
<SearchButton
onClick={() => {
triggerQuickSearchModal();
}}
>
<SearchIcon />
</SearchButton>
</NavContainer>
<PageList
pageList={pageList.filter(p => !p.trash)}
showFavoriteTag={false}
+3 -9
View File
@@ -1,14 +1,8 @@
debug-menu {
display: none !important;
}
.affine-block-children-container.edgeless {
background-color: #fff;
}
.affine-default-page-block-title-container {
margin-top: 78px;
margin-bottom: 40px;
}
.affine-editor-container {
background: transparent !important;
.affine-default-page-block-container {
width: 686px !important;
}
@@ -4,9 +4,11 @@ import { PageListHeader } from '@/components/header';
import { ReactElement } from 'react';
import WorkspaceLayout from '@/components/workspace-layout';
import { useTranslation } from '@affine/i18n';
import { useAppState } from '@/providers/app-state-provider';
import { PageMeta, useAppState } from '@/providers/app-state-provider';
const All = () => {
const { pageList } = useAppState();
const { currentWorkspace } = useAppState();
const pageList = (currentWorkspace?.blocksuiteWorkspace?.meta.pageMetas ||
[]) as PageMeta[];
const { t } = useTranslation();
return (
<>
@@ -10,8 +10,8 @@ import {
GeneralPage,
MembersPage,
PublishPage,
ExportPage,
SyncPage,
ExportPage,
} from '@/components/workspace-setting';
import { SettingsIcon } from '@blocksuite/icons';
import { useAppState } from '@/providers/app-state-provider';
@@ -20,41 +20,37 @@ import { WorkspaceUnit } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n';
import { PageListHeader } from '@/components/header';
type TabNames = 'General' | 'Sync' | 'Collaboration' | 'Publish' | 'Export';
const tabMap: {
name: TabNames;
panelRender: (workspace: WorkspaceUnit) => ReactNode;
}[] = [
{
name: 'General',
panelRender: workspace => <GeneralPage workspace={workspace} />,
},
{
name: 'Sync',
panelRender: workspace => <SyncPage workspace={workspace} />,
},
{
name: 'Collaboration',
panelRender: workspace => <MembersPage workspace={workspace} />,
},
{
name: 'Publish',
panelRender: workspace => <PublishPage workspace={workspace} />,
},
{
name: 'Export',
panelRender: workspace => <ExportPage workspace={workspace} />,
},
];
const WorkspaceSetting = () => {
const useTabMap = () => {
const { t } = useTranslation();
const { currentWorkspace, isOwner } = useAppState();
const [activeTab, setActiveTab] = useState<TabNames>(tabMap[0].name);
const handleTabChange = (tab: TabNames) => {
const { isOwner } = useAppState();
const tabMap: {
name: string;
panelRender: (workspace: WorkspaceUnit) => ReactNode;
}[] = [
{
name: t('General'),
panelRender: workspace => <GeneralPage workspace={workspace} />,
},
{
name: t('Sync'),
panelRender: workspace => <SyncPage workspace={workspace} />,
},
{
name: t('Collaboration'),
panelRender: workspace => <MembersPage workspace={workspace} />,
},
{
name: t('Publish'),
panelRender: workspace => <PublishPage workspace={workspace} />,
},
// TODO: next version will finish this feature
{
name: t('Export'),
panelRender: workspace => <ExportPage workspace={workspace} />,
},
];
const [activeTab, setActiveTab] = useState<string>(tabMap[0].name);
const handleTabChange = (tab: string) => {
setActiveTab(tab);
};
@@ -62,7 +58,7 @@ const WorkspaceSetting = () => {
tab => tab.name === activeTab
)?.panelRender;
let tableArr: {
name: TabNames;
name: string;
panelRender: (workspace: WorkspaceUnit) => ReactNode;
}[] = tabMap;
if (!isOwner) {
@@ -73,6 +69,14 @@ const WorkspaceSetting = () => {
},
];
}
return { activeTabPanelRender, tableArr, handleTabChange, activeTab };
};
const WorkspaceSetting = () => {
const { t } = useTranslation();
const { currentWorkspace } = useAppState();
const { activeTabPanelRender, tableArr, handleTabChange, activeTab } =
useTabMap();
return (
<>
<StyledSettingContainer>
@@ -10,16 +10,21 @@ import {
import { createDefaultWorkspace } from './utils';
import { User } from '@affine/datacenter';
export interface Disposable {
dispose(): void;
}
type AppStateContextProps = PropsWithChildren<Record<string, unknown>>;
export const AppState = createContext<AppStateContext>({} as AppStateContext);
export const useAppState = () => useContext(AppState);
export const AppStateProvider = ({
children,
}: PropsWithChildren<AppStateContextProps>) => {
const [appState, setAppState] = useState<AppStateValue>({} as AppStateValue);
const [blobState, setBlobState] = useState(false);
const [userInfo, setUser] = useState<User | null>({} as User);
useEffect(() => {
const initState = async () => {
const dataCenter = await getDataCenter();
@@ -27,10 +32,9 @@ export const AppStateProvider = ({
if (dataCenter.workspaces.length === 0) {
await createDefaultWorkspace(dataCenter);
}
setUser((await dataCenter.getUserInfo()) || null);
setAppState({
dataCenter,
user: (await dataCenter.getUserInfo()) || null,
workspaceList: dataCenter.workspaces,
currentWorkspace: null,
pageList: [],
@@ -90,15 +94,17 @@ export const AppStateProvider = ({
});
};
const loadWorkspace = useRef<AppStateFunction['loadWorkspace']>();
const loadWorkspace: AppStateFunction['loadWorkspace'] =
useRef() as AppStateFunction['loadWorkspace'];
loadWorkspace.current = async (workspaceId: string) => {
const { dataCenter, workspaceList, currentWorkspace, user } = appState;
const { dataCenter, workspaceList, currentWorkspace } = appState;
if (!workspaceList.find(v => v.id.toString() === workspaceId)) {
return null;
}
if (workspaceId === currentWorkspace?.id) {
return currentWorkspace;
}
const workspace = (await dataCenter.loadWorkspace(workspaceId)) ?? null;
let isOwner;
if (workspace?.provider === 'local') {
@@ -106,8 +112,9 @@ export const AppStateProvider = ({
isOwner = true;
} else {
// We must ensure workspace.owner exists, then ensure id same.
isOwner = workspace?.owner && user?.id === workspace.owner.id;
isOwner = workspace?.owner && userInfo?.id === workspace.owner.id;
}
const pageList =
(workspace?.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? [];
setAppState({
@@ -122,6 +129,26 @@ export const AppStateProvider = ({
return workspace;
};
useEffect(() => {
let syncChangeDisposable: Disposable | undefined;
const currentWorkspace = appState.currentWorkspace;
if (!currentWorkspace) {
return;
}
const getBlobStorage = async () => {
const blobStorage = await currentWorkspace?.blocksuiteWorkspace?.blobs;
syncChangeDisposable = blobStorage?.signals.onBlobSyncStateChange.on(
() => {
setBlobState(blobStorage?.uploading);
}
);
};
getBlobStorage();
return () => {
syncChangeDisposable?.dispose();
};
}, [appState.currentWorkspace]);
const setEditor: AppStateFunction['setEditor'] =
useRef() as AppStateFunction['setEditor'];
setEditor.current = editor => {
@@ -139,20 +166,13 @@ export const AppStateProvider = ({
if (!user) {
throw new Error('User info not found');
}
setAppState({
...appState,
user,
});
setUser(user);
return user;
};
const logout = async () => {
const { dataCenter } = appState;
await dataCenter.logout();
setAppState({
...appState,
user: null,
});
setUser(null);
};
return (
@@ -161,9 +181,11 @@ export const AppStateProvider = ({
...appState,
setEditor,
loadPage: loadPage.current,
loadWorkspace: loadWorkspace.current,
loadWorkspace: loadWorkspace,
login,
logout,
blobDataSynced: blobState,
user: userInfo,
}}
>
{children}
@@ -16,7 +16,7 @@ export interface PageMeta extends StorePageMeta {
export type AppStateValue = {
dataCenter: DataCenter;
user: User | null;
user?: User | null;
workspaceList: WorkspaceUnit[];
currentWorkspace: WorkspaceUnit | null;
pageList: PageMeta[];
@@ -24,12 +24,15 @@ export type AppStateValue = {
editor?: EditorContainer | null;
synced: boolean;
isOwner?: boolean;
blobDataSynced?: boolean;
};
export type AppStateFunction = {
setEditor: MutableRefObject<(page: EditorContainer) => void>;
loadWorkspace: (workspaceId: string) => Promise<WorkspaceUnit | null>;
loadWorkspace: MutableRefObject<
(workspaceId: string) => Promise<WorkspaceUnit | null>
>;
loadPage: (pageId: string) => void;
login: () => Promise<User>;
@@ -1,6 +1,6 @@
import { DataCenter } from '@affine/datacenter';
const DEFAULT_WORKSPACE_NAME = 'affine';
const DEFAULT_WORKSPACE_NAME = 'AFFiNE Test';
export const createDefaultWorkspace = async (dataCenter: DataCenter) => {
return dataCenter.createWorkspace({
+28 -2
View File
@@ -2,12 +2,12 @@ import type { CSSProperties } from 'react';
export const displayFlex = (
justifyContent: CSSProperties['justifyContent'] = 'unset',
alignItems: CSSProperties['alignContent'] = 'unset',
alignItems: CSSProperties['alignItems'] = 'unset',
alignContent: CSSProperties['alignContent'] = 'unset'
): {
display: CSSProperties['display'];
justifyContent: CSSProperties['justifyContent'];
alignItems: CSSProperties['alignContent'];
alignItems: CSSProperties['alignItems'];
alignContent: CSSProperties['alignContent'];
} => {
return {
@@ -117,3 +117,29 @@ export const textEllipsis = (lineNum = 1): CSSProperties => {
whiteSpace: 'nowrap',
};
};
export const positionAbsolute = ({
left,
top,
right,
bottom,
}: {
left?: CSSProperties['left'];
top?: CSSProperties['top'];
right?: CSSProperties['right'];
bottom?: CSSProperties['bottom'];
}): {
position: CSSProperties['position'];
left: CSSProperties['left'];
top: CSSProperties['top'];
right: CSSProperties['right'];
bottom: CSSProperties['bottom'];
} => {
return {
position: 'absolute',
left,
top,
right,
bottom,
};
};
+43 -23
View File
@@ -16,17 +16,20 @@ export const getLightTheme = (
pageBackground: '#fff',
hoverBackground: '#F1F3FF',
innerHoverBackground: '#E0E6FF',
innerHoverBackground: '#E9E9EC',
popoverBackground: '#fff',
tooltipBackground: '#6880FF',
codeBackground: '#f2f5f9',
codeBlockBackground: '#fafbfd',
codeBlockBackground: '#F9F9FB',
blockHubBackground: '#fbfbfc',
blockHubHoverBackground: '#f8f9ff',
warningBackground: '#FFF9C7',
errorBackground: '#FFDED8',
textColor: '#3A4C5C',
textColor: '#28293D',
edgelessTextColor: '#3A4C5C',
iconColor: '#888a9e',
iconColor: '#555770',
handleColor: '#c7c3d9',
linkColor: '#6880FF',
linkColor2: '#6880FF',
linkVisitedColor: '#ABB8FE',
@@ -35,28 +38,30 @@ export const getLightTheme = (
tooltipColor: '#fff',
codeColor: '#517ea6',
quoteColor: '#4C6275',
placeHolderColor: '#C7C7C7',
placeHolderColor: '#888A9E',
selectedColor: 'rgba(104, 128, 255, 0.1)',
borderColor: '#D0D7E3',
disableColor: '#C0C0C0',
borderColor: '#E3E2E4',
disableColor: '#555770',
warningColor: '#906616',
errorColor: '#EB4335',
lineNumberColor: '#888A9E',
lineNumberColor: '#555770',
},
font: {
xs: '12px',
sm: '16px',
base: '18px',
h1: '30px',
h2: '28px',
h3: '26px',
h4: '24px',
h5: '22px',
h6: '20px',
title: '36px',
h1: '28px',
h2: '26px',
h3: '24px',
h4: '22px',
h5: '20px',
h6: '18px',
base: '16px',
xs: '14px',
sm: '12px',
family: `Avenir Next, Poppins, ${basicFontFamily}`,
mono: `Space Mono, ${basicFontFamily}`,
lineHeightBase: '26px',
numberFamily: `Roboto Mono, ${basicFontFamily}`,
codeFamily: `Space Mono, Consolas, Menlo, Monaco, Courier, monospace, ${basicFontFamily}`,
lineHeight: 'calc(1em + 8px)',
},
zIndex: {
modal: 1000,
@@ -99,12 +104,15 @@ export const getDarkTheme = (
? lightTheme.colors.codeBackground
: '#505662',
codeBlockBackground: '#36383D',
blockHubBackground: '#272727',
blockHubHoverBackground: '#363636',
warningBackground: '#FFF9C7',
errorBackground: '#FFDED8',
textColor: '#fff',
edgelessTextColor: '#3A4C5C',
iconColor: '#888a9e',
handleColor: '#c7c3d9',
linkColor: '#7D91FF',
linkColor2: '#6880FF',
linkVisitedColor: '#505FAB',
@@ -148,6 +156,9 @@ export const globalThemeVariables: (
'--affine-code-background': theme.colors.codeBackground,
'--affine-tooltip-background': theme.colors.tooltipBackground,
'--affine-block-hub-background': theme.colors.blockHubBackground,
'--affine-block-hub-hover-background': theme.colors.blockHubHoverBackground,
'--affine-text-color': theme.colors.textColor,
'--affine-edgeless-text-color': theme.colors.edgelessTextColor,
'--affine-link-color': theme.colors.linkColor,
@@ -155,6 +166,7 @@ export const globalThemeVariables: (
'--affine-link-color2': theme.colors.linkColor2,
'--affine-link-visited-color': theme.colors.linkVisitedColor,
'--affine-icon-color': theme.colors.iconColor,
'--affine-block-handle-color': theme.colors.handleColor,
'--affine-popover-color': theme.colors.popoverColor,
'--affine-input-color': theme.colors.inputColor,
'--affine-code-color': theme.colors.codeColor,
@@ -171,16 +183,24 @@ export const globalThemeVariables: (
'--affine-popover-shadow': theme.shadow.popover,
'--affine-tooltip-shadow': theme.shadow.tooltip,
'--affine-font-xs': theme.font.xs, // tiny
'--affine-font-sm': theme.font.sm, // small
'--affine-font-h1': theme.font.h1,
'--affine-font-h2': theme.font.h2,
'--affine-font-h3': theme.font.h3,
'--affine-font-h4': theme.font.h4,
'--affine-font-h5': theme.font.h5,
'--affine-font-h6': theme.font.h6,
'--affine-font-base': theme.font.base,
'--affine-line-height-base': theme.font.lineHeightBase,
'--affine-font-sm': theme.font.sm, // small
'--affine-font-xs': theme.font.xs, // tiny
'--affine-line-height': theme.font.lineHeight,
'--affine-z-index-modal': theme.zIndex.modal,
'--affine-z-index-popover': theme.zIndex.popover,
'--affine-font-family': theme.font.family,
'--affine-font-mono': theme.font.mono,
'--affine-font-number-family': theme.font.numberFamily,
'--affine-font-code-family': theme.font.codeFamily,
'--affine-paragraph-space': theme.space.paragraph,
'--affine-popover-radius': theme.radius.popover,
+21 -10
View File
@@ -26,6 +26,8 @@ export interface AffineTheme {
innerHoverBackground: string;
codeBackground: string;
codeBlockBackground: string;
blockHubBackground: string;
blockHubHoverBackground: string;
warningBackground: string;
errorBackground: string;
// Use for the page`s text
@@ -37,6 +39,7 @@ export interface AffineTheme {
linkColor2: string;
linkVisitedColor: string;
iconColor: string;
handleColor: string;
popoverColor: string;
inputColor: string;
tooltipColor: string;
@@ -51,20 +54,22 @@ export interface AffineTheme {
lineNumberColor: string;
};
font: {
xs: string; // tiny
sm: string; // small
base: string;
title: string;
h1: string;
h2: string;
h3: string;
h4: string;
h5: string;
h6: string;
base: string;
sm: string; // small
xs: string; // tiny
family: string;
mono: string;
numberFamily: string;
codeFamily: string;
lineHeightBase: string | number;
lineHeight: string | number;
};
zIndex: {
modal: number;
@@ -116,17 +121,23 @@ export interface AffineThemeCSSVariables {
'--affine-modal-shadow': AffineTheme['shadow']['modal'];
'--affine-popover-shadow': AffineTheme['shadow']['popover'];
'--affine-tooltip-shadow': AffineTheme['shadow']['tooltip'];
'--affine-font-xs': AffineTheme['font']['xs']; // tiny
'--affine-font-sm': AffineTheme['font']['sm']; // small
'--affine-font-h1': AffineTheme['font']['h1'];
'--affine-font-h2': AffineTheme['font']['h2'];
'--affine-font-h3': AffineTheme['font']['h3'];
'--affine-font-h4': AffineTheme['font']['h4'];
'--affine-font-h5': AffineTheme['font']['h5'];
'--affine-font-h6': AffineTheme['font']['h6'];
'--affine-font-base': AffineTheme['font']['base'];
'--affine-line-height-base': AffineTheme['font']['lineHeightBase'];
'--affine-font-sm': AffineTheme['font']['sm']; // small
'--affine-font-xs': AffineTheme['font']['xs']; // tiny
'--affine-line-height': AffineTheme['font']['lineHeight'];
'--affine-z-index-modal': AffineTheme['zIndex']['modal'];
'--affine-z-index-popover': AffineTheme['zIndex']['popover'];
'--affine-font-family': AffineTheme['font']['family'];
'--affine-font-mono': AffineTheme['font']['mono'];
'--affine-font-number-family': AffineTheme['font']['numberFamily'];
'--affine-font-code-family': AffineTheme['font']['codeFamily'];
'--affine-paragraph-space': AffineTheme['space']['paragraph'];
@@ -1,24 +0,0 @@
We've been working hard on improving the user experience and have made several new features live for you to use.
If you are looking for **support**, would like to **suggest** your ideas, and keep on top of all the **latest updates**. We suggest you visit the [AFFiNE Community](https://community.affine.pro/home) site.
Let us know what you think of this latest version.
**AFFiNE Alpha Downhill brings:**
We are mainly working on features for pathfinder.
- Support SlashMenu #629
- Support BlockHub + DragHandle #630
- Support Whiteboard with basic usability #631
- Support bracket autocomplete #632 @lawvs
- Support Database #633
- Support Account(Google Login) #608 @alt1o @darkskygit
- Support AFFiNE Collaboration by AFFiNE Cloud #609 @darkskygit @zuoxiaodong0815
- Support Local First Multi Workspace #610 @QiShaoXuan @darkskygit
- Support AFFiNE Cloud Sync #611 @alt1o @SaikaSakura @darkskygit
- Support Download Workspace by AFFiNE Cloud #612 @darkskygit @alt1o @SaikaSakura
- Support Workspace Sharing by AFFiNE Cloud #613 @alt1o @SaikaSakura
- Import Markdown #615 @QiShaoXuan @thorseraq
- [improvement]: i18n integration #567 @JimmFly
- Support DataCEnter @SaikaSakura @alt1o #708
@@ -0,0 +1,24 @@
We've been working hard on improving the user experience and have made several new features live for you to use.
If you are looking for **support**, would like to **suggest** your ideas, and keep on top of all the **latest updates**. We suggest you visit the [AFFiNE Community](https://community.affine.pro/home) site.
Let us know what you think of this latest version.
**AFFiNE Alpha Downhills brings:**
We are mainly working on features for pathfinder.
- [feat] SlashMenu #629
- [feat] BlockHub + DragHandle #630
- [feat] edgeless with basic usability #631
- [feat] bracket autocomplete #632 @lawvs
- [feat] database #633
- [feat] login with Google #608 @alt1o @darkskygit
- [feat] collaboration by AFFiNE Cloud #609 @darkskygit @zuoxiaodong0815
- [feat] local first multi workspace #610 @QiShaoXuan @darkskygit
- [feat] AFFiNE Cloud sync #611 @alt1o @SaikaSakura @darkskygit
- [feat] download workspace by AFFiNE Cloud #612 @darkskygit @alt1o @SaikaSakura
- [feat] workspace sharing with AFFiNE Cloud #613 @alt1o @SaikaSakura
- [feat] import markdown #615 @QiShaoXuan @thorseraq
- [feat] i18n integration #567 @JimmFly
- [feat] implement cloud provider for DataCenter @SaikaSakura @alt1o #708
+8
View File
@@ -0,0 +1,8 @@
import MuiBreadcrumbs from '@mui/material/Breadcrumbs';
import { styled } from '@/styles';
export const Breadcrumbs = styled(MuiBreadcrumbs)(({ theme }) => {
return {
color: theme.colors.popoverColor,
};
});
+13 -6
View File
@@ -15,16 +15,26 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
hoverStyle,
shape = 'default',
icon,
iconPosition = 'start',
type = 'default',
children,
bold = false,
loading = false,
noBorder = false,
...props
},
ref
) => {
const { iconSize } = getSize(size);
const iconElement =
icon &&
cloneElement(Children.only(icon), {
width: iconSize,
height: iconSize,
className: `affine-button-icon ${icon.props.className ?? ''}`,
});
return (
<StyledButton
ref={ref}
@@ -38,19 +48,16 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
// @ts-ignore
type={type}
bold={bold}
noBorder={noBorder}
{...props}
>
{loading ? (
<Loading type={type}></Loading>
) : (
<>
{icon &&
cloneElement(Children.only(icon), {
width: iconSize,
height: iconSize,
className: `affine-button-icon ${icon.props.className ?? ''}`,
})}
{iconPosition === 'start' && iconElement}
{children && <span>{children}</span>}
{iconPosition === 'end' && iconElement}
</>
)}
</StyledButton>
+1 -1
View File
@@ -4,7 +4,7 @@ import { getButtonColors } from './utils';
export const LoadingContainer = styled('div')<Pick<ButtonProps, 'type'>>(
({ theme, type = 'default' }) => {
const { color } = getButtonColors(theme, type);
const { color } = getButtonColors(theme, type, false);
return `
margin: 0px auto;
width: 38px;
+3 -1
View File
@@ -17,8 +17,10 @@ export type ButtonProps = PropsWithChildren &
hoverColor?: CSSProperties['color'];
hoverStyle?: CSSProperties;
icon?: ReactElement;
iconPosition?: 'start' | 'end';
shape?: 'default' | 'round' | 'circle';
type?: 'primary' | 'warning' | 'danger' | 'default';
type?: 'primary' | 'light' | 'warning' | 'danger' | 'default';
bold?: boolean;
loading?: boolean;
noBorder?: boolean;
};
+9 -2
View File
@@ -145,10 +145,12 @@ export const StyledButton = styled('button', {
shouldForwardProp: prop => {
return ![
'hoverBackground',
'shape',
'hoverColor',
'hoverStyle',
'type',
'bold',
'noBorder',
].includes(prop);
},
})<
@@ -162,6 +164,7 @@ export const StyledButton = styled('button', {
| 'shape'
| 'type'
| 'bold'
| 'noBorder'
>
>(
({
@@ -174,6 +177,7 @@ export const StyledButton = styled('button', {
bold = false,
shape = 'default',
type = 'default',
noBorder = false,
}) => {
const { fontSize, borderRadius, padding, height } = getSize(size);
@@ -181,7 +185,7 @@ export const StyledButton = styled('button', {
height,
paddingLeft: padding,
paddingRight: padding,
border: '1px solid',
border: noBorder ? 'none' : '1px solid',
...displayInlineFlex('center', 'center'),
position: 'relative',
// TODO: disabled color is not decided
@@ -200,13 +204,16 @@ export const StyledButton = styled('button', {
'.affine-button-icon': {
color: theme.colors.iconColor,
},
'.affine-button-icon__fixed': {
color: theme.colors.iconColor,
},
'>span': {
marginLeft: '5px',
width: '100%',
},
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
...getButtonColors(theme, type, {
...getButtonColors(theme, type, disabled, {
hoverBackground,
hoverColor,
hoverStyle,
+13
View File
@@ -40,6 +40,7 @@ export const getSize = (
export const getButtonColors = (
theme: AffineTheme,
type: ButtonProps['type'],
disabled: boolean,
extend?: {
hoverBackground: ButtonProps['hoverBackground'];
hoverColor: ButtonProps['hoverColor'];
@@ -56,6 +57,18 @@ export const getButtonColors = (
color: '#fff',
},
};
case 'light':
return {
background: theme.colors.hoverBackground,
color: disabled ? theme.colors.disableColor : theme.colors.primaryColor,
borderColor: theme.colors.hoverBackground,
'.affine-button-icon': {
borderColor: theme.colors.primaryColor,
},
':hover': {
borderColor: theme.colors.primaryColor,
},
};
case 'warning':
return {
background: theme.colors.warningBackground,
+2 -1
View File
@@ -38,8 +38,9 @@ export const Input = (props: inputProps) => {
if (
(maxLength && e.target.value.length > maxLength) ||
(minLength && e.target.value.length < minLength)
)
) {
return;
}
setValue(e.target.value);
onChange && onChange(e.target.value);
};
+15 -2
View File
@@ -14,8 +14,21 @@ export type ContentProps = {
lineNum?: number;
children: string;
};
export const Content = styled.div<ContentProps>(
export const Content = styled('div', {
shouldForwardProp: prop => {
return ![
'color',
'fontSize',
'weight',
'lineHeight',
'ellipsis',
'lineNum',
'width',
'maxWidth',
'align',
].includes(prop);
},
})<ContentProps>(
({
theme,
color,
+85 -8
View File
@@ -2,6 +2,22 @@ import type { CSSProperties } from 'react';
import { styled } from '@/styles';
export type WrapperProps = {
display?: CSSProperties['display'];
width?: CSSProperties['width'];
height?: CSSProperties['height'];
padding?: CSSProperties['padding'];
paddingTop?: CSSProperties['paddingTop'];
paddingRight?: CSSProperties['paddingRight'];
paddingLeft?: CSSProperties['paddingLeft'];
paddingBottom?: CSSProperties['paddingBottom'];
margin?: CSSProperties['margin'];
marginTop?: CSSProperties['marginTop'];
marginLeft?: CSSProperties['marginLeft'];
marginRight?: CSSProperties['marginRight'];
marginBottom?: CSSProperties['marginBottom'];
};
export type FlexWrapperProps = {
display?: CSSProperties['display'];
flexDirection?: CSSProperties['flexDirection'];
justifyContent?: CSSProperties['justifyContent'];
@@ -16,6 +32,57 @@ export const Wrapper = styled('div', {
shouldForwardProp: prop => {
return ![
'display',
'width',
'height',
'padding',
'margin',
'paddingTop',
'paddingRight',
'paddingLeft',
'paddingBottom',
'marginTop',
'marginLeft',
'marginRight',
'marginBottom',
].includes(prop);
},
})<WrapperProps>(
({
display,
width,
height,
padding,
margin,
paddingTop,
paddingRight,
paddingLeft,
paddingBottom,
marginTop,
marginLeft,
marginRight,
marginBottom,
}) => {
return {
display,
width,
height,
padding,
margin,
paddingTop,
paddingRight,
paddingLeft,
paddingBottom,
marginTop,
marginLeft,
marginRight,
marginBottom,
};
}
);
export const FlexWrapper = styled(Wrapper, {
shouldForwardProp: prop => {
return ![
'justifyContent',
'alignItems',
'wrap',
@@ -24,18 +91,17 @@ export const Wrapper = styled('div', {
'flexGrow',
].includes(prop);
},
})<WrapperProps>(
})<FlexWrapperProps>(
({
display = 'flex',
justifyContent = 'flex-start',
alignItems = 'center',
justifyContent,
alignItems,
wrap = false,
flexDirection = 'row',
flexShrink = '0',
flexGrow = '0',
flexDirection,
flexShrink,
flexGrow,
}) => {
return {
display,
display: 'flex',
justifyContent,
alignItems,
flexWrap: wrap ? 'wrap' : 'nowrap',
@@ -46,4 +112,15 @@ export const Wrapper = styled('div', {
}
);
// TODO: Complete me
export const GridWrapper = styled(Wrapper, {
shouldForwardProp: prop => {
return ![''].includes(prop);
},
})<WrapperProps>(() => {
return {
display: 'grid',
};
});
export default Wrapper;
+12 -4
View File
@@ -5,6 +5,7 @@ import { styled } from '@/styles';
export type ModalCloseButtonProps = {
top?: number;
right?: number;
absolute?: boolean;
} & Omit<IconButtonProps, 'children'> &
HTMLAttributes<HTMLButtonElement>;
@@ -13,16 +14,23 @@ const StyledIconButton = styled(IconButton)<
>(({ top, right }) => {
return {
position: 'absolute',
top: top ?? 6,
right: right ?? 6,
top: top ?? 24,
right: right ?? 40,
};
});
export const ModalCloseButton = ({ ...props }: ModalCloseButtonProps) => {
return (
export const ModalCloseButton = ({
absolute = true,
...props
}: ModalCloseButtonProps) => {
return absolute ? (
<StyledIconButton {...props}>
<CloseIcon />
</StyledIconButton>
) : (
<IconButton {...props}>
<CloseIcon />
</IconButton>
);
};
+1 -1
View File
@@ -11,7 +11,7 @@ export const ModalWrapper = styled.div<{
height,
minHeight,
backgroundColor: theme.colors.popoverBackground,
borderRadius: '12px',
borderRadius: '24px',
position: 'relative',
};
});
+7
View File
@@ -0,0 +1,7 @@
import MuiBreadcrumbs from '@mui/material/Breadcrumbs';
import MuiCollapse from '@mui/material/Collapse';
import MuiSlide from '@mui/material/Slide';
import MuiAvatar from '@mui/material/Avatar';
import MuiGrow from '@mui/material/Grow';
export { MuiBreadcrumbs, MuiCollapse, MuiSlide, MuiAvatar, MuiGrow };
+5
View File
@@ -152,6 +152,11 @@ export const Popper = ({
onPointerLeave={onPointerLeaveHandler}
style={popoverStyle}
className={popoverClassName}
onClick={() => {
if (hasClickTrigger && !visibleControlledByParent) {
setVisible(false);
}
}}
>
{showArrow && (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+1
View File
@@ -5,6 +5,7 @@ import type { TooltipProps } from '@mui/material';
const StyledTooltip = styled(StyledPopperContainer)(({ theme }) => {
return {
maxWidth: '320px',
boxShadow: theme.shadow.tooltip,
padding: '4px 12px',
backgroundColor: theme.colors.tooltipBackground,