Merge remote-tracking branch 'refs/remotes/origin/feat/datacenter'

Conflicts:
	packages/app/src/components/workspace-modal/languageMenu.tsx
	packages/data-center/package.json
	pnpm-lock.yaml
This commit is contained in:
linonetwo
2023-01-30 11:00:31 +08:00
116 changed files with 2392 additions and 3945 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1,2 +1,3 @@
**/project.json @darkskygit **/project.json @darkskygit
**/pnpm-lock.yaml @darkskygit **/pnpm-lock.yaml @darkskygit
**/en.json @JimmFly

63
.github/workflows/languages-sync.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Languages Sync
on:
push:
branches: ['master']
paths:
- 'packages/i18n/**'
- '.github/workflows/languages-sync.yml'
pull_request:
branches: ['master']
paths:
- 'packages/i18n/**'
- '.github/workflows/languages-sync.yml'
workflow_dispatch:
# Cancels all previous workflow runs for pull requests that have not completed.
# See https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency:
# The concurrency group contains the workflow name and the branch name for
# pull requests or the commit hash for any other events.
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: true
jobs:
main:
strategy:
matrix:
node-version: [18]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use pnpm
uses: pnpm/action-setup@v2
with:
version: 7
- name: Use Node.js ${{ matrix.node-version }}
# https://github.com/actions/setup-node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install node modules
run: pnpm install
- name: Check Language Key
if: github.ref != 'refs/heads/master'
working-directory: ./packages/i18n
run: pnpm run sync-languages:check
env:
TOLGEE_API_KEY: ${{ secrets.TOLGEE_API_KEY }}
- name: Sync Languages
if: github.ref == 'refs/heads/master'
working-directory: ./packages/i18n
run: pnpm run sync-languages
env:
TOLGEE_API_KEY: ${{ secrets.TOLGEE_API_KEY }}

View File

@@ -20,7 +20,8 @@
"test:unit": "playwright test --config=playwright.config.unit.ts", "test:unit": "playwright test --config=playwright.config.unit.ts",
"postinstall": "husky install", "postinstall": "husky install",
"notify": "node --experimental-modules scripts/notify.mjs", "notify": "node --experimental-modules scripts/notify.mjs",
"check:ci": "pnpm lint & pnpm test" "check:ci": "pnpm lint & pnpm test",
"update:blocksuite": "pnpm i --filter @affine/app --filter @affine/datacenter @blocksuite/blocks@nightly @blocksuite/store@nightly && pnpm i --filter @affine/app @blocksuite/editor@nightly"
}, },
"lint-staged": { "lint-staged": {
"*": "prettier --write --ignore-unknown", "*": "prettier --write --ignore-unknown",

View File

@@ -12,11 +12,12 @@ const EDITOR_VERSION = enableDebugLocal
const profileTarget = { const profileTarget = {
ac: '100.85.73.88:12001', ac: '100.85.73.88:12001',
dev: '100.77.180.48:11001', dev: '100.77.180.48:11001',
test: '100.77.180.48:11001',
stage: '',
pro: 'http://pathfinder.affine.pro',
local: '127.0.0.1:3000', local: '127.0.0.1:3000',
}; };
// 100.77.180.48:11001
const getRedirectConfig = profile => { const getRedirectConfig = profile => {
const target = profileTarget[profile || 'dev'] || profileTarget['dev']; const target = profileTarget[profile || 'dev'] || profileTarget['dev'];

View File

@@ -11,10 +11,10 @@
"dependencies": { "dependencies": {
"@affine/datacenter": "workspace:*", "@affine/datacenter": "workspace:*",
"@affine/i18n": "workspace:*", "@affine/i18n": "workspace:*",
"@blocksuite/blocks": "0.3.1-20230109032243-37ad3ba", "@blocksuite/blocks": "0.4.0-20230111171650-bc63456",
"@blocksuite/editor": "0.3.1-20230109032243-37ad3ba", "@blocksuite/editor": "0.4.0-20230111171650-bc63456",
"@blocksuite/icons": "^2.0.2", "@blocksuite/icons": "^2.0.2",
"@blocksuite/store": "0.3.1-20230109032243-37ad3ba", "@blocksuite/store": "0.4.0-20230111171650-bc63456",
"@emotion/css": "^11.10.0", "@emotion/css": "^11.10.0",
"@emotion/react": "^11.10.4", "@emotion/react": "^11.10.4",
"@emotion/server": "^11.10.0", "@emotion/server": "^11.10.0",

View File

@@ -166,9 +166,9 @@ a:visited {
input { input {
border: none; border: none;
-moz-appearance: none; -moz-appearance: none;
-webkit-appearance: none; /*解决ios上按钮的圆角问题*/ -webkit-appearance: none; /*Solve the rounded corners of buttons on ios*/
border-radius: 0; /*解决ios上输入框圆角问题*/ border-radius: 0; /*Solve the problem of rounded corners of the input box on ios*/
outline: medium; /*去掉鼠标点击的默认黄色边框*/ outline: medium; /*Remove the default yellow border on mouse click*/
background-color: transparent; background-color: transparent;
} }

View File

@@ -1,10 +1,24 @@
import { NotFoundTitle, PageContainer } from './styles'; import { NotFoundTitle, PageContainer } from './styles';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { Button } from '@/ui/button';
import { useRouter } from 'next/router';
export const NotfoundPage = () => { export const NotfoundPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter();
return ( return (
<PageContainer> <PageContainer>
<NotFoundTitle>{t('404 - Page Not Found')}</NotFoundTitle> <NotFoundTitle>
{t('404 - Page Not Found')}
<p>
<Button
onClick={() => {
router.push('/workspace');
}}
>
Back Home
</Button>
</p>
</NotFoundTitle>
</PageContainer> </PageContainer>
); );
}; };

View File

@@ -76,6 +76,8 @@ export const ContactModal = ({
link: 'https://community.affine.pro', link: 'https://community.affine.pro',
}, },
]; ];
const date = new Date();
const year = date.getFullYear();
return ( return (
<Modal open={open} onClose={onClose} data-testid="contact-us-modal-content"> <Modal open={open} onClose={onClose} data-testid="contact-us-modal-content">
<ModalWrapper <ModalWrapper
@@ -135,7 +137,7 @@ export const ContactModal = ({
{t('How is AFFiNE Alpha different?')} {t('How is AFFiNE Alpha different?')}
</a> </a>
</p> </p>
<p>Copyright &copy; 2022 Toeverything</p> <p>Copyright &copy; {year} Toeverything</p>
</StyledModalFooter> </StyledModalFooter>
</ModalWrapper> </ModalWrapper>
</Modal> </Modal>

View File

@@ -16,11 +16,15 @@ interface ModalProps {
export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => { export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => {
const [workspaceName, setWorkspaceName] = useState(''); const [workspaceName, setWorkspaceName] = useState('');
const [loading, setLoading] = useState(false);
const { createWorkspace } = useWorkspaceHelper(); const { createWorkspace } = useWorkspaceHelper();
const router = useRouter(); const router = useRouter();
const handleCreateWorkspace = async () => { const handleCreateWorkspace = async () => {
setLoading(true);
const workspace = await createWorkspace(workspaceName); const workspace = await createWorkspace(workspaceName);
if (workspace && workspace.id) { if (workspace && workspace.id) {
setLoading(false);
router.replace(`/workspace/${workspace.id}`); router.replace(`/workspace/${workspace.id}`);
onClose(); onClose();
} else { } else {
@@ -37,9 +41,8 @@ export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => {
return ( return (
<div> <div>
<Modal open={open} onClose={onClose}> <Modal open={open} onClose={onClose}>
<ModalWrapper width={620} height={334} style={{ padding: '10px' }}> <ModalWrapper width={560} height={342} style={{ padding: '10px' }}>
<Header> <Header>
<ContentTitle>{t('New Workspace')}</ContentTitle>
<ModalCloseButton <ModalCloseButton
top={6} top={6}
right={6} right={6}
@@ -49,14 +52,28 @@ export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => {
/> />
</Header> </Header>
<Content> <Content>
<p>{t('Workspace description')}</p> <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>
<Input <Input
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={'Set a Workspace name'}
onChange={value => { onChange={value => {
setWorkspaceName(value); setWorkspaceName(value);
}} }}
></Input> ></Input>
<Button <Button
disabled={!workspaceName}
style={{
width: '260px',
textAlign: 'center',
marginTop: '16px',
opacity: !workspaceName ? 0.5 : 1,
}}
loading={loading}
type="primary"
onClick={() => { onClick={() => {
handleCreateWorkspace(); handleCreateWorkspace();
}} }}
@@ -75,20 +92,28 @@ const Header = styled('div')({
height: '44px', height: '44px',
}); });
const Content = styled('div')({ const Content = styled('div')(({ theme }) => {
display: 'flex', return {
padding: '0 48px', padding: '0 84px',
flexDirection: 'column', textAlign: 'center',
alignItems: 'center', fontSize: '18px',
gap: '16px', lineHeight: '26px',
color: theme.colors.inputColor,
p: {
marginTop: '12px',
marginBottom: '16px',
},
};
}); });
const ContentTitle = styled('span')({ const ContentTitle = styled('div')(() => {
fontSize: '20px', return {
lineHeight: '28px', fontSize: '20px',
fontWeight: 600, lineHeight: '28px',
textAlign: 'left', fontWeight: 600,
paddingBottom: '16px', textAlign: 'center',
paddingBottom: '16px',
};
}); });
// const Footer = styled('div')({ // const Footer = styled('div')({

View File

@@ -1,3 +1,3 @@
<svg width="36" height="35" viewBox="0 0 36 35" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="36" height="35" viewBox="0 0 36 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.8277 12.2008C15.0955 12.4686 15.0955 12.9028 14.8277 13.1706L12.3412 15.6571H20.9551C21.6982 15.6571 22.2977 15.6571 22.7831 15.6968C23.2828 15.7376 23.7218 15.8239 24.128 16.0308C24.7731 16.3595 25.2976 16.884 25.6263 17.5292C25.8332 17.9353 25.9195 18.3743 25.9604 18.8741C26 19.3595 26 19.9589 26 20.7021V21.8286C26 22.2073 25.693 22.5143 25.3143 22.5143C24.9356 22.5143 24.6286 22.2073 24.6286 21.8286V20.7314C24.6286 19.952 24.628 19.4087 24.5935 18.9858C24.5596 18.5708 24.4964 18.3324 24.4044 18.1518C24.2071 17.7647 23.8924 17.45 23.5054 17.2528C23.3248 17.1608 23.0864 17.0976 22.6714 17.0637C22.2484 17.0291 21.7051 17.0286 20.9257 17.0286H12.3412L14.8277 19.5151C15.0955 19.7829 15.0955 20.2171 14.8277 20.4849C14.5599 20.7527 14.1258 20.7527 13.858 20.4849L10.2008 16.8277C9.93305 16.5599 9.93305 16.1258 10.2008 15.858L13.858 12.2008C14.1258 11.9331 14.5599 11.9331 14.8277 12.2008Z" fill="#9096A5"/> <path fillRule="evenodd" clipRule="evenodd" d="M14.8277 12.2008C15.0955 12.4686 15.0955 12.9028 14.8277 13.1706L12.3412 15.6571H20.9551C21.6982 15.6571 22.2977 15.6571 22.7831 15.6968C23.2828 15.7376 23.7218 15.8239 24.128 16.0308C24.7731 16.3595 25.2976 16.884 25.6263 17.5292C25.8332 17.9353 25.9195 18.3743 25.9604 18.8741C26 19.3595 26 19.9589 26 20.7021V21.8286C26 22.2073 25.693 22.5143 25.3143 22.5143C24.9356 22.5143 24.6286 22.2073 24.6286 21.8286V20.7314C24.6286 19.952 24.628 19.4087 24.5935 18.9858C24.5596 18.5708 24.4964 18.3324 24.4044 18.1518C24.2071 17.7647 23.8924 17.45 23.5054 17.2528C23.3248 17.1608 23.0864 17.0976 22.6714 17.0637C22.2484 17.0291 21.7051 17.0286 20.9257 17.0286H12.3412L14.8277 19.5151C15.0955 19.7829 15.0955 20.2171 14.8277 20.4849C14.5599 20.7527 14.1258 20.7527 13.858 20.4849L10.2008 16.8277C9.93305 16.5599 9.93305 16.1258 10.2008 15.858L13.858 12.2008C14.1258 11.9331 14.5599 11.9331 14.8277 12.2008Z" fill="#9096A5"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react';
import type { Page, Workspace } from '@blocksuite/store'; import type { Page, Workspace } from '@blocksuite/store';
import '@blocksuite/blocks'; import '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor'; import { EditorContainer } from '@blocksuite/editor';
import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-v2.0.md'; import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-Downhill.md';
import { styled } from '@/styles'; import { styled } from '@/styles';
const StyledEditorContainer = styled('div')(() => { const StyledEditorContainer = styled('div')(() => {
@@ -39,7 +39,7 @@ export const Editor = ({ page, workspace, setEditor }: Props) => {
const title = metaTitle const title = metaTitle
? metaTitle ? metaTitle
: isFirstPage : isFirstPage
? 'Welcome to AFFiNE Alpha "Abbey Wood"' ? 'Welcome to AFFiNE Alpha "Downhill"'
: ''; : '';
workspace?.setPageMeta(page.id, { title }); workspace?.setPageMeta(page.id, { title });
@@ -60,7 +60,7 @@ export const Editor = ({ page, workspace, setEditor }: Props) => {
} }
setEditor(editor); setEditor(editor);
document.title = page?.meta.title || 'Untitled'; document.title = page.meta.title || 'Untitled';
return ret; return ret;
}, [workspace, page, setEditor]); }, [workspace, page, setEditor]);

View File

@@ -0,0 +1,24 @@
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { Button } from '@/ui/button';
import { useTranslation } from '@affine/i18n';
import { useState } from 'react';
export const EnableWorkspaceButton = () => {
const { t } = useTranslation();
const { enableWorkspace } = useWorkspaceHelper();
const [loading, setLoading] = useState(false);
return (
<Button
type="primary"
shape="circle"
loading={loading}
onClick={async () => {
setLoading(true);
await enableWorkspace();
setLoading(false);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
);
};

View File

@@ -7,7 +7,7 @@ import {
StyledCloseButton, StyledCloseButton,
} from './styles'; } from './styles';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { getWarningMessage, shouldShowWarning } from './utils'; import { useWarningMessage, shouldShowWarning } from './utils';
import EditorOptionMenu from './header-right-items/EditorOptionMenu'; import EditorOptionMenu from './header-right-items/EditorOptionMenu';
import TrashButtonGroup from './header-right-items/TrashButtonGroup'; import TrashButtonGroup from './header-right-items/TrashButtonGroup';
import ThemeModeSwitch from './header-right-items/theme-mode-switch'; import ThemeModeSwitch from './header-right-items/theme-mode-switch';
@@ -22,7 +22,7 @@ const BrowserWarning = ({
}) => { }) => {
return ( return (
<StyledBrowserWarning show={show}> <StyledBrowserWarning show={show}>
{getWarningMessage()} {useWarningMessage()}
<StyledCloseButton onClick={onClose}> <StyledCloseButton onClick={onClose}>
<CloseIcon /> <CloseIcon />
</StyledCloseButton> </StyledCloseButton>
@@ -44,7 +44,7 @@ const HeaderRightItems: Record<HeaderRightItemNames, ReactNode> = {
}; };
export const Header = ({ export const Header = ({
rightItems = ['syncUser'], rightItems = ['syncUser', 'themeModeSwitch'],
children, children,
}: PropsWithChildren<{ rightItems?: HeaderRightItemNames[] }>) => { }: PropsWithChildren<{ rightItems?: HeaderRightItemNames[] }>) => {
const [showWarning, setShowWarning] = useState(shouldShowWarning()); const [showWarning, setShowWarning] = useState(shouldShowWarning());

View File

@@ -1,7 +1,7 @@
import { PropsWithChildren, ReactNode } from 'react'; import { PropsWithChildren, ReactNode } from 'react';
import Header from './Header'; import Header from './Header';
import { StyledPageListTittleWrapper } from './styles'; import { StyledPageListTittleWrapper } from './styles';
import QuickSearchButton from './QuickSearchButton'; // import QuickSearchButton from './QuickSearchButton';
export type PageListHeaderProps = PropsWithChildren<{ export type PageListHeaderProps = PropsWithChildren<{
icon?: ReactNode; icon?: ReactNode;
@@ -12,7 +12,7 @@ export const PageListHeader = ({ icon, children }: PageListHeaderProps) => {
<StyledPageListTittleWrapper> <StyledPageListTittleWrapper>
{icon} {icon}
{children} {children}
<QuickSearchButton style={{ marginLeft: '5px' }} /> {/* <QuickSearchButton style={{ marginLeft: '5px' }} /> */}
</StyledPageListTittleWrapper> </StyledPageListTittleWrapper>
</Header> </Header>
); );

View File

@@ -1,12 +1,10 @@
import React from 'react'; import React from 'react';
import { IconButton, IconButtonProps } from '@/ui/button'; import { IconButton, IconButtonProps } from '@/ui/button';
import { Tooltip } from '@/ui/tooltip';
import { ArrowDownIcon } from '@blocksuite/icons'; import { ArrowDownIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/GlobalModalProvider'; import { useModal } from '@/providers/GlobalModalProvider';
import { useTranslation } from '@affine/i18n';
import { styled } from '@/styles'; import { styled } from '@/styles';
const StyledIconButtonWithAnimate = styled(IconButton)(() => { const StyledIconButtonWithAnimate = styled(IconButton)(({ theme }) => {
return { return {
svg: { svg: {
transition: 'transform 0.15s ease-in-out', transition: 'transform 0.15s ease-in-out',
@@ -15,6 +13,9 @@ const StyledIconButtonWithAnimate = styled(IconButton)(() => {
svg: { svg: {
transform: 'translateY(3px)', transform: 'translateY(3px)',
}, },
'::after': {
background: theme.colors.pageBackground,
},
}, },
}; };
}); });
@@ -23,20 +24,17 @@ export const QuickSearchButton = ({
...props ...props
}: Omit<IconButtonProps, 'children'>) => { }: Omit<IconButtonProps, 'children'>) => {
const { triggerQuickSearchModal } = useModal(); const { triggerQuickSearchModal } = useModal();
const { t } = useTranslation();
return ( return (
<Tooltip content={t('Switch to')} placement="bottom"> <StyledIconButtonWithAnimate
<StyledIconButtonWithAnimate data-testid="header-quickSearchButton"
data-testid="header-quickSearchButton" {...props}
{...props} onClick={e => {
onClick={e => { onClick?.(e);
onClick?.(e); triggerQuickSearchModal();
triggerQuickSearchModal(); }}
}} >
> <ArrowDownIcon />
<ArrowDownIcon /> </StyledIconButtonWithAnimate>
</StyledIconButtonWithAnimate>
</Tooltip>
); );
}; };

View File

@@ -3,35 +3,25 @@ import { displayFlex, styled } from '@/styles';
export const StyledHeaderContainer = styled.div<{ hasWarning: boolean }>( export const StyledHeaderContainer = styled.div<{ hasWarning: boolean }>(
({ hasWarning }) => { ({ hasWarning }) => {
return { return {
position: 'relative',
height: hasWarning ? '96px' : '60px', height: hasWarning ? '96px' : '60px',
}; };
} }
); );
export const StyledHeader = styled.div<{ hasWarning: boolean }>( export const StyledHeader = styled.div<{ hasWarning: boolean }>(() => {
({ hasWarning }) => { return {
return { height: '60px',
height: '60px', width: '100%',
width: '100%', ...displayFlex('flex-end', 'center'),
...displayFlex('flex-end', 'center'), background: 'var(--affine-page-background)',
background: 'var(--affine-page-background)', transition: 'background-color 0.5s',
transition: 'background-color 0.5s', zIndex: 99,
position: 'absolute', };
left: '0', });
top: hasWarning ? '36px' : '0',
padding: '0 22px',
zIndex: 99,
};
}
);
export const StyledTitle = styled('div')(({ theme }) => ({ export const StyledTitle = styled('div')(({ theme }) => ({
width: '720px', width: '720px',
height: '100%', height: '100%',
position: 'absolute',
left: 0,
right: 0,
top: 0,
margin: 'auto', margin: 'auto',
...displayFlex('center', 'center'), ...displayFlex('center', 'center'),

View File

@@ -1,4 +1,5 @@
import getIsMobile from '@/utils/get-is-mobile'; import getIsMobile from '@/utils/get-is-mobile';
import { Trans, useTranslation } from '@affine/i18n';
// Inspire by https://stackoverflow.com/a/4900484/8415727 // Inspire by https://stackoverflow.com/a/4900484/8415727
const getChromeVersion = () => { const getChromeVersion = () => {
const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
@@ -19,20 +20,20 @@ export const shouldShowWarning = () => {
); );
}; };
export const getWarningMessage = () => { export const useWarningMessage = () => {
const { t } = useTranslation();
if (!getIsChrome()) { if (!getIsChrome()) {
return ( return (
<span> <span>
We recommend the <strong>Chrome</strong> browser for optimal experience. <Trans i18nKey="recommendBrowser">
We recommend the <strong>Chrome</strong> browser for optimal
experience.
</Trans>
</span> </span>
); );
} }
if (getChromeVersion() < minimumChromeVersion) { if (getChromeVersion() < minimumChromeVersion) {
return ( return <span>{t('upgradeBrowser')}</span>;
<span>
Please upgrade to the latest version of Chrome for the best experience.
</span>
);
} }
return ''; return '';
}; };

View File

@@ -29,14 +29,21 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => {
currentWorkspace?.blocksuiteWorkspace?.setPageMeta(page.id, { title }); currentWorkspace?.blocksuiteWorkspace?.setPageMeta(page.id, { title });
if (page && page.root === null) { if (page && page.root === null) {
setTimeout(() => { setTimeout(() => {
const editor = document.querySelector('editor-container'); try {
if (editor) { const editor = document.querySelector('editor-container');
page.addBlock({ flavour: 'affine:surface' }, null); if (editor) {
const frameId = page.addBlock({ flavour: 'affine:frame' }, pageId); page.addBlock({ flavour: 'affine:surface' }, null);
// TODO blocksuite should offer a method to import markdown from store const frameId = page.addBlock(
editor.clipboard.importMarkdown(template.source, `${frameId}`); { flavour: 'affine:frame' },
page.resetHistory(); pageId
editor.requestUpdate(); );
// TODO blocksuite should offer a method to import markdown from store
editor.clipboard.importMarkdown(template.source, `${frameId}`);
page.resetHistory();
editor.requestUpdate();
}
} catch (e) {
console.error(e);
} }
}, 300); }, 300);
} }
@@ -98,13 +105,13 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => {
> >
Markdown Markdown
</Button> </Button>
{/* <Button <Button
onClick={() => { onClick={() => {
_handleAppleTemplateFromFilePicker(); _handleAppleTemplateFromFilePicker();
}} }}
> >
HTML HTML
</Button> */} </Button>
</StyledButtonWrapper> </StyledButtonWrapper>
)} )}

View File

@@ -1,5 +1,6 @@
import { styled } from '@/styles'; import { styled } from '@/styles';
import Loading from './Loading'; import Loading from './Loading';
import { useTranslation } from '@affine/i18n';
// Used for the full page loading // Used for the full page loading
const StyledLoadingContainer = styled('div')(() => { const StyledLoadingContainer = styled('div')(() => {
@@ -17,12 +18,13 @@ const StyledLoadingContainer = styled('div')(() => {
}; };
}); });
export const PageLoading = ({ text = 'Loading...' }: { text?: string }) => { export const PageLoading = ({ text }: { text?: string }) => {
const { t } = useTranslation();
return ( return (
<StyledLoadingContainer> <StyledLoadingContainer>
<div className="wrapper"> <div className="wrapper">
<Loading /> <Loading />
<h1>{text}</h1> <h1>{text ? text : t('Loading')}</h1>
</div> </div>
</StyledLoadingContainer> </StyledLoadingContainer>
); );

View File

@@ -13,8 +13,6 @@ export const GoogleIcon = () => {
}; };
const GoogleIconWrapper = styled('div')(({ theme }) => ({ const GoogleIconWrapper = styled('div')(({ theme }) => ({
width: '48px',
height: '48px',
background: theme.colors.pageBackground, background: theme.colors.pageBackground,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',

View File

@@ -1,38 +1,24 @@
// import { getDataCenter } from '@affine/datacenter';
import { styled } from '@/styles'; import { styled } from '@/styles';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
// import { useModal } from '@/providers/GlobalModalProvider';
import { GoogleIcon, StayLogOutIcon } from './Icons'; import { GoogleIcon, StayLogOutIcon } from './Icons';
import { useTranslation } from '@affine/i18n';
export const GoogleLoginButton = () => { export const GoogleLoginButton = () => {
// const { triggerLoginModal } = useModal(); const { t } = useTranslation();
return ( return (
<StyledGoogleButton <StyledGoogleButton>
onClick={() => {
// getDataCenter()
// .then(dc => dc.apis.signInWithGoogle?.())
// .then(() => {
// triggerLoginModal();
// })
// .catch(error => {
// console.log('sign google error', error);
// });
}}
>
<ButtonWrapper> <ButtonWrapper>
<IconWrapper> <IconWrapper>
<GoogleIcon /> <GoogleIcon />
</IconWrapper> </IconWrapper>
<TextWrapper> <TextWrapper>{t('Continue with Google')}</TextWrapper>
<Title>Continue with Google</Title>
<Description>Set up an AFFiNE account to sync data</Description>
</TextWrapper>
</ButtonWrapper> </ButtonWrapper>
</StyledGoogleButton> </StyledGoogleButton>
); );
}; };
export const StayLogOutButton = () => { export const StayLogOutButton = () => {
const { t } = useTranslation();
return ( return (
<StyledStayLogOutButton> <StyledStayLogOutButton>
<ButtonWrapper> <ButtonWrapper>
@@ -40,29 +26,26 @@ export const StayLogOutButton = () => {
<StayLogOutIcon /> <StayLogOutIcon />
</IconWrapper> </IconWrapper>
<TextWrapper> <TextWrapper>
<Title>Stay logged out</Title> <Title>{t('Stay logged out')}</Title>
<Description>All changes are saved locally</Description> <Description>{t('All changes are saved locally')}</Description>
</TextWrapper> </TextWrapper>
</ButtonWrapper> </ButtonWrapper>
</StyledStayLogOutButton> </StyledStayLogOutButton>
); );
}; };
const StyledGoogleButton = styled(Button)(() => { const StyledGoogleButton = styled('div')(({ theme }) => {
return { return {
width: '361px', width: '284px',
height: '56px', height: '40px',
padding: '4px', marginTop: '30px',
background: '#6880FF', fontSize: '16px',
color: '#fff', cursor: 'pointer',
borderRadius: '40px',
'& > span': { border: `1px solid ${theme.colors.iconColor}`,
marginLeft: 0, overflow: 'hidden',
},
':hover': { ':hover': {
background: '#516BF4', border: `1px solid ${theme.colors.primaryColor}`,
color: '#fff',
}, },
}; };
}); });
@@ -72,11 +55,6 @@ const StyledStayLogOutButton = styled(Button)(() => {
width: '361px', width: '361px',
height: '56px', height: '56px',
padding: '4px', padding: '4px',
'& > span': {
marginLeft: 0,
},
':hover': { ':hover': {
borderColor: '#6880FF', borderColor: '#6880FF',
}, },
@@ -86,20 +64,22 @@ const StyledStayLogOutButton = styled(Button)(() => {
const ButtonWrapper = styled('div')({ const ButtonWrapper = styled('div')({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
width: '100%',
}); });
const IconWrapper = styled('div')({ const IconWrapper = styled('div')({
width: '48px',
height: '48px',
flex: '0 48px', flex: '0 48px',
borderRadius: '5px', borderRadius: '5px',
overflow: 'hidden', overflow: 'hidden',
marginRight: '12px', marginRight: '12px',
marginTop: '8px',
}); });
const TextWrapper = styled('div')({ const TextWrapper = styled('div')({
flex: 1, flex: 1,
textAlign: 'left', textAlign: 'left',
height: '40px',
lineHeight: '40px',
}); });
const Title = styled('h1')(() => { const Title = styled('h1')(() => {

View File

@@ -1,18 +1,17 @@
import { ResetIcon } from '@blocksuite/icons';
import { styled } from '@/styles'; import { styled } from '@/styles';
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal'; import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { TextButton } from '@/ui/button'; import { GoogleLoginButton } from './LoginOptionButton';
import { GoogleLoginButton, StayLogOutButton } from './LoginOptionButton'; import { useAppState } from '@/providers/app-state-provider';
interface LoginModalProps { interface LoginModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
} }
export const LoginModal = ({ open, onClose }: LoginModalProps) => { export const LoginModal = ({ open, onClose }: LoginModalProps) => {
const { login } = useAppState();
return ( return (
<Modal open={open} onClose={onClose} data-testid="login-modal"> <Modal open={open} onClose={onClose} data-testid="login-modal">
<ModalWrapper width={620} height={334}> <ModalWrapper width={560} height={292}>
<Header> <Header>
<ModalCloseButton <ModalCloseButton
top={6} top={6}
@@ -23,13 +22,17 @@ export const LoginModal = ({ open, onClose }: LoginModalProps) => {
/> />
</Header> </Header>
<Content> <Content>
<ContentTitle>Currently not logged in</ContentTitle> <ContentTitle>{'Sign in'}</ContentTitle>
<GoogleLoginButton /> <SignDes>Set up an AFFINE account to sync data</SignDes>
<StayLogOutButton /> <span
onClick={async () => {
await login();
onClose();
}}
>
<GoogleLoginButton />
</span>
</Content> </Content>
<Footer>
<TextButton icon={<StyledResetIcon />}>Clear local data</TextButton>
</Footer>
</ModalWrapper> </ModalWrapper>
</Modal> </Modal>
); );
@@ -55,14 +58,10 @@ const ContentTitle = styled('h1')({
paddingBottom: '16px', paddingBottom: '16px',
}); });
const Footer = styled('div')({ const SignDes = styled('div')(({ theme }) => {
height: '70px', return {
paddingLeft: '24px', fontWeight: 400,
marginTop: '32px', color: theme.colors.textColor,
}); fontSize: '16px',
};
const StyledResetIcon = styled(ResetIcon)({
marginRight: '12px',
width: '20px',
height: '20px',
}); });

View File

@@ -0,0 +1,48 @@
export const Check = () => {
return (
<span>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_9266_16831)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.83301 3.33331C4.4523 3.33331 3.33301 4.4526 3.33301 5.83331V14.1666C3.33301 15.5474 4.4523 16.6666 5.83301 16.6666H14.1663C15.5471 16.6666 16.6663 15.5474 16.6663 14.1666V5.83331C16.6663 4.4526 15.5471 3.33331 14.1663 3.33331H5.83301ZM14.6385 7.97059C14.8984 7.70982 14.8977 7.28771 14.6369 7.02778C14.3762 6.76785 13.9541 6.76852 13.6941 7.02929L8.62861 12.1111L6.30522 9.77929C6.04534 9.51847 5.62323 9.51771 5.36241 9.77759C5.10159 10.0375 5.10083 10.4596 5.36071 10.7204L8.03822 13.4076C8.36386 13.7344 8.89304 13.7344 9.21874 13.4077L14.6385 7.97059Z"
fill="#888A9E"
/>
</g>
<defs>
<clipPath id="clip0_9266_16831">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
</span>
);
};
export const UnCheck = () => {
return (
<span>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.1673 4.66665H5.83398C5.18965 4.66665 4.66732 5.18898 4.66732 5.83331V14.1666C4.66732 14.811 5.18965 15.3333 5.83398 15.3333H14.1673C14.8116 15.3333 15.334 14.811 15.334 14.1666V5.83331C15.334 5.18898 14.8116 4.66665 14.1673 4.66665ZM5.83398 3.33331C4.45327 3.33331 3.33398 4.4526 3.33398 5.83331V14.1666C3.33398 15.5474 4.45327 16.6666 5.83398 16.6666H14.1673C15.548 16.6666 16.6673 15.5474 16.6673 14.1666V5.83331C16.6673 4.4526 15.548 3.33331 14.1673 3.33331H5.83398Z"
fill="#888A9E"
/>
</svg>
</span>
);
};

View File

@@ -0,0 +1,118 @@
import { styled } from '@/styles';
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import { Check, UnCheck } from './icon';
import { useState } from 'react';
interface LoginModalProps {
open: boolean;
onClose: (wait: boolean) => void;
}
export const LogoutModal = ({ open, onClose }: LoginModalProps) => {
const [localCache, setLocalCache] = useState(false);
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>
<StyleTips>
{localCache ? (
<StyleCheck
onClick={() => {
setLocalCache(false);
}}
>
<Check></Check>
</StyleCheck>
) : (
<StyleCheck
onClick={() => {
setLocalCache(true);
}}
>
<UnCheck></UnCheck>
</StyleCheck>
)}
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>
</Content>
</ModalWrapper>
</Modal>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '16px',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
});
const SignDes = styled('div')(({ theme }) => {
return {
fontWeight: 400,
color: theme.colors.textColor,
fontSize: '16px',
};
});
const StyleCheck = styled('span')(() => {
return {
display: 'inline-block',
cursor: 'pointer',
svg: {
verticalAlign: 'sub',
marginRight: '8px',
},
};
});
const StyleTips = styled('span')(() => {
return {
userSelect: 'none',
};
});

View File

@@ -3,8 +3,10 @@ import Modal, { ModalCloseButton, ModalWrapper } from '@/ui/modal';
import getIsMobile from '@/utils/get-is-mobile'; import getIsMobile from '@/utils/get-is-mobile';
import { StyledButton, StyledContent, StyledTitle } from './styles'; import { StyledButton, StyledContent, StyledTitle } from './styles';
import bg from './bg.png'; import bg from './bg.png';
import { useTranslation } from '@affine/i18n';
export const MobileModal = () => { export const MobileModal = () => {
const [showModal, setShowModal] = useState(getIsMobile()); const [showModal, setShowModal] = useState(getIsMobile());
const { t } = useTranslation();
return ( return (
<Modal <Modal
open={showModal} open={showModal}
@@ -25,20 +27,17 @@ export const MobileModal = () => {
}} }}
/> />
<StyledTitle>Ooops!</StyledTitle> <StyledTitle>{t('Ooops!')}</StyledTitle>
<StyledContent> <StyledContent>
<p>Looks like you are browsing on a mobile device.</p> <p>{t('mobile device')}</p>
<p> <p>{t('mobile device description')}</p>
We are still working on mobile support and recommend you use a
desktop device.
</p>
</StyledContent> </StyledContent>
<StyledButton <StyledButton
onClick={() => { onClick={() => {
setShowModal(false); setShowModal(false);
}} }}
> >
Got it {t('Got it')}
</StyledButton> </StyledButton>
</ModalWrapper> </ModalWrapper>
</Modal> </Modal>

View File

@@ -1,6 +1,9 @@
import React from 'react'; import React from 'react';
import { Empty } from '@/ui/empty'; import { Empty } from '@/ui/empty';
export const PageListEmpty = () => { import { useTranslation } from '@affine/i18n';
export const PageListEmpty = (props: { listType?: string }) => {
const { listType } = props;
const { t } = useTranslation();
return ( return (
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<Empty <Empty
@@ -8,8 +11,10 @@ export const PageListEmpty = () => {
height={300} height={300}
sx={{ marginTop: '100px', marginBottom: '30px' }} sx={{ marginTop: '100px', marginBottom: '30px' }}
/> />
<p>Tips: Click Add to Favourites/Trash and the page will appear here.</p> {listType === 'all' && <p>{t('emptyAllPages')}</p>}
<p>(Designer is grappling with designing)</p> {listType === 'favorite' && <p>{t('emptyFavourite')}</p>}
{listType === 'trash' && <p>{t('emptyTrash')}</p>}
<p>{t('still designed')}</p>
</div> </div>
); );
}; };

View File

@@ -68,17 +68,19 @@ export const PageList = ({
showFavoriteTag = false, showFavoriteTag = false,
isTrash = false, isTrash = false,
isPublic = false, isPublic = false,
listType,
}: { }: {
pageList: PageMeta[]; pageList: PageMeta[];
showFavoriteTag?: boolean; showFavoriteTag?: boolean;
isTrash?: boolean; isTrash?: boolean;
isPublic?: boolean; isPublic?: boolean;
listType?: 'all' | 'trash' | 'favorite';
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { currentWorkspace } = useAppState(); const { currentWorkspace } = useAppState();
const { t } = useTranslation(); const { t } = useTranslation();
if (pageList.length === 0) { if (pageList.length === 0) {
return <Empty />; return <Empty listType={listType} />;
} }
return ( return (

View File

@@ -9,6 +9,7 @@ import { SearchIcon } from '@blocksuite/icons';
import { StyledInputContent, StyledLabel } from './style'; import { StyledInputContent, StyledLabel } from './style';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { useTranslation } from '@affine/i18n';
export const Input = (props: { export const Input = (props: {
query: string; query: string;
setQuery: Dispatch<SetStateAction<string>>; setQuery: Dispatch<SetStateAction<string>>;
@@ -18,7 +19,7 @@ export const Input = (props: {
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const { currentWorkspace } = useAppState(); const { currentWorkspace } = useAppState();
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
inputRef.current?.addEventListener( inputRef.current?.addEventListener(
'blur', 'blur',
@@ -78,8 +79,10 @@ export const Input = (props: {
}} }}
placeholder={ placeholder={
currentWorkspace?.isPublish currentWorkspace?.isPublish
? `Search in ${currentWorkspace?.blocksuiteWorkspace?.meta.name}` ? t('Quick search placeholder2', {
: 'Quick Search...' workspace: currentWorkspace?.blocksuiteWorkspace?.meta.name,
})
: t('Quick search placeholder')
} }
/> />
</StyledInputContent> </StyledInputContent>

View File

@@ -79,7 +79,7 @@ export const Results = (props: {
</StyledNotFound> </StyledNotFound>
) )
) : ( ) : (
<Command.Group heading={t('Switch to')}> <Command.Group heading={t('Jump to')}>
{List.map(link => { {List.map(link => {
return ( return (
<Command.Item <Command.Item

View File

@@ -94,7 +94,7 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
setShowCreatePage={setShowCreatePage} setShowCreatePage={setShowCreatePage}
/> />
</StyledContent> </StyledContent>
{currentWorkspace?.isPublish ? ( {currentWorkspace?.published ? (
<></> <></>
) : showCreatePage ? ( ) : showCreatePage ? (
<> <>

View File

@@ -3,6 +3,7 @@ interface IWorkspaceAvatar {
size: number; size: number;
name: string; name: string;
avatar: string; avatar: string;
style?: React.CSSProperties;
} }
export const WorkspaceAvatar = (props: IWorkspaceAvatar) => { export const WorkspaceAvatar = (props: IWorkspaceAvatar) => {
@@ -14,21 +15,29 @@ export const WorkspaceAvatar = (props: IWorkspaceAvatar) => {
{props.avatar ? ( {props.avatar ? (
<div <div
style={{ style={{
...props.style,
width: sizeStr, width: sizeStr,
height: sizeStr, height: sizeStr,
border: '1px solid #fff', border: '1px solid #fff',
color: '#fff', color: '#fff',
borderRadius: '50%', borderRadius: '50%',
overflow: 'hidden', overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
}} }}
> >
<picture> <picture>
<img src={props.avatar} alt="" /> <img
style={{ width: sizeStr, height: sizeStr }}
src={props.avatar}
alt=""
/>
</picture> </picture>
</div> </div>
) : ( ) : (
<div <div
style={{ style={{
...props.style,
width: sizeStr, width: sizeStr,
height: sizeStr, height: sizeStr,
border: '1px solid #fff', border: '1px solid #fff',
@@ -38,6 +47,8 @@ export const WorkspaceAvatar = (props: IWorkspaceAvatar) => {
borderRadius: '50%', borderRadius: '50%',
textAlign: 'center', textAlign: 'center',
lineHeight: size + 'px', lineHeight: size + 'px',
display: 'inline-block',
verticalAlign: 'middle',
}} }}
> >
{(props.name || 'AFFiNE').substring(0, 1)} {(props.name || 'AFFiNE').substring(0, 1)}

View File

@@ -0,0 +1,43 @@
import { useState, useEffect } from 'react';
import type { WorkspaceUnit } from '@affine/datacenter';
import { WorkspaceAvatar as Avatar } from './Avatar';
const useAvatar = (workspaceUnit?: WorkspaceUnit) => {
const [avatarUrl, setAvatarUrl] = useState('');
const avatarId =
workspaceUnit?.avatar || workspaceUnit?.blocksuiteWorkspace?.meta.avatar;
const blobs = workspaceUnit?.blocksuiteWorkspace?.blobs;
useEffect(() => {
if (avatarId && blobs) {
blobs.then(blobs => {
blobs?.get(avatarId).then(url => setAvatarUrl(url || ''));
});
} else {
setAvatarUrl('');
}
}, [avatarId, blobs]);
return avatarUrl;
};
export const WorkspaceUnitAvatar = ({
size = 20,
name,
workspaceUnit,
style,
}: {
size?: number;
name?: string;
workspaceUnit?: WorkspaceUnit | null;
style?: React.CSSProperties;
}) => {
const avatarUrl = useAvatar(workspaceUnit || undefined);
return (
<Avatar
size={size}
name={name || workspaceUnit?.name || ''}
avatar={avatarUrl}
style={style}
/>
);
};

View File

@@ -0,0 +1,2 @@
export { WorkspaceAvatar } from './Avatar';
export { WorkspaceUnitAvatar } from './WorkspaceUnitAvatar';

View File

@@ -0,0 +1,99 @@
export const LocalIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.86315 3.54163H13.1382C13.738 3.54162 14.2261 3.54161 14.6222 3.57398C15.0314 3.60741 15.3972 3.67848 15.7377 3.85196C16.2734 4.12493 16.709 4.5605 16.982 5.09624C17.1555 5.43671 17.2265 5.80252 17.26 6.21174C17.2923 6.60785 17.2923 7.09591 17.2923 7.69577V10.9153C17.2923 11.5151 17.2923 12.0032 17.26 12.3993C17.2265 12.8085 17.1555 13.1743 16.982 13.5148C16.709 14.0505 16.2734 14.4861 15.7377 14.7591C15.3972 14.9326 15.0314 15.0036 14.6222 15.0371C14.2261 15.0694 13.738 15.0694 13.1382 15.0694H12.8479V16.0416H13.7044C14.0495 16.0416 14.3294 16.3214 14.3294 16.6666C14.3294 17.0118 14.0495 17.2916 13.7044 17.2916H6.29695C5.95177 17.2916 5.67195 17.0118 5.67195 16.6666C5.67195 16.3214 5.95177 16.0416 6.29695 16.0416H7.15343V15.0694H6.86313C6.26327 15.0694 5.77521 15.0694 5.37909 15.0371C4.96988 15.0036 4.60407 14.9326 4.2636 14.7591C3.72786 14.4861 3.29229 14.0505 3.01931 13.5148C2.84583 13.1743 2.77477 12.8085 2.74134 12.3993C2.70897 12.0032 2.70898 11.5151 2.70898 10.9152V7.69579C2.70898 7.09592 2.70897 6.60786 2.74134 6.21174C2.77477 5.80252 2.84583 5.43671 3.01931 5.09624C3.29229 4.5605 3.72786 4.12493 4.2636 3.85196C4.60407 3.67848 4.96988 3.60741 5.37909 3.57398C5.77521 3.54161 6.26328 3.54162 6.86315 3.54163ZM3.96013 11.4583C3.96232 11.801 3.96868 12.071 3.98719 12.2975C4.0143 12.6294 4.06434 12.8124 4.13307 12.9473C4.2862 13.2478 4.53055 13.4922 4.83108 13.6453C4.96597 13.714 5.14897 13.7641 5.48088 13.7912C5.82009 13.8189 6.25695 13.8194 6.88954 13.8194H13.1118C13.7444 13.8194 14.1812 13.8189 14.5204 13.7912C14.8523 13.7641 15.0353 13.714 15.1702 13.6453C15.4708 13.4922 15.7151 13.2478 15.8682 12.9473C15.937 12.8124 15.987 12.6294 16.0141 12.2975C16.0326 12.071 16.039 11.801 16.0412 11.4583H3.96013ZM16.0423 10.2083H3.95898V7.72218C3.95898 7.08959 3.95947 6.65273 3.98719 6.31353C4.0143 5.98162 4.06434 5.79861 4.13307 5.66372C4.2862 5.36319 4.53055 5.11884 4.83108 4.96571C4.96597 4.89698 5.14897 4.84694 5.48088 4.81983C5.82009 4.79211 6.25695 4.79163 6.88954 4.79163H13.1118C13.7444 4.79163 14.1812 4.79211 14.5204 4.81983C14.8523 4.84694 15.0353 4.89698 15.1702 4.96571C15.4708 5.11884 15.7151 5.36319 15.8682 5.66372C15.937 5.79861 15.987 5.98162 16.0141 6.31353C16.0418 6.65273 16.0423 7.08959 16.0423 7.72218V10.2083ZM11.5979 15.0694H8.40343V16.0416H11.5979V15.0694ZM10.0007 11.9583C10.3458 11.9583 10.6257 12.2381 10.6257 12.5833V12.5916C10.6257 12.9368 10.3458 13.2166 10.0007 13.2166C9.65547 13.2166 9.37565 12.9368 9.37565 12.5916V12.5833C9.37565 12.2381 9.65547 11.9583 10.0007 11.9583Z"
fill="#FDBD32"
/>
</svg>
);
};
export const OfflineIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.12249 3.54165C8.47134 3.54145 8.74594 3.54129 9.01129 3.60499C9.24512 3.66113 9.46866 3.75372 9.6737 3.87937C9.90638 4.02195 10.1004 4.21624 10.347 4.46306L10.4942 4.61035C10.8035 4.91964 10.889 4.99941 10.9794 5.05484C11.0726 5.11195 11.1742 5.15404 11.2805 5.17956C11.3837 5.20432 11.5005 5.20834 11.9379 5.20834L14.8587 5.20834C15.3038 5.20833 15.6754 5.20832 15.9789 5.23312C16.2955 5.25899 16.5927 5.31491 16.8737 5.45812C17.3049 5.67783 17.6555 6.02841 17.8752 6.45961C18.0184 6.74066 18.0744 7.03788 18.1002 7.35445C18.125 7.65797 18.125 8.02951 18.125 8.47463V13.192C18.125 13.6372 18.125 14.0087 18.1002 14.3122C18.0744 14.6288 18.0184 14.926 17.8752 15.2071C17.6555 15.6383 17.3049 15.9889 16.8737 16.2086C16.5927 16.3518 16.2955 16.4077 15.9789 16.4336C15.6754 16.4584 15.3039 16.4583 14.8587 16.4583H5.14129C4.69618 16.4583 4.32463 16.4584 4.02111 16.4336C3.70454 16.4077 3.40732 16.3518 3.12627 16.2086C2.69507 15.9889 2.34449 15.6383 2.12478 15.2071C1.98157 14.926 1.92565 14.6288 1.89978 14.3122C1.87498 14.0087 1.87499 13.6372 1.875 13.192V6.80798C1.87499 6.36285 1.87498 5.99131 1.89978 5.68778C1.92565 5.37121 1.98157 5.074 2.12478 4.79294C2.34449 4.36174 2.69507 4.01116 3.12627 3.79145C3.40732 3.64825 3.70454 3.59232 4.02111 3.56645C4.32464 3.54166 4.69618 3.54166 5.14131 3.54167L8.12249 3.54165ZM3.125 13.1667V6.83334C3.125 6.69601 3.12504 6.57168 3.12558 6.45836L14.8333 6.45834C15.3104 6.45834 15.6305 6.45882 15.8771 6.47897C16.1164 6.49852 16.2308 6.53342 16.3062 6.57187C16.5022 6.67174 16.6616 6.8311 16.7615 7.0271C16.7999 7.10257 16.8348 7.21697 16.8544 7.45624C16.8745 7.7028 16.875 8.02298 16.875 8.50001V13.1667C16.875 13.6437 16.8745 13.9639 16.8544 14.2104C16.8348 14.4497 16.7999 14.5641 16.7615 14.6396C16.6616 14.8356 16.5022 14.9949 16.3062 15.0948C16.2308 15.1333 16.1164 15.1682 15.8771 15.1877C15.6305 15.2079 15.3104 15.2083 14.8333 15.2083H5.16667C4.68964 15.2083 4.36946 15.2079 4.1229 15.1877C3.88363 15.1682 3.76923 15.1333 3.69376 15.0948C3.49776 14.9949 3.3384 14.8356 3.23854 14.6396C3.20008 14.5641 3.16518 14.4497 3.14563 14.2104C3.12549 13.9639 3.125 13.6437 3.125 13.1667ZM9.20211 7.49996C9.01802 7.49996 8.86878 7.6492 8.86878 7.83329V10.867H7.47722C7.17943 10.867 7.03108 11.2277 7.24271 11.4372L10 14.1666L12.7573 11.4372C12.9689 11.2277 12.8206 10.867 12.5228 10.867H11.1312V7.83329C11.1312 7.6492 10.982 7.49996 10.7979 7.49996H9.20211Z"
fill="#62CD80"
/>
</svg>
);
};
export const PublishedIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 18.125C14.4873 18.125 18.125 14.4873 18.125 10C18.125 5.51269 14.4873 1.875 10 1.875C5.51269 1.875 1.875 5.51269 1.875 10C1.875 14.4873 5.51269 18.125 10 18.125ZM9.99992 16.523C13.6024 16.523 16.5228 13.6026 16.5228 10.0001C16.5228 6.39761 13.6024 3.47722 9.99992 3.47722C6.39742 3.47722 3.47703 6.39761 3.47703 10.0001C3.47703 13.6026 6.39742 16.523 9.99992 16.523Z"
fill="#8699FF"
/>
<path
d="M7.13957 8.05468C8.34659 8.05468 9.33537 7.12035 9.42212 5.93548C9.57023 5.97414 9.72565 5.99472 9.88588 5.99472H10.8014C11.8126 5.99472 12.6324 5.17496 12.6324 4.16374C12.6324 3.15251 11.8126 2.33275 10.8014 2.33275H9.88588C9.03665 2.33275 8.32245 2.91091 8.11542 3.69509C7.81941 3.55535 7.48862 3.47722 7.13957 3.47722C5.8756 3.47722 4.85094 4.50182 4.85084 5.76577H3.70524V8.39782L7.59609 12.2887V12.9753C7.59609 13.8601 8.31338 14.5774 9.1982 14.5774V16.4084L10.457 17.4383L14.6912 15.264C14.6912 14.1264 13.7689 13.2042 12.6313 13.2042H12.288C12.288 11.3713 10.8022 9.88549 8.96932 9.88549H6.79503V8.02892C6.90741 8.04589 7.02246 8.05468 7.13957 8.05468Z"
fill="#8699FF"
/>
</svg>
);
};
export const CloudIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<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"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.54757 7.5L7.5 13.3333H8.6993L10.0017 9.29884L11.3048 13.3333H12.5L10.4521 7.5H9.54757Z"
fill="#60A5FA"
/>
</svg>
);
};
export const LineIcon = () => {
return (
<svg
width="2"
height="20"
viewBox="0 0 2 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M1 0V20" stroke="black" strokeOpacity="0.15" />
</svg>
);
};

View File

@@ -1,20 +1,28 @@
import { styled } from '@/styles'; import { styled } from '@/styles';
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal'; import { Modal, ModalWrapper } from '@/ui/modal';
import { Button } from '@/ui/button'; import { Button, IconButton } from '@/ui/button';
import { useState } from 'react'; import { useState } from 'react';
import { CreateWorkspaceModal } from '../create-workspace'; import { CreateWorkspaceModal } from '../create-workspace';
import { import {
CloudUnsyncedIcon,
CloudInsyncIcon,
UsersIcon, UsersIcon,
AddIcon, AddIcon,
LogOutIcon,
CloudInsyncIcon,
PublishIcon,
CloseIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { toast } from '@/ui/toast'; import {
import { WorkspaceAvatar } from '@/components/workspace-avatar'; WorkspaceAvatar,
WorkspaceUnitAvatar,
} from '@/components/workspace-avatar';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useConfirm } from '@/providers/ConfirmProvider'; import { useTranslation } from '@affine/i18n';
import { LanguageMenu } from './languageMenu';
import { CloudIcon, LineIcon, LocalIcon, OfflineIcon } from './icons';
import { LoginModal } from '../login-modal';
import { LogoutModal } from '../logout-modal';
interface WorkspaceModalProps { interface WorkspaceModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@@ -22,27 +30,49 @@ interface WorkspaceModalProps {
export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => { export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => {
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false); const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
const { confirm } = useConfirm(); const { workspaceList, currentWorkspace, user, logout, isOwner } =
const { workspaceList, currentWorkspace, login, user, logout } =
useAppState(); useAppState();
const router = useRouter(); const router = useRouter();
const { t } = useTranslation();
const [loginOpen, setLoginOpen] = useState(false);
const [logoutOpen, setLogoutOpen] = useState(false);
return ( return (
<div> <div>
<Modal open={open} onClose={onClose}> <Modal open={open} onClose={onClose}>
<ModalWrapper <ModalWrapper
width={820} width={720}
style={{ padding: '10px', display: 'flex', flexDirection: 'column' }} style={{
padding: '24px 40px',
display: 'flex',
flexDirection: 'column',
}}
> >
<Header> <Header>
<ContentTitle>My Workspaces</ContentTitle> <ContentTitle>{t('My Workspaces')}</ContentTitle>
{/* <LanguageMenu /> */} <HeaderOption>
<ModalCloseButton <LanguageMenu />
top={6} <div
right={6} style={{
onClick={() => { display: 'inline-block',
onClose(); border: 'none',
}} margin: '2px 16px',
/> height: '24px',
position: 'relative',
top: '4px',
}}
>
<LineIcon></LineIcon>
</div>
<Button
style={{ border: 'none', padding: 0 }}
onClick={() => {
onClose();
}}
>
<CloseIcon></CloseIcon>
</Button>
</HeaderOption>
</Header> </Header>
<Content> <Content>
<WorkspaceList> <WorkspaceList>
@@ -56,139 +86,142 @@ export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => {
active={item.id === currentWorkspace?.id} active={item.id === currentWorkspace?.id}
key={index} key={index}
> >
<span style={{ width: '100px' }}> <div>
<div <WorkspaceUnitAvatar size={58} workspaceUnit={item} />
style={{ </div>
float: 'left',
marginTop: '6px',
marginLeft: '10px',
marginRight: '10px',
}}
>
<WorkspaceAvatar
size={50}
name={item.name}
avatar={item.avatar ?? ''}
/>
</div>
<span <StyleWorkspaceInfo>
style={{ <StyleWorkspaceTitle>
width: '235px',
fontSize: '16px',
display: 'inline-block',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
position: 'relative',
top: '20px',
}}
>
{item.name || 'AFFiNE'} {item.name || 'AFFiNE'}
</span> </StyleWorkspaceTitle>
</span> {isOwner ? (
<span item.provider === 'local' ? (
style={{ <p>
position: 'relative', <LocalIcon />
top: '20px', Local Workspace
}} </p>
> ) : (
{(item.provider === 'local' || !item.provider) && ( <p>
<CloudUnsyncedIcon fontSize={24} /> <CloudIcon />
Cloud Workspace
</p>
)
) : (
<p>
<UsersIcon fontSize={20} color={'#FF646B'} />
Joined Workspace
</p>
)} )}
{item.provider === 'affine' && ( {item.provider === 'local' && (
<CloudInsyncIcon fontSize={24} /> <p>
<OfflineIcon />
All data can be accessed offline
</p>
)} )}
{item.published && <UsersIcon fontSize={24} />} {item.published && (
</span> <p>
{/* {item.isLocal ? 'isLocal' : ''}/ */} <PublishIcon fontSize={16} /> Published to Web
</p>
)}
</StyleWorkspaceInfo>
</WorkspaceItem> </WorkspaceItem>
); );
})} })}
<li> <WorkspaceItem
<Button onClick={() => {
style={{ setCreateWorkspaceOpen(true);
marginTop: '20px', }}
}} >
type="primary" <div>
onClick={() => { <StyleWorkspaceAdd className="add-icon">
setCreateWorkspaceOpen(true); <AddIcon fontSize={18} />
}} </StyleWorkspaceAdd>
> </div>
<AddIcon
style={{ <StyleWorkspaceInfo>
fontSize: '20px', <StyleWorkspaceTitle>New workspace</StyleWorkspaceTitle>
top: '5px', <p>Crete or import</p>
position: 'relative', </StyleWorkspaceInfo>
marginRight: '10px', </WorkspaceItem>
}}
/>
Create Or Import
</Button>
</li>
</WorkspaceList> </WorkspaceList>
<p style={{ fontSize: '14px', color: '#ccc', margin: '12px 0' }}> {/* <p style={{ fontSize: '14px', color: '#ccc', margin: '12px 0' }}>
Tips:Workspace is your virtual space to capture, create and plan {t('Tips')}
as just one person or together as a team. {t('Workspace description')}
</p> </p> */}
</Content> </Content>
<LoginModal
open={loginOpen}
onClose={() => {
setLoginOpen(false);
}}
></LoginModal>
<Footer> <Footer>
{!user ? ( {!user ? (
<Button <StyleSignIn
onClick={async () => { onClick={async () => {
await login(); setLoginOpen(true);
toast('Login success');
}} }}
> >
Sign in AFFiNE Cloud <span>
</Button> <CloudInsyncIcon fontSize={16} />
</span>
Sign in to sync with AFFINE Cloud
</StyleSignIn>
) : ( ) : (
<Button <div style={{ display: 'flex' }}>
onClick={() => { <div style={{ paddingTop: '20px' }}>
confirm({ <WorkspaceAvatar
title: 'Sign out?', size={40}
content: `All data has been stored in the cloud. `, name={user.name}
confirmText: 'Sign out', avatar={user.avatar}
cancelText: 'Cancel', ></WorkspaceAvatar>
}).then(async confirm => { </div>
if (confirm) { <StyleUserInfo style={{}}>
if (user) { <p>{user.name}</p>
await logout(); <p>{user.email}</p>
router.replace(`/workspace`); </StyleUserInfo>
toast('Enabled success'); <div style={{ paddingTop: '25px' }}>
} <IconButton
} onClick={() => {
}); setLogoutOpen(true);
}} // confirm({
> // title: 'Sign out?',
Sign out of AFFiNE Cloud // content: `All data has been stored in the cloud. `,
</Button> // 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> </Footer>
<CreateWorkspaceModal <CreateWorkspaceModal
open={createWorkspaceOpen} open={createWorkspaceOpen}
onClose={() => { onClose={() => {
setCreateWorkspaceOpen(false); setCreateWorkspaceOpen(false);
onClose();
// 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',
// }).then(confirm => {
// if (confirm) {
// if (user) {
// // workspaceId &&
// // updateWorkspaceMeta(workspaceId, { isPublish: true });
// } else {
// // login();
// // workspaceId &&
// // updateWorkspaceMeta(workspaceId, { isPublish: true });
// }
// }
// });
}} }}
></CreateWorkspaceModal> ></CreateWorkspaceModal>
<LogoutModal
open={logoutOpen}
onClose={async wait => {
if (!wait) {
await logout();
router.replace(`/workspace`);
}
setLogoutOpen(false);
}}
></LogoutModal>
</ModalWrapper> </ModalWrapper>
</Modal> </Modal>
</div> </div>
@@ -196,52 +229,138 @@ export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => {
}; };
const Header = styled('div')({ const Header = styled('div')({
position: 'relative', display: 'flex',
height: '44px',
}); });
const Content = styled('div')({ const Content = styled('div')({
padding: '0 20px',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
gap: '16px', gap: '16px',
flex: 1, flex: 1,
}); });
const HeaderOption = styled.div(() => {
const ContentTitle = styled('span')({ return {
marginLeft: '16px',
};
});
const ContentTitle = styled('div')({
fontSize: '20px', fontSize: '20px',
lineHeight: '28px', lineHeight: '24px',
fontWeight: 600, fontWeight: 600,
textAlign: 'left', textAlign: 'left',
paddingBottom: '16px', flex: 1,
});
const Footer = styled('div')({
height: '70px',
paddingLeft: '24px',
marginTop: '32px',
textAlign: 'center',
}); });
const WorkspaceList = styled('div')({ const WorkspaceList = styled('div')({
maxHeight: '500px',
overflow: 'auto',
display: 'grid', display: 'grid',
gridRowGap: '10px', gridRowGap: '24px',
gridColumnGap: '10px', gridColumnGap: '24px',
fontSize: '16px', fontSize: '16px',
marginTop: '36px',
gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateColumns: 'repeat(2, 1fr)',
}); });
export const WorkspaceItem = styled.div<{ export const WorkspaceItem = styled.div<{
active: boolean; active?: boolean;
}>(({ theme, active }) => { }>(({ theme, active }) => {
const backgroundColor = active ? theme.colors.hoverBackground : 'transparent'; const borderColor = active ? theme.colors.primaryColor : 'transparent';
return { return {
cursor: 'pointer', cursor: 'pointer',
padding: '8px', padding: '16px',
border: '1px solid #eee', height: '124px',
backgroundColor: backgroundColor, boxShadow: theme.shadow.modal,
display: 'flex',
borderRadius: '12px',
border: `1px solid ${borderColor}`,
':hover': { ':hover': {
background: theme.colors.hoverBackground, 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,
},
}, },
}; };
}); });

View File

@@ -1,9 +1,11 @@
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; import { LOCALES } from '@affine/i18n';
import type { TooltipProps } from '@mui/material'; import { TooltipProps } from '@mui/material';
import { styled } from '@/styles'; import { styled } from '@/styles';
import { Button, Tooltip } from '@mui/material'; import { Tooltip } from '@mui/material';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation, LOCALES } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { ArrowDownIcon } from '@blocksuite/icons';
import { Button } from '@/ui/button';
export const LanguageMenu = () => { export const LanguageMenu = () => {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
@@ -22,6 +24,7 @@ export const LanguageMenu = () => {
{LOCALES.map(option => { {LOCALES.map(option => {
return ( return (
<ListItem <ListItem
style={{ border: 'none' }}
key={option.name} key={option.name}
title={option.name} title={option.name}
onClick={() => { onClick={() => {
@@ -39,14 +42,14 @@ export const LanguageMenu = () => {
open={show} open={show}
> >
<StyledTitleButton <StyledTitleButton
variant="text" style={{ border: 'none', padding: '0px' }}
onClick={() => { onClick={() => {
setShow(!show); setShow(!show);
}} }}
> >
<StyledContainer> <StyledContainer>
<StyledText>{languageName}</StyledText> <StyledText>{languageName}</StyledText>
<UnfoldMoreIcon /> <ArrowDownIcon fontSize={18} />
</StyledContainer> </StyledContainer>
</StyledTitleButton> </StyledTitleButton>
</StyledTooltip> </StyledTooltip>
@@ -55,8 +58,6 @@ export const LanguageMenu = () => {
const StyledContainer = styled('div')(() => ({ const StyledContainer = styled('div')(() => ({
display: 'flex', display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
})); }));
@@ -87,8 +88,6 @@ const ListItem = styled(Button)(({ theme }) => ({
})); }));
const StyledTitleButton = styled(Button)(({ theme }) => ({ const StyledTitleButton = styled(Button)(({ theme }) => ({
position: 'absolute',
right: '50px',
color: theme.colors.popoverColor, color: theme.colors.popoverColor,
fontSize: theme.font.sm, fontSize: theme.font.sm,
})); }));

View File

@@ -1,10 +1,10 @@
import { styled } from '@/styles'; import { styled } from '@/styles';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { Trans } from '@affine/i18n';
export const ExportPageTitleContainer = styled('div')(() => { export const ExportPageTitleContainer = styled('div')(() => {
return { return {
display: 'flex', display: 'flex',
marginTop: '60px',
fontWeight: '500', fontWeight: '500',
flex: 1, flex: 1,
}; };
@@ -12,8 +12,13 @@ export const ExportPageTitleContainer = styled('div')(() => {
export const ExportPage = ({ workspace }: { workspace: WorkspaceUnit }) => { export const ExportPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
return ( return (
<ExportPageTitleContainer> <ExportPageTitleContainer>
Export Workspace{' '} <Trans i18nKey="Export Workspace">
<code style={{ margin: '0 10px' }}>{workspace.name}</code> Is Comming Export Workspace
<code style={{ margin: '0 10px' }}>
{{ workspace: workspace.name }}
</code>
Is Comming
</Trans>
</ExportPageTitleContainer> </ExportPageTitleContainer>
); );
}; };

View File

@@ -4,21 +4,28 @@ import {
StyledPublishCopyContainer, StyledPublishCopyContainer,
StyledPublishExplanation, StyledPublishExplanation,
StyledSettingH2, StyledSettingH2,
StyledStopPublishContainer,
} from './style'; } from './style';
import { useState } from 'react';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import Input from '@/ui/input'; import Input from '@/ui/input';
import { toast } from '@/ui/toast'; import { toast } from '@/ui/toast';
// import { useAppState } from '@/providers/app-state-provider3'; // import { useAppState } from '@/providers/app-state-provider3';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { useTranslation } from '@affine/i18n';
import { EnableWorkspaceButton } from '../enable-workspace';
export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => { export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const shareUrl = window.location.host + '/public-workspace/' + workspace.id; const shareUrl = window.location.host + '/public-workspace/' + workspace.id;
const { publishWorkspace, enableWorkspace } = useWorkspaceHelper(); const { publishWorkspace } = useWorkspaceHelper();
const { t } = useTranslation();
const [loaded, setLoaded] = useState(false);
const togglePublic = async (flag: boolean) => { const togglePublic = async (flag: boolean) => {
await publishWorkspace(workspace.id.toString(), flag); try {
await publishWorkspace(workspace.id.toString(), flag);
} catch (e) {
toast('Failed to publish workspace');
}
}; };
const copyUrl = () => { const copyUrl = () => {
@@ -26,56 +33,70 @@ export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
toast('Copied url to clipboard'); toast('Copied url to clipboard');
}; };
const enableAffineCloud = async () => {
await enableWorkspace();
};
return ( return (
<> <>
{workspace.provider === 'affine' ? ( {workspace.provider === 'affine' ? (
<div> <div
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
>
<StyledPublishContent> <StyledPublishContent>
{workspace?.published ? ( {workspace.published ? (
<> <>
<StyledPublishExplanation> <StyledPublishExplanation>
Publishing to web requires AFFiNE Cloud service . The current workspace has been published to the web, everyone
can view the contents of this workspace through the link.
</StyledPublishExplanation> </StyledPublishExplanation>
<StyledSettingH2>Share with link</StyledSettingH2>
<StyledPublishCopyContainer> <StyledPublishCopyContainer>
<StyledSettingH2 marginBottom={16}>
{t('Share with link')}
</StyledSettingH2>
<Input width={500} value={shareUrl} disabled={true}></Input> <Input width={500} value={shareUrl} disabled={true}></Input>
<StyledCopyButtonContainer> <StyledCopyButtonContainer>
<Button onClick={copyUrl} type="primary" shape="circle"> <Button onClick={copyUrl} type="primary" shape="circle">
Copy Link {t('Copy Link')}
</Button> </Button>
</StyledCopyButtonContainer> </StyledCopyButtonContainer>
</StyledPublishCopyContainer> </StyledPublishCopyContainer>
</> </>
) : ( ) : (
<StyledPublishExplanation> <StyledPublishExplanation>
After publishing to the web, everyone can view the content of {t('Publishing Description')}
this workspace through the link. <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> </StyledPublishExplanation>
)} )}
</StyledPublishContent> </StyledPublishContent>
{workspace.published ? ( {workspace.published ? (
<Button <StyledStopPublishContainer>
onClick={() => { <Button
togglePublic(false); onClick={async () => {
}} setLoaded(true);
type="primary" await togglePublic(false);
shape="circle" setLoaded(true);
> }}
Stop publishing loading={false}
</Button> type="danger"
shape="circle"
>
{t('Stop publishing')}
</Button>
</StyledStopPublishContainer>
) : ( ) : (
<Button <></>
onClick={() => {
togglePublic(true);
}}
type="primary"
shape="circle"
>
Publish to web
</Button>
)} )}
</div> </div>
) : ( ) : (
@@ -85,17 +106,9 @@ export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
Publishing to web requires AFFiNE Cloud service. Publishing to web requires AFFiNE Cloud service.
</StyledPublishExplanation> </StyledPublishExplanation>
<StyledPublishCopyContainer> <div style={{ marginTop: '72px' }}>
<Button <EnableWorkspaceButton></EnableWorkspaceButton>
onClick={() => { </div>
enableAffineCloud();
}}
type="primary"
shape="circle"
>
Enable AFFiNE Cloud
</Button>
</StyledPublishCopyContainer>
</> </>
</StyledPublishContent> </StyledPublishContent>
)} )}

View File

@@ -1,45 +1,52 @@
import { import {
StyleAsync,
StyledPublishContent, StyledPublishContent,
StyledPublishCopyContainer,
StyledPublishExplanation, StyledPublishExplanation,
StyledWorkspaceName,
StyledWorkspaceType,
} from './style'; } from './style';
import { DownloadIcon } from '@blocksuite/icons'; import { DownloadIcon } from '@blocksuite/icons';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { Menu, MenuItem } from '@/ui/menu'; import { Menu, MenuItem } from '@/ui/menu';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; import { Trans, useTranslation } from '@affine/i18n';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { EnableWorkspaceButton } from '../enable-workspace';
export const SyncPage = ({ workspace }: { workspace: WorkspaceUnit }) => { export const SyncPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { enableWorkspace } = useWorkspaceHelper(); const { t } = useTranslation();
return ( return (
<div> <div>
<StyledPublishContent> <StyledPublishContent>
{workspace?.provider === 'local' ? ( {workspace.provider === 'local' ? (
<> <>
<StyledPublishExplanation> <StyledPublishExplanation>
{workspace.name ?? 'Affine'} is Local Workspace. All data is <WorkspaceUnitAvatar
stored on the current device. You can enable AFFiNE Cloud for this size={32}
workspace to keep data in sync with the cloud. name={workspace.name}
workspaceUnit={workspace}
style={{ marginRight: '12px' }}
/>
<StyledWorkspaceName>{workspace.name};</StyledWorkspaceName>
<StyledWorkspaceType>is a Local Workspace.</StyledWorkspaceType>
</StyledPublishExplanation> </StyledPublishExplanation>
<StyledWorkspaceType>
<StyledPublishCopyContainer> All data is stored on the current device. You can enable AFFiNE
<Button Cloud for this workspace to keep data in sync with the cloud.
onClick={async () => { </StyledWorkspaceType>
await enableWorkspace(); <StyleAsync>
}} <EnableWorkspaceButton></EnableWorkspaceButton>
type="primary" </StyleAsync>
shape="circle"
>
Enable AFFiNE Cloud
</Button>
</StyledPublishCopyContainer>
</> </>
) : ( ) : (
<> <>
<StyledPublishExplanation> <StyledPublishExplanation>
<code>{workspace.name ?? 'Affine'}</code> is Cloud Workspace. All <Trans i18nKey="Sync Description2">
data will be synchronized and saved to the AFFiNE <code>{{ workspaceName: workspace.name ?? 'Affine' }}</code>
is Cloud Workspace. All data will be synchronised and saved to
the AFFiNE
</Trans>
</StyledPublishExplanation> </StyledPublishExplanation>
<StyledPublishCopyContainer> <StyleAsync>
<Menu <Menu
content={ content={
<> <>
@@ -49,7 +56,7 @@ export const SyncPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
}} }}
icon={<DownloadIcon />} icon={<DownloadIcon />}
> >
Download core data to device {t('Download data to device', { CoreOrAll: 'core' })}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
@@ -57,16 +64,18 @@ export const SyncPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
}} }}
icon={<DownloadIcon />} icon={<DownloadIcon />}
> >
Download all data to device {t('Download data to device', { CoreOrAll: 'all' })}
</MenuItem> </MenuItem>
</> </>
} }
placement="bottom-end" placement="bottom-end"
disablePortal={true} disablePortal={true}
> >
<Button>Download all data to device</Button> <Button type="primary">
{t('Download data to device', { CoreOrAll: 'all' })}
</Button>
</Menu> </Menu>
</StyledPublishCopyContainer> </StyleAsync>
</> </>
)} )}
</StyledPublishContent> </StyledPublishContent>

View File

@@ -3,26 +3,36 @@ import {
// StyledSettingAvatar, // StyledSettingAvatar,
StyledSettingAvatarContent, StyledSettingAvatarContent,
StyledSettingInputContainer, StyledSettingInputContainer,
StyledDoneButtonContainer,
StyledInput,
StyledProviderInfo,
StyleGeneral,
StyledAvatar,
} from './style'; } from './style';
import { StyledSettingH2 } from '../style'; import { StyledSettingH2 } from '../style';
import { useState } from 'react'; import { useState } from 'react';
import { Button, TextButton } from '@/ui/button'; import { Button } from '@/ui/button';
import Input from '@/ui/input';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceDelete } from './delete'; import { WorkspaceDelete } from './delete';
import { WorkspaceLeave } from './leave'; import { WorkspaceLeave } from './leave';
import { Upload } from '@/components/file-upload'; import { DoneIcon, UsersIcon } from '@blocksuite/icons';
import { WorkspaceAvatar } from '@/components/workspace-avatar'; // import { Upload } from '@/components/file-upload';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { useTranslation } from '@affine/i18n';
import { CloudIcon, LocalIcon } from '@/components/workspace-modal/icons';
import { CameraIcon } from './icons';
import { Upload } from '@/components/file-upload';
export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => { export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const [showDelete, setShowDelete] = useState<boolean>(false); const [showDelete, setShowDelete] = useState<boolean>(false);
const [showLeave, setShowLeave] = useState<boolean>(false); const [showLeave, setShowLeave] = useState<boolean>(false);
const [uploading, setUploading] = useState<boolean>(false);
const [workspaceName, setWorkspaceName] = useState<string>(workspace.name); const [workspaceName, setWorkspaceName] = useState<string>(workspace.name);
const { currentWorkspace, isOwner } = useAppState(); const { currentWorkspace, isOwner } = useAppState();
const { updateWorkspace } = useWorkspaceHelper(); const { updateWorkspace } = useWorkspaceHelper();
const { t } = useTranslation();
const handleChangeWorkSpaceName = (newName: string) => { const handleChangeWorkSpaceName = (newName: string) => {
setWorkspaceName(newName); setWorkspaceName(newName);
}; };
@@ -32,74 +42,99 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
}; };
const fileChange = async (file: File) => { const fileChange = async (file: File) => {
setUploading(true);
const blob = new Blob([file], { type: file.type }); const blob = new Blob([file], { type: file.type });
currentWorkspace && currentWorkspace &&
(await updateWorkspace({ avatarBlob: blob }, currentWorkspace)); (await updateWorkspace({ avatarBlob: blob }, currentWorkspace));
setUploading(false);
}; };
return workspace ? ( return workspace ? (
<div> <StyleGeneral>
<StyledSettingH2 marginTop={56}>Workspace Icon</StyledSettingH2> <div style={{ flex: 1, overflow: 'auto' }}>
<StyledSettingAvatarContent> <StyledSettingH2>Workspace Avatar</StyledSettingH2>
<div <StyledSettingAvatarContent>
style={{ <StyledAvatar>
float: 'left', <Upload
marginRight: '20px', accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
}} fileChange={fileChange}
> >
<WorkspaceAvatar <>
size={60} <div className="camera-icon">
name={workspace.name} <CameraIcon></CameraIcon>
avatar={workspace.avatar ?? ''} </div>
/> <WorkspaceUnitAvatar
</div> size={60}
<Upload 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" accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange} fileChange={fileChange}
> >
<Button loading={uploading}>Upload</Button> <Button loading={uploading}>{t('Upload')}</Button>
</Upload> </Upload> */}
{/* TODO: add upload logic */} {/* TODO: add upload logic */}
</StyledSettingAvatarContent> </StyledSettingAvatarContent>
<StyledSettingH2 marginTop={20}>Workspace Name</StyledSettingH2> <StyledSettingH2 marginTop={20}>{t('Workspace Name')}</StyledSettingH2>
<StyledSettingInputContainer> <StyledSettingInputContainer>
<Input <StyledInput
width={327} width={284}
value={workspaceName} height={32}
placeholder="Workspace Name" value={workspaceName}
maxLength={14} placeholder={t('Workspace Name')}
minLength={1} maxLength={14}
disabled={!isOwner} minLength={1}
onChange={handleChangeWorkSpaceName} disabled={!isOwner}
></Input> onChange={handleChangeWorkSpaceName}
{isOwner ? ( ></StyledInput>
<TextButton {isOwner ? (
onClick={() => { <StyledDoneButtonContainer
handleUpdateWorkspaceName(); onClick={() => {
}} handleUpdateWorkspaceName();
style={{ marginLeft: '0px' }} }}
> >
✔️ <DoneIcon />
</TextButton> </StyledDoneButtonContainer>
) : null} ) : null}
</StyledSettingInputContainer> </StyledSettingInputContainer>
<StyledSettingH2 marginTop={20}>Workspace Type</StyledSettingH2> <StyledSettingH2 marginTop={20}>{t('Workspace Type')}</StyledSettingH2>
<StyledSettingInputContainer> <StyledSettingInputContainer>
<code>{workspace.provider} </code> {isOwner ? (
</StyledSettingInputContainer> currentWorkspace?.provider === 'local' ? (
<StyledProviderInfo>
<LocalIcon />
Local Workspace
</StyledProviderInfo>
) : (
<StyledProviderInfo>
<CloudIcon />
All data can be accessed offline
</StyledProviderInfo>
)
) : (
<StyledProviderInfo>
<UsersIcon fontSize={20} color={'#FF646B'} />
Joined Workspace
</StyledProviderInfo>
)}
</StyledSettingInputContainer>
</div>
<StyledDeleteButtonContainer> <StyledDeleteButtonContainer>
{isOwner ? ( {isOwner ? (
<> <>
<Button <Button
type="danger" type="danger"
shape="circle" shape="circle"
style={{ borderRadius: '40px' }}
onClick={() => { onClick={() => {
setShowDelete(true); setShowDelete(true);
}} }}
> >
Delete Workspace {t('Delete Workspace')}
</Button> </Button>
<WorkspaceDelete <WorkspaceDelete
open={showDelete} open={showDelete}
@@ -118,7 +153,7 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
setShowLeave(true); setShowLeave(true);
}} }}
> >
Leave Workspace {t('Leave Workspace')}
</Button> </Button>
<WorkspaceLeave <WorkspaceLeave
open={showLeave} open={showLeave}
@@ -129,6 +164,6 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
</> </>
)} )}
</StyledDeleteButtonContainer> </StyledDeleteButtonContainer>
</div> </StyleGeneral>
) : null; ) : null;
}; };

View File

@@ -14,6 +14,7 @@ import { Button } from '@/ui/button';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { Trans, useTranslation } from '@affine/i18n';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
interface WorkspaceDeleteProps { interface WorkspaceDeleteProps {
@@ -28,6 +29,7 @@ export const WorkspaceDelete = ({
workspace, workspace,
}: WorkspaceDeleteProps) => { }: WorkspaceDeleteProps) => {
const [deleteStr, setDeleteStr] = useState<string>(''); const [deleteStr, setDeleteStr] = useState<string>('');
const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { deleteWorkSpace } = useWorkspaceHelper(); const { deleteWorkSpace } = useWorkspaceHelper();
const handlerInputChange = (workspaceName: string) => { const handlerInputChange = (workspaceName: string) => {
@@ -44,31 +46,42 @@ export const WorkspaceDelete = ({
<Modal open={open} onClose={onClose}> <Modal open={open} onClose={onClose}>
<StyledModalWrapper> <StyledModalWrapper>
<ModalCloseButton onClick={onClose} /> <ModalCloseButton onClick={onClose} />
<StyledModalHeader>Delete Workspace</StyledModalHeader> <StyledModalHeader>{t('Delete Workspace')}?</StyledModalHeader>
{workspace.provider === 'local' ? ( {workspace.provider === 'local' ? (
<StyledTextContent> <StyledTextContent>
Deleting ( <Trans i18nKey="Delete Workspace Description">
<StyledWorkspaceName>{workspace.name}</StyledWorkspaceName>) cannot Deleting (
be undone, please proceed with caution. along with all its content. <StyledWorkspaceName>
{{ workspace: workspace.name }}
</StyledWorkspaceName>
) cannot be undone, please proceed with caution. along with all
its content.
</Trans>
</StyledTextContent> </StyledTextContent>
) : ( ) : (
<StyledTextContent> <StyledTextContent>
Deleting ( <Trans i18nKey="Delete Workspace Description2">
<StyledWorkspaceName>{workspace.name}</StyledWorkspaceName>) will Deleting (
delete both local and cloud data, this operation cannot be undone, <StyledWorkspaceName>
please proceed with caution. {{ workspace: workspace.name }}
</StyledWorkspaceName>
) will delete both local and cloud data, this operation cannot be
undone, please proceed with caution.
</Trans>
</StyledTextContent> </StyledTextContent>
)} )}
<StyledInputContent> <StyledInputContent>
<Input <Input
onChange={handlerInputChange} onChange={handlerInputChange}
placeholder="Please type “Delete” to confirm" placeholder={t('Delete Workspace placeholder')}
value={deleteStr} value={deleteStr}
width={284}
height={42}
></Input> ></Input>
</StyledInputContent> </StyledInputContent>
<StyledButtonContent> <StyledButtonContent>
<Button shape="circle" onClick={onClose}> <Button shape="circle" onClick={onClose}>
Cancel {t('Cancel')}
</Button> </Button>
<Button <Button
disabled={deleteStr.toLowerCase() !== 'delete'} disabled={deleteStr.toLowerCase() !== 'delete'}
@@ -77,7 +90,7 @@ export const WorkspaceDelete = ({
shape="circle" shape="circle"
style={{ marginLeft: '24px' }} style={{ marginLeft: '24px' }}
> >
Delete {t('Delete')}
</Button> </Button>
</StyledButtonContent> </StyledButtonContent>
</StyledModalWrapper> </StyledModalWrapper>

View File

@@ -4,20 +4,20 @@ export const StyledModalWrapper = styled('div')(({ theme }) => {
return { return {
position: 'relative', position: 'relative',
padding: '0px', padding: '0px',
width: '460px', width: '560px',
background: theme.colors.popoverBackground, background: theme.colors.popoverBackground,
borderRadius: '12px', borderRadius: '12px',
height: '312px',
}; };
}); });
export const StyledModalHeader = styled('div')(({ theme }) => { export const StyledModalHeader = styled('div')(({ theme }) => {
return { return {
margin: '44px 0px 12px 0px', margin: '44px 0px 12px 0px',
width: '460px', width: '560px',
fontWeight: '600', fontWeight: '600',
fontSize: '20px;', fontSize: '20px;',
textAlign: 'left', textAlign: 'center',
paddingLeft: '20px',
color: theme.colors.popoverColor, color: theme.colors.popoverColor,
}; };
}); });
@@ -37,21 +37,25 @@ export const StyledTextContent = styled('div')(() => {
}; };
}); });
export const StyledInputContent = styled('div')(() => { export const StyledInputContent = styled('div')(({ theme }) => {
return { return {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
margin: '20px 0 24px 0', margin: '32px 0',
fontSize: theme.font.base,
}; };
}); });
export const StyledButtonContent = styled('div')(() => { export const StyledButtonContent = styled('div')(() => {
return { return {
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
bottom: '32px',
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
margin: '0px 0 32px 0',
}; };
}); });

View File

@@ -0,0 +1,20 @@
export const CameraIcon = () => {
return (
<span>
<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="M10.6236 4.25001C10.635 4.25001 10.6467 4.25002 10.6584 4.25002H13.3416C13.3533 4.25002 13.365 4.25001 13.3764 4.25001C13.5609 4.24995 13.7105 4.2499 13.8543 4.26611C14.5981 4.34997 15.2693 4.75627 15.6826 5.38026C15.7624 5.50084 15.83 5.63398 15.9121 5.79586C15.9173 5.80613 15.9226 5.81652 15.9279 5.82703C15.9538 5.87792 15.9679 5.90562 15.9789 5.9261C15.9832 5.9341 15.9857 5.93861 15.9869 5.94065C16.0076 5.97069 16.0435 5.99406 16.0878 5.99905L16.0849 5.99877C16.0849 5.99877 16.0907 5.99918 16.1047 5.99947C16.1286 5.99998 16.1604 6.00002 16.2181 6.00002L17.185 6.00001C17.6577 6 18.0566 5.99999 18.3833 6.02627C18.7252 6.05377 19.0531 6.11364 19.3656 6.27035C19.8402 6.50842 20.2283 6.88944 20.4723 7.36077C20.6336 7.67233 20.6951 7.99944 20.7232 8.33858C20.75 8.66166 20.75 9.05554 20.75 9.51992V16.2301C20.75 16.6945 20.75 17.0884 20.7232 17.4114C20.6951 17.7506 20.6336 18.0777 20.4723 18.3893C20.2283 18.8606 19.8402 19.2416 19.3656 19.4797C19.0531 19.6364 18.7252 19.6963 18.3833 19.7238C18.0566 19.75 17.6578 19.75 17.185 19.75H6.81497C6.34225 19.75 5.9434 19.75 5.61668 19.7238C5.27477 19.6963 4.94688 19.6364 4.63444 19.4797C4.15978 19.2416 3.77167 18.8606 3.52771 18.3893C3.36644 18.0777 3.30494 17.7506 3.27679 17.4114C3.24998 17.0884 3.24999 16.6945 3.25 16.2302V9.51987C3.24999 9.05551 3.24998 8.66164 3.27679 8.33858C3.30494 7.99944 3.36644 7.67233 3.52771 7.36077C3.77167 6.88944 4.15978 6.50842 4.63444 6.27035C4.94688 6.11364 5.27477 6.05377 5.61668 6.02627C5.9434 5.99999 6.34225 6 6.81498 6.00001L7.78191 6.00002C7.83959 6.00002 7.87142 5.99998 7.8953 5.99947C7.90607 5.99924 7.91176 5.99897 7.91398 5.99884C7.95747 5.99343 7.99267 5.9703 8.01312 5.94066C8.01429 5.93863 8.01684 5.93412 8.02113 5.9261C8.0321 5.90561 8.04622 5.87791 8.07206 5.82703C8.07739 5.81653 8.08266 5.80615 8.08787 5.79588C8.17004 5.63397 8.23759 5.50086 8.31745 5.38026C8.73067 4.75627 9.40192 4.34997 10.1457 4.26611C10.2895 4.2499 10.4391 4.24995 10.6236 4.25001ZM10.6584 5.75002C10.422 5.75002 10.3627 5.75114 10.3138 5.75666C10.0055 5.79142 9.73316 5.95919 9.56809 6.20845C9.54218 6.24758 9.51544 6.29761 9.40943 6.50633C9.40611 6.51287 9.40274 6.5195 9.39934 6.52622C9.36115 6.60161 9.31758 6.68761 9.26505 6.76694C8.9964 7.17261 8.56105 7.4354 8.08026 7.48961C7.98625 7.50021 7.89021 7.50011 7.80434 7.50003C7.79678 7.50002 7.7893 7.50002 7.78191 7.50002H6.84445C6.33444 7.50002 5.99634 7.50058 5.73693 7.52144C5.48594 7.54163 5.37478 7.57713 5.30693 7.61115C5.11257 7.70864 4.95675 7.86306 4.85983 8.05029C4.82733 8.11308 4.79194 8.21816 4.77165 8.46266C4.7506 8.71626 4.75 9.0474 4.75 9.55001V16.2C4.75 16.7026 4.7506 17.0338 4.77165 17.2874C4.79194 17.5319 4.82733 17.6369 4.85983 17.6997C4.95675 17.887 5.11257 18.0414 5.30693 18.1389C5.37478 18.1729 5.48594 18.2084 5.73693 18.2286C5.99634 18.2494 6.33444 18.25 6.84445 18.25H17.1556C17.6656 18.25 18.0037 18.2494 18.2631 18.2286C18.5141 18.2084 18.6252 18.1729 18.6931 18.1389C18.8874 18.0414 19.0433 17.887 19.1402 17.6997C19.1727 17.6369 19.2081 17.5319 19.2283 17.2874C19.2494 17.0338 19.25 16.7026 19.25 16.2V9.55001C19.25 9.0474 19.2494 8.71626 19.2283 8.46266C19.2081 8.21816 19.1727 8.11308 19.1402 8.05029C19.0433 7.86306 18.8874 7.70864 18.6931 7.61115C18.6252 7.57713 18.5141 7.54163 18.2631 7.52144C18.0037 7.50058 17.6656 7.50002 17.1556 7.50002H16.2181C16.2107 7.50002 16.2032 7.50002 16.1957 7.50003C16.1098 7.50011 16.0138 7.50021 15.9197 7.48961C15.4389 7.4354 15.0036 7.17261 14.735 6.76694C14.6824 6.68761 14.6389 6.60163 14.6007 6.52622C14.5973 6.5195 14.5939 6.51287 14.5906 6.50633C14.4846 6.29763 14.4578 6.24758 14.4319 6.20846C14.2668 5.95919 13.9945 5.79142 13.6862 5.75666C13.6373 5.75114 13.578 5.75002 13.3416 5.75002H10.6584ZM12 11C10.9303 11 10.0833 11.8506 10.0833 12.875C10.0833 13.8995 10.9303 14.75 12 14.75C13.0697 14.75 13.9167 13.8995 13.9167 12.875C13.9167 11.8506 13.0697 11 12 11ZM8.58333 12.875C8.58333 11 10.1242 9.50002 12 9.50002C13.8758 9.50002 15.4167 11 15.4167 12.875C15.4167 14.7501 13.8758 16.25 12 16.25C10.1242 16.25 8.58333 14.7501 8.58333 12.875Z"
fill="white"
/>
</svg>
</span>
);
};

View File

@@ -7,6 +7,7 @@ import {
} from './style'; } from './style';
import { ModalCloseButton } from '@/ui/modal'; import { ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { useTranslation } from '@affine/i18n';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
// import { getDataCenter } from '@affine/datacenter'; // import { getDataCenter } from '@affine/datacenter';
// import { useAppState } from '@/providers/app-state-provider'; // import { useAppState } from '@/providers/app-state-provider';
@@ -18,6 +19,7 @@ interface WorkspaceDeleteProps {
export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => { export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
const { leaveWorkSpace } = useWorkspaceHelper(); const { leaveWorkSpace } = useWorkspaceHelper();
const { t } = useTranslation();
const handleLeave = async () => { const handleLeave = async () => {
await leaveWorkSpace(); await leaveWorkSpace();
onClose(); onClose();
@@ -27,14 +29,13 @@ export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
<Modal open={open} onClose={onClose}> <Modal open={open} onClose={onClose}>
<StyledModalWrapper> <StyledModalWrapper>
<ModalCloseButton onClick={onClose} /> <ModalCloseButton onClick={onClose} />
<StyledModalHeader>Leave Workspace</StyledModalHeader> <StyledModalHeader>{t('Leave Workspace')}</StyledModalHeader>
<StyledTextContent> <StyledTextContent>
After you leave, you will not be able to access all the contents of {t('Leave Workspace Description')}
this workspace.
</StyledTextContent> </StyledTextContent>
<StyledButtonContent> <StyledButtonContent>
<Button shape="circle" onClick={onClose}> <Button shape="circle" onClick={onClose}>
Cancel {t('Cancel')}
</Button> </Button>
<Button <Button
onClick={handleLeave} onClick={handleLeave}
@@ -42,7 +43,7 @@ export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
shape="circle" shape="circle"
style={{ marginLeft: '24px' }} style={{ marginLeft: '24px' }}
> >
Leave {t('Leave')}
</Button> </Button>
</StyledButtonContent> </StyledButtonContent>
</StyledModalWrapper> </StyledModalWrapper>

View File

@@ -1,15 +1,45 @@
import { styled } from '@/styles'; import { displayFlex, styled } from '@/styles';
import MuiAvatar from '@mui/material/Avatar'; import MuiAvatar from '@mui/material/Avatar';
import IconButton from '@/ui/button/IconButton';
import Input from '@/ui/input';
export const StyledSettingInputContainer = styled('div')(() => { export const StyledSettingInputContainer = styled('div')(() => {
return { return {
marginTop: '12px', marginTop: '12px',
width: '100%',
...displayFlex('flex-start', 'center'),
}; };
}); });
export const StyledDeleteButtonContainer = styled('div')(() => { export const StyledDeleteButtonContainer = styled('div')(() => {
return { return {
marginTop: '30px', 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}`,
borderRadius: '10px',
fontSize: theme.font.sm,
}; };
}); });
@@ -26,3 +56,39 @@ export const StyledSettingAvatarContent = styled('div')(() => {
export const StyledSettingAvatar = styled(MuiAvatar)(() => { export const StyledSettingAvatar = styled(MuiAvatar)(() => {
return { height: '72px', width: '72px', marginRight: '24px' }; 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',
},
};
});
export const StyledAvatar = styled('div')(() => {
return {
position: 'relative',
marginRight: '20px',
cursor: 'pointer',
':hover': {
'.camera-icon': {
display: 'block',
},
},
'.camera-icon': {
position: 'absolute',
display: 'none',
width: '100%',
height: '100%',
borderRadius: '50%',
backgroundColor: 'rgba(60, 61, 63, 0.5)',
top: 0,
left: 0,
textAlign: 'center',
lineHeight: '72px',
},
};
});

View File

@@ -7,6 +7,7 @@ import { useState } from 'react';
import { Avatar } from '@mui/material'; import { Avatar } from '@mui/material';
import useMembers from '@/hooks/use-members'; import useMembers from '@/hooks/use-members';
import { User } from '@affine/datacenter'; import { User } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n';
interface LoginModalProps { interface LoginModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@@ -53,6 +54,7 @@ export const InviteMemberModal = ({
const [showTip, setShowTip] = useState<boolean>(false); const [showTip, setShowTip] = useState<boolean>(false);
const [userData, setUserData] = useState<User | null>(null); const [userData, setUserData] = useState<User | null>(null);
const { inviteMember, getUserByEmail } = useMembers(); const { inviteMember, getUserByEmail } = useMembers();
const { t } = useTranslation();
const inputChange = (value: string) => { const inputChange = (value: string) => {
setShowMember(true); setShowMember(true);
if (gmailReg.test(value)) { if (gmailReg.test(value)) {
@@ -82,7 +84,7 @@ export const InviteMemberModal = ({
/> />
</Header> </Header>
<Content> <Content>
<ContentTitle>Invite members</ContentTitle> <ContentTitle>{t('Invite Members')}</ContentTitle>
<InviteBox> <InviteBox>
<Input <Input
width={360} width={360}
@@ -91,12 +93,12 @@ export const InviteMemberModal = ({
onBlur={() => { onBlur={() => {
setShowMember(false); setShowMember(false);
}} }}
placeholder="Search mail (Gmail support only)" placeholder={t('Invite placeholder')}
></Input> ></Input>
{showMember ? ( {showMember ? (
<Members> <Members>
{showTip ? ( {showTip ? (
<NoFind>Non-Gmail is not supported</NoFind> <NoFind>{t('Non-Gmail')}</NoFind>
) : ( ) : (
<Member> <Member>
{userData?.avatar ? ( {userData?.avatar ? (
@@ -120,12 +122,13 @@ export const InviteMemberModal = ({
<Button <Button
shape="circle" shape="circle"
type="primary" type="primary"
style={{ width: '364px', height: '38px', borderRadius: '40px' }}
onClick={async () => { onClick={async () => {
await inviteMember(email); await inviteMember(email);
onInviteSuccess(); onInviteSuccess();
}} }}
> >
Invite {t('Invite')}
</Button> </Button>
</Footer> </Footer>
</ModalWrapper> </ModalWrapper>
@@ -141,10 +144,8 @@ const Header = styled('div')({
const Content = styled('div')({ const Content = styled('div')({
display: 'flex', display: 'flex',
padding: '0 48px',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
gap: '16px',
}); });
const ContentTitle = styled('h1')({ const ContentTitle = styled('h1')({
@@ -156,9 +157,8 @@ const ContentTitle = styled('h1')({
}); });
const Footer = styled('div')({ const Footer = styled('div')({
height: '70px', height: '102px',
paddingLeft: '24px', margin: '32px 0',
marginTop: '32px',
textAlign: 'center', textAlign: 'center',
}); });

View File

@@ -12,6 +12,7 @@ import {
StyledMoreVerticalButton, StyledMoreVerticalButton,
StyledPublishExplanation, StyledPublishExplanation,
StyledMemberWarp, StyledMemberWarp,
StyledMemberContainer,
} from './style'; } from './style';
import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons'; import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons';
import { useState } from 'react'; import { useState } from 'react';
@@ -25,17 +26,19 @@ import { toast } from '@/ui/toast';
import useMembers from '@/hooks/use-members'; import useMembers from '@/hooks/use-members';
import Loading from '@/components/loading'; import Loading from '@/components/loading';
import { Wrapper } from '@/ui/layout'; import { Wrapper } from '@/ui/layout';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; import { useTranslation } from '@affine/i18n';
import { EnableWorkspaceButton } from '@/components/enable-workspace';
export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => { export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false); const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const { members, removeMember, loaded } = useMembers(); const { members, removeMember, loaded } = useMembers();
const { enableWorkspace } = useWorkspaceHelper();
const { t } = useTranslation();
const { confirm } = useConfirm(); const { confirm } = useConfirm();
if (workspace.provider === 'affine') { if (workspace.provider === 'affine') {
return ( return (
<> <StyledMemberContainer>
<StyledMemberListContainer> <StyledMemberListContainer>
{!loaded && ( {!loaded && (
<Wrapper justifyContent="center"> <Wrapper justifyContent="center">
@@ -45,15 +48,16 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
{loaded && members.length === 0 && ( {loaded && members.length === 0 && (
<Empty width={648} sx={{ marginTop: '60px' }} height={300} /> <Empty width={648} sx={{ marginTop: '60px' }} height={300} />
)} )}
{loaded && members.length && ( {loaded && members.length > 0 && (
<> <>
<StyledMemberTitleContainer> <StyledMemberTitleContainer>
<StyledMemberNameContainer> <StyledMemberNameContainer>
Users({members.length}) {t('Users')} ({members.length})
</StyledMemberNameContainer> </StyledMemberNameContainer>
<StyledMemberRoleContainer> <StyledMemberRoleContainer>
Access level {t('Access level')}
</StyledMemberRoleContainer> </StyledMemberRoleContainer>
<div style={{ width: '24px', paddingRight: '48px' }}></div>
</StyledMemberTitleContainer> </StyledMemberTitleContainer>
{members.map((member, index) => { {members.map((member, index) => {
const user = Object.assign( const user = Object.assign(
@@ -136,7 +140,7 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
type="primary" type="primary"
shape="circle" shape="circle"
> >
Invite Members {t('Invite Members')}
</Button> </Button>
<InviteMemberModal <InviteMemberModal
onClose={() => { onClose={() => {
@@ -150,23 +154,15 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
open={isInviteModalShow} open={isInviteModalShow}
></InviteMemberModal> ></InviteMemberModal>
</StyledMemberButtonContainer> </StyledMemberButtonContainer>
</> </StyledMemberContainer>
); );
} }
return ( return (
<StyledMemberWarp> <StyledMemberWarp>
<>Collaborating with other members requires AFFiNE Cloud service.</> {t('Collaboration Description')}
<StyledPublishExplanation> <StyledPublishExplanation>
<Button <EnableWorkspaceButton></EnableWorkspaceButton>
type="primary"
shape="circle"
onClick={async () => {
await enableWorkspace();
}}
>
Enable AFFiNE Cloud
</Button>
</StyledPublishExplanation> </StyledPublishExplanation>
</StyledMemberWarp> </StyledMemberWarp>
); );

View File

@@ -1,14 +1,21 @@
import { styled } from '@/styles'; import { styled } from '@/styles';
import MuiAvatar from '@mui/material/Avatar'; import MuiAvatar from '@mui/material/Avatar';
export const StyledMemberTitleContainer = styled('div')(() => { export const StyledMemberTitleContainer = styled('li')(() => {
return { return {
display: 'flex', display: 'flex',
marginTop: '60px',
fontWeight: '500', fontWeight: '500',
marginBottom: '32px',
flex: 1, flex: 1,
}; };
}); });
export const StyledMemberContainer = styled('div')(() => {
return {
display: 'flex',
height: '100%',
flexDirection: 'column',
};
});
export const StyledMemberAvatar = styled(MuiAvatar)(() => { export const StyledMemberAvatar = styled(MuiAvatar)(() => {
return { height: '40px', width: '40px' }; return { height: '40px', width: '40px' };
@@ -18,7 +25,7 @@ export const StyledMemberNameContainer = styled('div')(() => {
return { return {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '402px', flex: '2 0 402px',
}; };
}); });
@@ -26,14 +33,15 @@ export const StyledMemberRoleContainer = styled('div')(() => {
return { return {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '222px', flex: '1 0 222px',
}; };
}); });
export const StyledMemberListContainer = styled('ul')(() => { export const StyledMemberListContainer = styled('ul')(() => {
return { return {
marginTop: '15px',
overflowY: 'scroll', overflowY: 'scroll',
width: '100%',
flex: 1,
}; };
}); });
@@ -42,6 +50,7 @@ export const StyledMemberListItem = styled('li')(() => {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
height: '72px', height: '72px',
width: '100%',
}; };
}); });
@@ -55,7 +64,7 @@ export const StyledMemberName = styled('div')(({ theme }) => {
return { return {
fontWeight: '400', fontWeight: '400',
fontSize: '18px', fontSize: '18px',
lineHeight: '16px', lineHeight: '26px',
color: theme.colors.textColor, color: theme.colors.textColor,
}; };
}); });
@@ -71,7 +80,9 @@ export const StyledMemberEmail = styled('div')(({ theme }) => {
export const StyledMemberButtonContainer = styled('div')(() => { export const StyledMemberButtonContainer = styled('div')(() => {
return { return {
marginTop: '14px', position: 'absolute',
bottom: '0',
marginBottom: '20px',
}; };
}); });
@@ -83,6 +94,7 @@ export const StyledMoreVerticalButton = styled('button')(() => {
width: '24px', width: '24px',
height: '24px', height: '24px',
cursor: 'pointer', cursor: 'pointer',
paddingRight: '48px',
}; };
}); });
@@ -93,7 +105,7 @@ export const StyledPublishExplanation = styled('div')(() => {
fontSize: '18px', fontSize: '18px',
lineHeight: '26px', lineHeight: '26px',
flex: 1, flex: 1,
marginTop: '60px', marginTop: '64px',
}; };
}); });
@@ -101,6 +113,8 @@ export const StyledMemberWarp = styled('div')(() => {
return { return {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
padding: '40px 0', padding: '0 0 48px 0',
fontWeight: '500',
fontSize: '18px',
}; };
}); });

View File

@@ -3,25 +3,27 @@ import { styled } from '@/styles';
export const StyledSettingContainer = styled('div')(() => { export const StyledSettingContainer = styled('div')(() => {
return { return {
display: 'flex', display: 'flex',
padding: '50px',
flexDirection: 'column', flexDirection: 'column',
padding: '0 34px 20px 48px',
height: '100vh', height: '100vh',
overflow: 'auto',
}; };
}); });
export const StyledSettingSidebar = styled('div')(() => { export const StyledSettingSidebar = styled('div')(() => {
{ {
return { return {
flexShrink: 0, // height: '48px',
flexGrow: 0, marginTop: '50px',
}; };
} }
}); });
export const StyledSettingContent = styled('div')(() => { export const StyledSettingContent = styled('div')(() => {
return { return {
paddingLeft: '48px', overflow: 'hidden',
flex: 1,
paddingTop: '48px',
}; };
}); });
@@ -50,7 +52,6 @@ export const StyledSettingTabContainer = styled('ul')(() => {
{ {
return { return {
display: 'flex', display: 'flex',
marginTop: '25px',
}; };
} }
}); });
@@ -60,14 +61,18 @@ export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
{ {
return { return {
display: 'flex', display: 'flex',
marginBottom: '12px', margin: '0 48px 0 0',
padding: '0 24px', height: '34px',
height: '32px',
color: isActive ? theme.colors.primaryColor : theme.colors.textColor, color: isActive ? theme.colors.primaryColor : theme.colors.textColor,
fontWeight: '400', fontWeight: '500',
fontSize: '16px', fontSize: theme.font.base,
lineHeight: '32px', lineHeight: theme.font.lineHeightBase,
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.15s ease',
borderBottom: `2px solid ${
isActive ? theme.colors.primaryColor : 'none'
}`,
':hover': { color: theme.colors.primaryColor },
}; };
} }
} }
@@ -77,22 +82,21 @@ export const StyledSettingTagIconContainer = styled('div')(() => {
return { return {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
marginRight: '14.64px',
width: '14.47px',
fontSize: '14.47px',
}; };
}); });
export const StyledSettingH2 = styled('h2')<{ marginTop?: number }>( export const StyledSettingH2 = styled('h2')<{
({ marginTop }) => { marginTop?: number;
return { marginBottom?: number;
fontWeight: '500', }>(({ marginTop, marginBottom, theme }) => {
fontSize: '18px', return {
lineHeight: '26px', fontWeight: '500',
marginTop: marginTop ? `${marginTop}px` : '0px', fontSize: theme.font.base,
}; lineHeight: theme.font.lineHeightBase,
} marginTop: marginTop ? `${marginTop}px` : '0px',
); marginBottom: marginBottom ? `${marginBottom}px` : '0px',
};
});
export const StyledPublishExplanation = styled('div')(() => { export const StyledPublishExplanation = styled('div')(() => {
return { return {
@@ -100,25 +104,41 @@ export const StyledPublishExplanation = styled('div')(() => {
fontWeight: '500', fontWeight: '500',
fontSize: '18px', fontSize: '18px',
lineHeight: '26px', lineHeight: '26px',
flex: 1, marginBottom: '22px',
marginTop: '60px', };
});
export const StyledWorkspaceName = styled('span')(() => {
return {
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
};
});
export const StyledWorkspaceType = styled('span')(() => {
return {
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
}; };
}); });
export const StyledPublishCopyContainer = styled('div')(() => { export const StyledPublishCopyContainer = styled('div')(() => {
return { return {
marginTop: '12px', marginTop: '12px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginBottom: '20px', marginBottom: '20px',
paddingTop: '20px', paddingTop: '20px',
flex: 1,
};
});
export const StyledStopPublishContainer = styled('div')(() => {
return {
textAlign: 'center',
}; };
}); });
export const StyledCopyButtonContainer = styled('div')(() => { export const StyledCopyButtonContainer = styled('div')(() => {
return { return {
marginLeft: '12px', marginTop: '64px',
}; };
}); });
@@ -128,3 +148,9 @@ export const StyledPublishContent = styled('div')(() => {
flexDirection: 'column', flexDirection: 'column',
}; };
}); });
export const StyleAsync = styled('div')(() => {
return {
marginTop: '64px',
};
});

View File

@@ -1,183 +0,0 @@
// import { InformationIcon, LogOutIcon } from '@blocksuite/icons';
// import { styled } from '@/styles';
// import { Divider } from '@/ui/divider';
// import { useAppState } from '@/providers/app-state-provider';
// import { SelectorPopperContainer } from './styles';
// import {
// PrivateWorkspaceItem,
// WorkspaceItem,
// CreateWorkspaceItem,
// ListItem,
// LoginItem,
// } from './WorkspaceItem';
// import { WorkspaceSetting } from '@/components/workspace-setting';
// import { useCallback, useEffect, useState } from 'react';
// import { WorkspaceInfo } from '@affine/datacenter';
// import { useModal } from '@/providers/GlobalModalProvider';
// export type WorkspaceDetails = Record<
// string,
// { memberCount: number; owner: { id: string; name: string } }
// >;
// type SelectorPopperContentProps = {
// isShow: boolean;
// };
// export const SelectorPopperContent = ({
// isShow,
// }: SelectorPopperContentProps) => {
// const { user, workspacesMeta, refreshWorkspacesMeta } = useAppState();
// const [settingWorkspaceId, setSettingWorkspaceId] = useState<string | null>(
// null
// );
// const [workSpaceDetails, setWorkSpaceDetails] = useState<WorkspaceDetails>(
// {}
// );
// const { triggerContactModal } = useModal();
// const handleClickSettingWorkspace = (workspaceId: string) => {
// setSettingWorkspaceId(workspaceId);
// };
// const handleCloseWorkSpace = () => {
// setSettingWorkspaceId(null);
// };
// const settingWorkspace = settingWorkspaceId
// ? workspacesMeta.find(workspace => workspace.id === settingWorkspaceId)
// : undefined;
// const refreshDetails = useCallback(async () => {
// const workspaceDetailList = await Promise.all(
// workspacesMeta.map(async ({ id, type }) => {
// if (user) {
// if (type === WorkspaceType.Private) {
// return { id, member_count: 1, owner: user };
// } else {
// // const dc = await getDataCenter();
// // const data = await dc.apis.getWorkspaceDetail({ id });
// // return { id, ...data } || { id, member_count: 0, owner: user };
// }
// }
// })
// );
// const workSpaceDetails: WorkspaceDetails = {};
// workspaceDetailList.forEach(details => {
// if (details) {
// const { id, member_count, owner } = details;
// if (!owner) return;
// workSpaceDetails[id] = {
// memberCount: member_count || 1,
// owner: {
// id: owner.id,
// name: owner.name,
// },
// };
// }
// });
// setWorkSpaceDetails(workSpaceDetails);
// }, [user, workspacesMeta]);
// useEffect(() => {
// if (isShow) {
// setSettingWorkspaceId(null);
// refreshWorkspacesMeta();
// refreshDetails();
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [isShow]);
// return !user ? (
// <SelectorPopperContainer placement="bottom-start">
// <LoginItem />
// <StyledDivider />
// <ListItem
// icon={<InformationIcon />}
// name="About AFFiNE"
// onClick={() => triggerContactModal()}
// />
// </SelectorPopperContainer>
// ) : (
// <SelectorPopperContainer placement="bottom-start">
// <PrivateWorkspaceItem
// privateWorkspaceId={
// workspacesMeta.find(
// workspace => workspace.type === WorkspaceType.Private
// )?.id
// }
// />
// <StyledDivider />
// <WorkspaceGroupTitle>Workspace</WorkspaceGroupTitle>
// <WorkspaceWrapper>
// {workspacesMeta.map(workspace => {
// return workspace.type !== WorkspaceType.Private ? (
// <WorkspaceItem
// type={workspace.type}
// key={workspace.id}
// id={workspace.id}
// icon={`loading...`}
// onClickSetting={handleClickSettingWorkspace}
// name={`loading...`}
// memberCount={workSpaceDetails[workspace.id]?.memberCount || 1}
// />
// ) : null;
// })}
// </WorkspaceWrapper>
// <CreateWorkspaceItem />
// {settingWorkspace ? (
// <WorkspaceSetting
// isShow={false}
// onClose={handleCloseWorkSpace}
// // workspace={settingWorkspace}
// // owner={
// // (settingWorkspaceId &&
// // workSpaceDetails[settingWorkspaceId]?.owner) || {
// // id: user.id,
// // name: user.name,
// // }
// // }
// />
// ) : null}
// <StyledDivider />
// <ListItem
// icon={<InformationIcon />}
// name="About AFFiNE"
// onClick={() => triggerContactModal()}
// />
// <ListItem
// icon={<LogOutIcon />}
// name="Sign out"
// onClick={() => {
// console.log('Sign out');
// // FIXME: remove token from local storage and reload the page
// localStorage.removeItem('affine_token');
// window.location.reload();
// }}
// />
// </SelectorPopperContainer>
// );
// };
// const StyledDivider = styled(Divider)({
// margin: '8px 12px',
// width: 'calc(100% - 24px)',
// });
// const WorkspaceGroupTitle = styled('div')(({ theme }) => {
// return {
// color: theme.colors.iconColor,
// fontSize: theme.font.sm,
// lineHeight: '30px',
// height: '30px',
// padding: '0 12px',
// };
// });
// const WorkspaceWrapper = styled('div')(() => {
// return {
// maxHeight: '200px',
// overflow: 'auto',
// };
// });
export const test = () => {
return <></>;
};

View File

@@ -1,39 +0,0 @@
import { useState } from 'react';
import { AddIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import {
WorkspaceItemAvatar,
WorkspaceItemWrapper,
WorkspaceItemContent,
} from '../styles';
import { WorkspaceCreate } from './workspace-create';
const name = 'Create new Workspace';
export const CreateWorkspaceItem = () => {
const [open, setOpen] = useState(false);
return (
<>
<WorkspaceItemWrapper onClick={() => setOpen(true)}>
<WorkspaceItemAvatar>
<AddIcon />
</WorkspaceItemAvatar>
<WorkspaceItemContent>
<Name title={name}>{name}</Name>
</WorkspaceItemContent>
</WorkspaceItemWrapper>
<WorkspaceCreate open={open} onClose={() => setOpen(false)} />
</>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.base,
fontWeight: 400,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});

View File

@@ -1,123 +0,0 @@
// import { getDataCenter } from '@affine/datacenter';
import Modal from '@/ui/modal';
import Input from '@/ui/input';
import {
StyledModalHeader,
StyledTextContent,
StyledModalWrapper,
StyledInputContent,
StyledButtonContent,
StyledButton,
} from './style';
import { useState } from 'react';
import { ModalCloseButton } from '@/ui/modal';
// import router from 'next/router';
// import { useAppState } from '@/providers/app-state-provider';
interface WorkspaceCreateProps {
open: boolean;
onClose: () => void;
}
const DefaultHeadImgColors = [
['#C6F2F3', '#0C6066'],
['#FFF5AB', '#896406'],
['#FFCCA7', '#8F4500'],
['#FFCECE', '#AF1212'],
['#E3DEFF', '#511AAB'],
];
export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => {
const [workspaceName, setWorkspaceId] = useState<string>('');
const [creating, setCreating] = useState<boolean>(false);
// const { refreshWorkspacesMeta } = useAppState();
const handlerInputChange = (workspaceName: string) => {
setWorkspaceId(workspaceName);
};
const createDefaultHeadImg = (workspaceName: string) => {
const canvas = document.createElement('canvas');
canvas.height = 100;
canvas.width = 100;
const ctx = canvas.getContext('2d');
return new Promise<string>((resolve, reject) => {
if (ctx) {
const randomNumber = Math.floor(Math.random() * 5);
const randomColor = DefaultHeadImgColors[randomNumber];
ctx.fillStyle = randomColor[0];
ctx.fillRect(0, 0, 100, 100);
ctx.font = "600 50px 'PingFang SC', 'Microsoft Yahei'";
ctx.fillStyle = randomColor[1];
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(workspaceName[0], 50, 50);
canvas.toBlob(blob => {
if (blob) {
// const blobId = getDataCenter().then(dc =>
// dc.apis.uploadBlob({ blob })
// );
// resolve(blobId);
} else {
reject();
}
}, 'image/png');
} else {
reject();
}
});
};
const handleCreateWorkspace = async () => {
setCreating(true);
const blobId = await createDefaultHeadImg(workspaceName).catch(() => {
setCreating(false);
});
if (blobId) {
// getDataCenter()
// .then(dc =>
// dc.apis.createWorkspace({ name: workspaceName, avatar: blobId })
// )
// .then(async data => {
// await refreshWorkspacesMeta();
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// // @ts-ignore
// router.push(`/workspace/${data.id}`);
// onClose();
// })
// .catch(err => {
// console.log(err, 'err');
// })
// .finally(() => {
// setCreating(false);
// });
}
};
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>Create new Workspace</StyledModalHeader>
<StyledTextContent>
Workspaces are shared environments where teams can collaborate. After
creating a Workspace, you can invite others to join.
</StyledTextContent>
<StyledInputContent>
<Input
onChange={handlerInputChange}
placeholder="Set a Workspace name"
value={workspaceName}
></Input>
</StyledInputContent>
<StyledButtonContent>
<StyledButton
disabled={!workspaceName.length || creating}
onClick={handleCreateWorkspace}
loading={creating}
>
Create
</StyledButton>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};
export default WorkspaceCreate;

View File

@@ -1,63 +0,0 @@
import { styled } from '@/styles';
import { Button } from '@/ui/button';
export const StyledModalWrapper = styled('div')(({ theme }) => {
return {
position: 'relative',
padding: '0px',
width: '460px',
background: theme.colors.popoverBackground,
borderRadius: '12px',
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
margin: '44px 0px 12px 0px',
width: '460px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
color: theme.colors.popoverColor,
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'center',
};
});
export const StyledInputContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '40px 0 24px 0',
};
});
export const StyledButtonContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '0px 0 32px 0',
};
});
export const StyledButton = styled(Button)(() => {
return {
width: '260px',
justifyContent: 'center',
};
});

View File

@@ -1,63 +0,0 @@
// import { styled } from '@/styles';
// import { useAppState } from '@/providers/app-state-provider';
// import {
// WorkspaceItemAvatar,
// PrivateWorkspaceWrapper,
// WorkspaceItemContent,
// } from './styles';
// import { useRouter } from 'next/router';
// type PrivateWorkspaceItemProps = {
// privateWorkspaceId?: string;
// };
// export const PrivateWorkspaceItem = ({
// privateWorkspaceId,
// }: PrivateWorkspaceItemProps) => {
// const { user } = useAppState();
// const router = useRouter();
// const handleClick = () => {
// if (privateWorkspaceId) {
// router.push(`/workspace/${privateWorkspaceId}`);
// }
// };
// if (user) {
// const Username = user.name;
// return (
// <PrivateWorkspaceWrapper onClick={handleClick}>
// <WorkspaceItemAvatar alt={Username} src={user.avatar_url}>
// {Username}
// </WorkspaceItemAvatar>
// <WorkspaceItemContent>
// <Name title={Username}>{Username}</Name>
// <Email title={user.email}>{user.email}</Email>
// </WorkspaceItemContent>
// </PrivateWorkspaceWrapper>
// );
// }
// return null;
// };
// const Name = styled('div')(({ theme }) => {
// return {
// color: theme.colors.quoteColor,
// fontSize: theme.font.base,
// fontWeight: 500,
// overflow: 'hidden',
// textOverflow: 'ellipsis',
// whiteSpace: 'nowrap',
// };
// });
// const Email = styled('div')(({ theme }) => {
// return {
// color: theme.colors.iconColor,
// fontSize: theme.font.sm,
// overflow: 'hidden',
// textOverflow: 'ellipsis',
// whiteSpace: 'nowrap',
// };
// });
export const test = () => {
return <></>;
};

View File

@@ -1,32 +0,0 @@
import { SettingsIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import { IconButton } from '@/ui/button';
import { MouseEventHandler } from 'react';
type SettingProps = {
onClick?: () => void;
};
export const FooterSetting = ({ onClick }: SettingProps) => {
const handleClick: MouseEventHandler<HTMLButtonElement> = e => {
e.stopPropagation();
onClick && onClick();
};
return (
<Wrapper
className="footer-setting"
onClick={e => {
e.stopPropagation();
handleClick(e);
}}
>
<SettingsIcon />
</Wrapper>
);
};
const Wrapper = styled(IconButton)(() => {
return {
fontSize: '20px',
};
});

View File

@@ -1,29 +0,0 @@
import { UsersIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import { IconButton } from '@/ui/button';
type FooterUsersProps = {
memberCount: number;
};
export const FooterUsers = ({ memberCount = 1 }: FooterUsersProps) => {
return (
<Wrapper className="footer-users">
<>
<UsersIcon />
<Tip>{memberCount > 99 ? '99+' : memberCount}</Tip>
</>
</Wrapper>
);
};
const Wrapper = styled(IconButton)({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '16px',
});
const Tip = styled('span')({
fontSize: '12px',
});

View File

@@ -1,100 +0,0 @@
// import { useRouter } from 'next/router';
// import { styled } from '@/styles';
// import {
// WorkspaceItemAvatar,
// WorkspaceItemWrapper,
// WorkspaceItemContent,
// } from '../styles';
// import { FooterSetting } from './FooterSetting';
// import { FooterUsers } from './FooterUsers';
// import { WorkspaceType } from '@affine/datacenter';
// import { useAppState } from '@/providers/app-state-provider';
// interface WorkspaceItemProps {
// id: string;
// name: string;
// icon: string;
// type: WorkspaceType;
// memberCount: number;
// onClickSetting?: (workspaceId: string) => void;
// }
// export const WorkspaceItem = ({
// id,
// name,
// icon,
// type,
// onClickSetting,
// memberCount,
// }: WorkspaceItemProps) => {
// const router = useRouter();
// const { currentWorkspaceId } = useAppState();
// const handleClickSetting = async () => {
// onClickSetting && onClickSetting(id);
// };
// return (
// <StyledWrapper
// onClick={() => {
// router.push(`/workspace/${id}`);
// }}
// canSet={
// type !== WorkspaceType.Private && currentWorkspaceId === String(id)
// }
// >
// <WorkspaceItemAvatar alt={name} src={icon}>
// {name.charAt(0)}
// </WorkspaceItemAvatar>
// <WorkspaceItemContent>
// <Name title={name}>{name}</Name>
// </WorkspaceItemContent>
// <Footer>
// <FooterUsers memberCount={memberCount} />
// <FooterSetting onClick={handleClickSetting} />
// </Footer>
// </StyledWrapper>
// );
// };
// const Name = styled('div')(({ theme }) => {
// return {
// color: theme.colors.quoteColor,
// fontSize: theme.font.sm,
// fontWeight: 400,
// overflow: 'hidden',
// textOverflow: 'ellipsis',
// whiteSpace: 'nowrap',
// };
// });
// const StyledWrapper = styled(WorkspaceItemWrapper)<{ canSet: boolean }>(
// ({ canSet }) => {
// return {
// '& .footer-setting': {
// display: 'none',
// },
// ':hover .footer-users': {
// display: canSet ? 'none' : '',
// },
// ':hover .footer-setting': {
// display: canSet ? 'block' : 'none',
// },
// };
// }
// );
// const Footer = styled('div')({
// width: '42px',
// flex: '0 42px',
// fontSize: '20px',
// display: 'flex',
// alignItems: 'center',
// justifyContent: 'center',
// marginLeft: '12px',
// });
export const test = () => {
return <></>;
};

View File

@@ -1,5 +1,2 @@
export * from './PrivateWorkspaceItem';
// export * from './WorkspaceItem';
export * from './CreateWorkspaceItem';
export * from './ListItem'; export * from './ListItem';
export * from './LoginItem'; export * from './LoginItem';

View File

@@ -1,7 +1,7 @@
import { Avatar, WorkspaceName, SelectorWrapper } from './styles'; import { Avatar, WorkspaceName, SelectorWrapper } from './styles';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { WorkspaceModal } from '@/components/workspace-modal'; import { WorkspaceModal } from '@/components/workspace-modal';
import { WorkspaceAvatar } from '@/components/workspace-avatar'; import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
export const WorkspaceSelector = () => { export const WorkspaceSelector = () => {
const [workspaceListShow, setWorkspaceListShow] = useState(false); const [workspaceListShow, setWorkspaceListShow] = useState(false);
@@ -23,17 +23,17 @@ export const WorkspaceSelector = () => {
<Avatar <Avatar
alt="Affine" alt="Affine"
data-testid="workspace-avatar" data-testid="workspace-avatar"
src={currentWorkspace?.avatar} // src={currentWorkspace?.avatar}
> >
<div <div
style={{ style={{
float: 'left', float: 'left',
}} }}
> >
<WorkspaceAvatar <WorkspaceUnitAvatar
size={28} size={28}
name={currentWorkspace?.name ?? 'AFFiNE'} name={currentWorkspace?.name ?? 'AFFiNE'}
avatar={currentWorkspace?.avatar ?? ''} workspaceUnit={currentWorkspace}
/> />
</div> </div>
</Avatar> </Avatar>

View File

@@ -84,7 +84,6 @@ export const WorkSpaceSliderBar = () => {
? `/workspace/${currentWorkspaceId}/setting` ? `/workspace/${currentWorkspaceId}/setting`
: '', : '',
}; };
return ( return (
<> <>
<StyledSliderBar show={show}> <StyledSliderBar show={show}>
@@ -152,7 +151,7 @@ export const WorkSpaceSliderBar = () => {
<StyledListItem active={router.asPath === paths.setting}> <StyledListItem active={router.asPath === paths.setting}>
<StyledLink href={{ pathname: paths.setting }}> <StyledLink href={{ pathname: paths.setting }}>
<SettingsIcon /> <SettingsIcon />
Settings {t('Settings')}
</StyledLink> </StyledLink>
</StyledListItem> </StyledListItem>

View File

@@ -22,7 +22,7 @@ export const useMembers = () => {
const inviteMember = async (email: string) => { const inviteMember = async (email: string) => {
currentWorkspace && currentWorkspace &&
dataCenter && dataCenter &&
(await dataCenter.inviteMember(currentWorkspace?.id, email)); (await dataCenter.inviteMember(currentWorkspace.id, email));
}; };
const removeMember = async (permissionId: number) => { const removeMember = async (permissionId: number) => {

View File

@@ -42,6 +42,8 @@ export const useWorkspaceHelper = () => {
content: `If enabled, the data in this workspace will be backed up and synchronized via 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', confirmText: user ? 'Enable' : 'Sign in and Enable',
cancelText: 'Skip', cancelText: 'Skip',
confirmType: 'primary',
buttonDirection: 'column',
}).then(async confirm => { }).then(async confirm => {
if (confirm && currentWorkspace) { if (confirm && currentWorkspace) {
if (!user) { if (!user) {
@@ -57,11 +59,18 @@ export const useWorkspaceHelper = () => {
}; };
const deleteWorkSpace = async () => { const deleteWorkSpace = async () => {
currentWorkspace && currentWorkspace && (await dataCenter.deleteWorkspace(currentWorkspace.id));
(await dataCenter.deleteWorkspace(currentWorkspace?.id));
}; };
const leaveWorkSpace = async () => { const leaveWorkSpace = async () => {
currentWorkspace && (await dataCenter.leaveWorkspace(currentWorkspace?.id)); currentWorkspace && (await dataCenter.leaveWorkspace(currentWorkspace.id));
};
const acceptInvite = async (inviteCode: string) => {
let inviteInfo;
if (inviteCode) {
inviteInfo = await dataCenter.acceptInvitation(inviteCode);
}
return inviteInfo;
}; };
return { return {
@@ -71,5 +80,6 @@ export const useWorkspaceHelper = () => {
enableWorkspace, enableWorkspace,
deleteWorkSpace, deleteWorkSpace,
leaveWorkSpace, leaveWorkSpace,
acceptInvite,
}; };
}; };

View File

@@ -1,9 +1,5 @@
import NotfoundPage from '@/components/404'; import NotfoundPage from '@/components/404';
import { useAppState } from '@/providers/app-state-provider';
export default function Custom404() { export default function Custom404() {
const { workspaceList } = useAppState();
console.log('workspaceList: ', workspaceList);
return <NotfoundPage></NotfoundPage>; return <NotfoundPage></NotfoundPage>;
} }

View File

@@ -1,45 +1,35 @@
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { styled } from '@/styles'; import { styled } from '@/styles';
import { Empty } from '@/ui/empty'; import { Empty } from '@/ui/empty';
import { Avatar } from '@mui/material'; // import { Avatar } from '@mui/material';
// import { getDataCenter } from '@affine/datacenter';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
const User = ({ name, avatar }: { name: string; avatar?: string }) => { // const User = ({ name, avatar }: { name: string; avatar?: string }) => {
return ( // return (
<UserContent> // <UserContent>
{avatar ? ( // {avatar ? (
<Avatar src={avatar}></Avatar> // <Avatar src={avatar}></Avatar>
) : ( // ) : (
<UserIcon>{name.slice(0, 1)}</UserIcon> // <UserIcon>{name.slice(0, 1)}</UserIcon>
)} // )}
<span>{name}</span> // <span>{name}</span>
</UserContent> // </UserContent>
); // );
}; // };
export default function DevPage() { export default function DevPage() {
const router = useRouter(); const router = useRouter();
const [successInvited, setSuccessInvited] = useState<boolean>(false); const [successInvited, setSuccessInvited] = useState<boolean>(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any const { acceptInvite } = useWorkspaceHelper();
const [inviteData, setInviteData] = useState<any>(null);
useEffect(() => { useEffect(() => {
// getDataCenter() router.query.invite_code &&
// .then(dc => acceptInvite(router.query.invite_code as string).then(data => {
// dc.apis.acceptInviting({ if (data && data.accepted) {
// invitingCode: router.query.invite_code as string, setSuccessInvited(true);
// }) }
// ) });
// .then(data => { }, [router, acceptInvite]);
// setSuccessInvited(true);
// setInviteData(data);
// })
// .catch(err => {
// console.log('err: ', err);
// });
setSuccessInvited(true);
setInviteData(null);
}, [router.query.invite_code]);
return ( return (
<Invited> <Invited>
@@ -47,11 +37,12 @@ export default function DevPage() {
<Empty width={310} height={310}></Empty> <Empty width={310} height={310}></Empty>
<Content> <Content>
<User name={inviteData?.name ? inviteData.name : '-'}></User> invited {/* TODO add inviteInfo*/}
you to join {/* <User name={inviteData?.name ? inviteData.name : '-'}></User> invited */}
<User {/* you to join */}
{/* <User
name={inviteData?.workspaceName ? inviteData.workspaceName : '-'} name={inviteData?.workspaceName ? inviteData.workspaceName : '-'}
></User> ></User> */}
{successInvited ? ( {successInvited ? (
<Status> <Status>
<svg <svg
@@ -104,16 +95,16 @@ export default function DevPage() {
</Invited> </Invited>
); );
} }
const UserIcon = styled('div')({ // const UserIcon = styled('div')({
display: 'inline-block', // display: 'inline-block',
width: '28px', // width: '28px',
height: '28px', // height: '28px',
borderRadius: '50%', // borderRadius: '50%',
backgroundColor: '#FFF5AB', // backgroundColor: '#FFF5AB',
textAlign: 'center', // textAlign: 'center',
color: '#896406', // color: '#896406',
lineHeight: '28px', // lineHeight: '28px',
}); // });
const Invited = styled('div')(({ theme }) => { const Invited = styled('div')(({ theme }) => {
return { return {
@@ -132,14 +123,6 @@ const Content = styled('div')({
marginTop: '35px', marginTop: '35px',
}); });
const UserContent = styled('span')({
fontSize: '18px',
marginLeft: '12px',
span: {
padding: '0 12px',
},
});
const Status = styled('div')(() => { const Status = styled('div')(() => {
return { return {
marginTop: '16px', marginTop: '16px',

View File

@@ -5,6 +5,8 @@ import { styled } from '@/styles';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { Page as PageStore, Workspace } from '@blocksuite/store'; import { Page as PageStore, Workspace } from '@blocksuite/store';
import { PageLoading } from '@/components/loading';
const DynamicBlocksuite = dynamic(() => import('@/components/editor'), { const DynamicBlocksuite = dynamic(() => import('@/components/editor'), {
ssr: false, ssr: false,
}); });
@@ -13,6 +15,8 @@ const Page: NextPageWithLayout = () => {
const [page, setPage] = useState<PageStore>(); const [page, setPage] = useState<PageStore>();
const { dataCenter } = useAppState(); const { dataCenter } = useAppState();
const router = useRouter(); const router = useRouter();
const [loaded, setLoaded] = useState(false);
useEffect(() => { useEffect(() => {
dataCenter dataCenter
.loadPublicWorkspace(router.query.workspaceId as string) .loadPublicWorkspace(router.query.workspaceId as string)
@@ -25,7 +29,7 @@ const Page: NextPageWithLayout = () => {
p => p.id === router.query.pageId p => p.id === router.query.pageId
) )
) { ) {
const page = data.blocksuiteWorkspace?.getPage( const page = data.blocksuiteWorkspace.getPage(
router.query.pageId as string router.query.pageId as string
); );
page && setPage(page); page && setPage(page);
@@ -39,17 +43,21 @@ const Page: NextPageWithLayout = () => {
}); });
}, [router, dataCenter]); }, [router, dataCenter]);
return ( return (
<PageContainer> <>
{workspace && page && ( {!loaded && <PageLoading />}
<DynamicBlocksuite <PageContainer>
page={page} {workspace && page && (
workspace={workspace} <DynamicBlocksuite
setEditor={editor => { page={page}
editor.readonly = true; workspace={workspace}
}} setEditor={editor => {
/> editor.readonly = true;
)} setLoaded(true);
</PageContainer> }}
/>
)}
</PageContainer>
</>
); );
}; };

View File

@@ -14,6 +14,7 @@ const All = () => {
<PageList <PageList
pageList={pageList.filter(p => !p.trash)} pageList={pageList.filter(p => !p.trash)}
showFavoriteTag={true} showFavoriteTag={true}
listType="all"
/> />
</> </>
); );

View File

@@ -13,7 +13,10 @@ export const Favorite = () => {
<PageListHeader icon={<FavouritesIcon />}> <PageListHeader icon={<FavouritesIcon />}>
{t('Favourites')} {t('Favourites')}
</PageListHeader> </PageListHeader>
<PageList pageList={pageList.filter(p => p.favorite && !p.trash)} /> <PageList
pageList={pageList.filter(p => p.favorite && !p.trash)}
listType="favorite"
/>
</> </>
); );
}; };

View File

@@ -2,17 +2,9 @@ import {
StyledSettingContainer, StyledSettingContainer,
StyledSettingContent, StyledSettingContent,
StyledSettingSidebar, StyledSettingSidebar,
StyledSettingSidebarHeader,
StyledSettingTabContainer, StyledSettingTabContainer,
StyledSettingTagIconContainer,
WorkspaceSettingTagItem, WorkspaceSettingTagItem,
} from '@/components/workspace-setting/style'; } from '@/components/workspace-setting/style';
import {
EditIcon,
UsersIcon,
PublishIcon,
CloudInsyncIcon,
} from '@blocksuite/icons';
import { ReactElement, ReactNode, useState } from 'react'; import { ReactElement, ReactNode, useState } from 'react';
import { import {
GeneralPage, GeneralPage,
@@ -21,45 +13,44 @@ import {
ExportPage, ExportPage,
SyncPage, SyncPage,
} from '@/components/workspace-setting'; } from '@/components/workspace-setting';
import { SettingsIcon } from '@blocksuite/icons';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import WorkspaceLayout from '@/components/workspace-layout'; import WorkspaceLayout from '@/components/workspace-layout';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n';
import { PageListHeader } from '@/components/header';
type TabNames = 'general' | 'members' | 'publish' | 'sync' | 'export'; type TabNames = 'General' | 'Sync' | 'Collaboration' | 'Publish' | 'Export';
const tabMap: { const tabMap: {
name: TabNames; name: TabNames;
icon: ReactNode;
panelRender: (workspace: WorkspaceUnit) => ReactNode; panelRender: (workspace: WorkspaceUnit) => ReactNode;
}[] = [ }[] = [
{ {
name: 'general', name: 'General',
icon: <EditIcon />,
panelRender: workspace => <GeneralPage workspace={workspace} />, panelRender: workspace => <GeneralPage workspace={workspace} />,
}, },
{ {
name: 'members', name: 'Sync',
icon: <CloudInsyncIcon />,
panelRender: workspace => <MembersPage workspace={workspace} />,
},
{
name: 'publish',
icon: <UsersIcon />,
panelRender: workspace => <PublishPage workspace={workspace} />,
},
{
name: 'sync',
icon: <PublishIcon />,
panelRender: workspace => <SyncPage workspace={workspace} />, panelRender: workspace => <SyncPage workspace={workspace} />,
}, },
{ {
name: 'export', name: 'Collaboration',
icon: <PublishIcon />, panelRender: workspace => <MembersPage workspace={workspace} />,
},
{
name: 'Publish',
panelRender: workspace => <PublishPage workspace={workspace} />,
},
{
name: 'Export',
panelRender: workspace => <ExportPage workspace={workspace} />, panelRender: workspace => <ExportPage workspace={workspace} />,
}, },
]; ];
const WorkspaceSetting = () => { const WorkspaceSetting = () => {
const { t } = useTranslation();
const { currentWorkspace, isOwner } = useAppState(); const { currentWorkspace, isOwner } = useAppState();
const [activeTab, setActiveTab] = useState<TabNames>(tabMap[0].name); const [activeTab, setActiveTab] = useState<TabNames>(tabMap[0].name);
@@ -72,48 +63,43 @@ const WorkspaceSetting = () => {
)?.panelRender; )?.panelRender;
let tableArr: { let tableArr: {
name: TabNames; name: TabNames;
icon: ReactNode;
panelRender: (workspace: WorkspaceUnit) => ReactNode; panelRender: (workspace: WorkspaceUnit) => ReactNode;
}[] = tabMap; }[] = tabMap;
if (!isOwner) { if (!isOwner) {
tableArr = [ tableArr = [
{ {
name: 'general', name: 'General',
icon: <EditIcon />,
panelRender: workspace => <GeneralPage workspace={workspace} />, panelRender: workspace => <GeneralPage workspace={workspace} />,
}, },
]; ];
} }
return ( return (
<StyledSettingContainer> <>
<StyledSettingSidebar> <StyledSettingContainer>
<StyledSettingSidebarHeader> <PageListHeader icon={<SettingsIcon />}>{t('Settings')}</PageListHeader>
Workspace Settings <StyledSettingSidebar>
</StyledSettingSidebarHeader> <StyledSettingTabContainer>
<StyledSettingTabContainer> {tableArr.map(({ name }) => {
{tableArr.map(({ icon, name }) => { return (
return ( <WorkspaceSettingTagItem
<WorkspaceSettingTagItem key={name}
key={name} isActive={activeTab === name}
isActive={activeTab === name} onClick={() => {
onClick={() => { handleTabChange(name);
handleTabChange(name); }}
}} >
> {name}
<StyledSettingTagIconContainer> </WorkspaceSettingTagItem>
{icon} );
</StyledSettingTagIconContainer> })}
{name} </StyledSettingTabContainer>
</WorkspaceSettingTagItem> </StyledSettingSidebar>
);
})}
</StyledSettingTabContainer>
</StyledSettingSidebar>
<StyledSettingContent> <StyledSettingContent>
{currentWorkspace && activeTabPanelRender?.(currentWorkspace)} {currentWorkspace && activeTabPanelRender?.(currentWorkspace)}
</StyledSettingContent> </StyledSettingContent>
</StyledSettingContainer> </StyledSettingContainer>
</>
); );
}; };
WorkspaceSetting.getLayout = function getLayout(page: ReactElement) { WorkspaceSetting.getLayout = function getLayout(page: ReactElement) {

View File

@@ -11,7 +11,11 @@ export const Trash = () => {
return ( return (
<> <>
<PageListHeader icon={<TrashIcon />}>{t('Trash')}</PageListHeader> <PageListHeader icon={<TrashIcon />}>{t('Trash')}</PageListHeader>
<PageList pageList={pageList.filter(p => p.trash)} isTrash={true} /> <PageList
pageList={pageList.filter(p => p.trash)}
isTrash={true}
listType="trash"
/>
</> </>
); );
}; };

View File

@@ -101,11 +101,12 @@ export const AppStateProvider = ({
} }
const workspace = (await dataCenter.loadWorkspace(workspaceId)) ?? null; const workspace = (await dataCenter.loadWorkspace(workspaceId)) ?? null;
let isOwner; let isOwner;
if (workspace.provider === 'local') { if (workspace?.provider === 'local') {
// isOwner is useful only in the cloud // isOwner is useful only in the cloud
isOwner = true; isOwner = true;
} else { } else {
isOwner = workspace?.owner && user?.id === workspace?.owner?.id; // We must ensure workspace.owner exists, then ensure id same.
isOwner = workspace?.owner && user?.id === workspace.owner.id;
} }
const pageList = const pageList =
(workspace?.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? []; (workspace?.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? [];
@@ -133,7 +134,11 @@ export const AppStateProvider = ({
const login = async () => { const login = async () => {
const { dataCenter } = appState; const { dataCenter } = appState;
await dataCenter.login(); await dataCenter.login();
const user = (await dataCenter.getUserInfo()) as User; const user = (await dataCenter.getUserInfo()) as User;
if (!user) {
throw new Error('User info not found');
}
setAppState({ setAppState({
...appState, ...appState,
user, user,

View File

@@ -106,7 +106,7 @@ export const textEllipsis = (lineNum = 1): CSSProperties => {
display: '-webkit-box', display: '-webkit-box',
wordBreak: 'break-all', wordBreak: 'break-all',
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
WebkitLineClamp: `${lineNum}`, //需要显示的行数 WebkitLineClamp: `${lineNum}`, //the number of rows to display
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
}; };

View File

@@ -20,6 +20,7 @@ export const getLightTheme = (
popoverBackground: '#fff', popoverBackground: '#fff',
tooltipBackground: '#6880FF', tooltipBackground: '#6880FF',
codeBackground: '#f2f5f9', codeBackground: '#f2f5f9',
codeBlockBackground: '#fafbfd',
warningBackground: '#FFF9C7', warningBackground: '#FFF9C7',
errorBackground: '#FFDED8', errorBackground: '#FFDED8',
@@ -40,6 +41,7 @@ export const getLightTheme = (
disableColor: '#C0C0C0', disableColor: '#C0C0C0',
warningColor: '#906616', warningColor: '#906616',
errorColor: '#EB4335', errorColor: '#EB4335',
lineNumberColor: '#888A9E',
}, },
font: { font: {
xs: '12px', xs: '12px',
@@ -96,6 +98,7 @@ export const getDarkTheme = (
editorMode === 'edgeless' editorMode === 'edgeless'
? lightTheme.colors.codeBackground ? lightTheme.colors.codeBackground
: '#505662', : '#505662',
codeBlockBackground: '#36383D',
warningBackground: '#FFF9C7', warningBackground: '#FFF9C7',
errorBackground: '#FFDED8', errorBackground: '#FFDED8',
@@ -117,6 +120,7 @@ export const getDarkTheme = (
disableColor: '#4b4b4b', disableColor: '#4b4b4b',
warningColor: '#906616', warningColor: '#906616',
errorColor: '#EB4335', errorColor: '#EB4335',
lineNumberColor: '#888A9E',
}, },
shadow: { shadow: {
popover: popover:
@@ -154,12 +158,14 @@ export const globalThemeVariables: (
'--affine-popover-color': theme.colors.popoverColor, '--affine-popover-color': theme.colors.popoverColor,
'--affine-input-color': theme.colors.inputColor, '--affine-input-color': theme.colors.inputColor,
'--affine-code-color': theme.colors.codeColor, '--affine-code-color': theme.colors.codeColor,
'--affine-code-block-background': theme.colors.codeBlockBackground,
'--affine-quote-color': theme.colors.quoteColor, '--affine-quote-color': theme.colors.quoteColor,
'--affine-selected-color': theme.colors.selectedColor, '--affine-selected-color': theme.colors.selectedColor,
'--affine-placeholder-color': theme.colors.placeHolderColor, '--affine-placeholder-color': theme.colors.placeHolderColor,
'--affine-border-color': theme.colors.borderColor, '--affine-border-color': theme.colors.borderColor,
'--affine-disable-color': theme.colors.disableColor, '--affine-disable-color': theme.colors.disableColor,
'--affine-tooltip-color': theme.colors.tooltipColor, '--affine-tooltip-color': theme.colors.tooltipColor,
'--affine-line-number-color': theme.colors.lineNumberColor,
'--affine-modal-shadow': theme.shadow.modal, '--affine-modal-shadow': theme.shadow.modal,
'--affine-popover-shadow': theme.shadow.popover, '--affine-popover-shadow': theme.shadow.popover,

View File

@@ -25,6 +25,7 @@ export interface AffineTheme {
hoverBackground: string; hoverBackground: string;
innerHoverBackground: string; innerHoverBackground: string;
codeBackground: string; codeBackground: string;
codeBlockBackground: string;
warningBackground: string; warningBackground: string;
errorBackground: string; errorBackground: string;
// Use for the page`s text // Use for the page`s text
@@ -47,6 +48,7 @@ export interface AffineTheme {
disableColor: string; disableColor: string;
warningColor: string; warningColor: string;
errorColor: string; errorColor: string;
lineNumberColor: string;
}; };
font: { font: {
xs: string; // tiny xs: string; // tiny
@@ -90,6 +92,8 @@ export interface AffineThemeCSSVariables {
'--affine-popover-background': AffineTheme['colors']['popoverBackground']; '--affine-popover-background': AffineTheme['colors']['popoverBackground'];
'--affine-hover-background': AffineTheme['colors']['hoverBackground']; '--affine-hover-background': AffineTheme['colors']['hoverBackground'];
'--affine-code-background': AffineTheme['colors']['codeBackground']; '--affine-code-background': AffineTheme['colors']['codeBackground'];
'--affine-code-block-background': AffineTheme['colors']['codeBlockBackground'];
'--affine-tooltip-background': AffineTheme['colors']['tooltipBackground']; '--affine-tooltip-background': AffineTheme['colors']['tooltipBackground'];
'--affine-text-color': AffineTheme['colors']['textColor']; '--affine-text-color': AffineTheme['colors']['textColor'];
@@ -107,6 +111,7 @@ export interface AffineThemeCSSVariables {
'--affine-border-color': AffineTheme['colors']['borderColor']; '--affine-border-color': AffineTheme['colors']['borderColor'];
'--affine-disable-color': AffineTheme['colors']['disableColor']; '--affine-disable-color': AffineTheme['colors']['disableColor'];
'--affine-tooltip-color': AffineTheme['colors']['tooltipColor']; '--affine-tooltip-color': AffineTheme['colors']['tooltipColor'];
'--affine-line-number-color': AffineTheme['colors']['lineNumberColor'];
'--affine-modal-shadow': AffineTheme['shadow']['modal']; '--affine-modal-shadow': AffineTheme['shadow']['modal'];
'--affine-popover-shadow': AffineTheme['shadow']['popover']; '--affine-popover-shadow': AffineTheme['shadow']['popover'];

View File

@@ -39,7 +39,7 @@ docker run -it --name affine -d -v [YOUR_PATH]:/app/data -p 3000:3000 ghcr.io/to
[] What about a code block? ````` [] What about a code block? `````
```javascript ```JavaScript
console.log('Hello world'); console.log('Hello world');
``` ```

View File

@@ -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 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

View File

@@ -182,7 +182,7 @@ export const StyledButton = styled('button', {
paddingLeft: padding, paddingLeft: padding,
paddingRight: padding, paddingRight: padding,
border: '1px solid', border: '1px solid',
...displayInlineFlex('flex-start', 'center'), ...displayInlineFlex('center', 'center'),
position: 'relative', position: 'relative',
// TODO: disabled color is not decided // TODO: disabled color is not decided
...(disabled ...(disabled
@@ -202,6 +202,7 @@ export const StyledButton = styled('button', {
}, },
'>span': { '>span': {
marginLeft: '5px', marginLeft: '5px',
width: '100%',
}, },
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore

View File

@@ -1,7 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import { Modal, ModalCloseButton, ModalProps } from '../modal'; import { Modal, ModalCloseButton, ModalProps } from '../modal';
import { import {
StyledButtonWrapper, StyledRowButtonWrapper,
StyledColumnButtonWrapper,
StyledConfirmContent, StyledConfirmContent,
StyledConfirmTitle, StyledConfirmTitle,
StyledModalWrapper, StyledModalWrapper,
@@ -15,6 +16,7 @@ export type ConfirmProps = {
cancelText?: string; cancelText?: string;
// TODO: Confirm button's color should depend on confirm type // TODO: Confirm button's color should depend on confirm type
confirmType?: 'primary' | 'warning' | 'danger'; confirmType?: 'primary' | 'warning' | 'danger';
buttonDirection?: 'row' | 'column';
onConfirm?: () => void; onConfirm?: () => void;
onCancel?: () => void; onCancel?: () => void;
} & Omit<ModalProps, 'open' | 'children'>; } & Omit<ModalProps, 'open' | 'children'>;
@@ -26,6 +28,7 @@ export const Confirm = ({
confirmType, confirmType,
onConfirm, onConfirm,
onCancel, onCancel,
buttonDirection = 'row',
cancelText = 'Cancel', cancelText = 'Cancel',
}: ConfirmProps) => { }: ConfirmProps) => {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
@@ -41,31 +44,63 @@ export const Confirm = ({
/> />
<StyledConfirmTitle>{title}</StyledConfirmTitle> <StyledConfirmTitle>{title}</StyledConfirmTitle>
<StyledConfirmContent>{content}</StyledConfirmContent> <StyledConfirmContent>{content}</StyledConfirmContent>
{buttonDirection === 'row' ? (
<StyledButtonWrapper> <StyledRowButtonWrapper>
<Button <Button
shape="round" shape="round"
bold={true} bold={true}
onClick={() => { onClick={() => {
setOpen(false); setOpen(false);
onCancel?.(); onCancel?.();
}} }}
style={{ marginRight: '24px' }} style={{ marginRight: '24px' }}
> >
{cancelText === 'Cancel' ? t('Cancel') : cancelText} {cancelText === 'Cancel' ? t('Cancel') : cancelText}
</Button> </Button>
<Button <Button
type={confirmType} type={confirmType}
shape="round" shape="round"
bold={true} bold={true}
onClick={() => { onClick={() => {
setOpen(false); setOpen(false);
onConfirm?.(); onConfirm?.();
}} }}
> >
{confirmText} {confirmText}
</Button> </Button>
</StyledButtonWrapper> </StyledRowButtonWrapper>
) : (
<StyledColumnButtonWrapper>
<Button
type={confirmType}
shape="round"
bold={true}
onClick={() => {
setOpen(false);
onConfirm?.();
}}
style={{ width: '284px', height: '38px', textAlign: 'center' }}
>
{confirmText}
</Button>
<Button
shape="round"
bold={true}
onClick={() => {
setOpen(false);
onCancel?.();
}}
style={{
marginTop: '16px',
width: '284px',
height: '38px',
textAlign: 'center',
}}
>
{cancelText === 'Cancel' ? t('Cancel') : cancelText}
</Button>
</StyledColumnButtonWrapper>
)}
</StyledModalWrapper> </StyledModalWrapper>
</Modal> </Modal>
); );

View File

@@ -3,8 +3,10 @@ import { ModalWrapper } from '@/ui/modal';
export const StyledModalWrapper = styled(ModalWrapper)(() => { export const StyledModalWrapper = styled(ModalWrapper)(() => {
return { return {
width: '460px', minWidth: '460px',
padding: '46px 60px 32px', maxWidth: '560px',
maxHeight: '292px',
padding: '44px 84px 32px 84px',
}; };
}); });
@@ -14,6 +16,7 @@ export const StyledConfirmTitle = styled.div(({ theme }) => {
fontWeight: 600, fontWeight: 600,
textAlign: 'center', textAlign: 'center',
color: theme.colors.popoverColor, color: theme.colors.popoverColor,
lineHeight: '28px',
}; };
}); });
@@ -23,12 +26,21 @@ export const StyledConfirmContent = styled.div(({ theme }) => {
textAlign: 'center', textAlign: 'center',
marginTop: '12px', marginTop: '12px',
color: theme.colors.textColor, color: theme.colors.textColor,
lineHeight: '26px',
}; };
}); });
export const StyledButtonWrapper = styled.div(() => { export const StyledColumnButtonWrapper = styled.div(() => {
return { return {
...displayFlex('center', 'center'), ...displayFlex('center', 'center'),
flexDirection: 'column',
marginTop: '32px',
};
});
export const StyledRowButtonWrapper = styled.div(() => {
return {
...displayFlex('center', 'center'),
flexDirection: 'row',
marginTop: '32px', marginTop: '32px',
}; };
}); });

View File

@@ -12,6 +12,7 @@ type inputProps = {
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
width?: number; width?: number;
height?: number;
maxLength?: number; maxLength?: number;
minLength?: number; minLength?: number;
onChange?: (value: string) => void; onChange?: (value: string) => void;
@@ -26,6 +27,7 @@ export const Input = (props: inputProps) => {
placeholder, placeholder,
maxLength, maxLength,
minLength, minLength,
height,
width = 260, width = 260,
onChange, onChange,
onBlur, onBlur,
@@ -62,6 +64,7 @@ export const Input = (props: inputProps) => {
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
height={height}
></StyledInput> ></StyledInput>
); );
}; };

View File

@@ -4,7 +4,8 @@ export const StyledInput = styled('input')<{
disabled?: boolean; disabled?: boolean;
value?: string; value?: string;
width: number; width: number;
}>(({ theme, width, disabled }) => { height?: number;
}>(({ theme, width, disabled, height }) => {
const fontWeight = 400; const fontWeight = 400;
const fontSize = '16px'; const fontSize = '16px';
return { return {
@@ -13,6 +14,7 @@ export const StyledInput = styled('input')<{
padding: '8px 12px', padding: '8px 12px',
fontWeight, fontWeight,
fontSize, fontSize,
height: height ? `${height}px` : 'auto',
color: disabled ? theme.colors.disableColor : theme.colors.inputColor, color: disabled ? theme.colors.disableColor : theme.colors.inputColor,
border: `1px solid`, border: `1px solid`,
borderColor: theme.colors.borderColor, // TODO: check out disableColor, borderColor: theme.colors.borderColor, // TODO: check out disableColor,

View File

@@ -23,11 +23,12 @@
"@playwright/test": "^1.29.1", "@playwright/test": "^1.29.1",
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
"fake-indexeddb": "4.0.1", "fake-indexeddb": "4.0.1",
"typescript": "^4.8.4" "typescript": "^4.8.4",
"yjs": "^13.5.44"
}, },
"dependencies": { "dependencies": {
"@blocksuite/blocks": "0.3.1-20230109032243-37ad3ba", "@blocksuite/blocks": "0.4.0-20230111171650-bc63456",
"@blocksuite/store": "0.3.1-20230109032243-37ad3ba", "@blocksuite/store": "0.4.0-20230111171650-bc63456",
"@tauri-apps/api": "^1.2.0", "@tauri-apps/api": "^1.2.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"encoding": "^0.1.13", "encoding": "^0.1.13",
@@ -37,11 +38,6 @@
"ky-universal": "^0.11.0", "ky-universal": "^0.11.0",
"lib0": "^0.2.58", "lib0": "^0.2.58",
"swr": "^2.0.0", "swr": "^2.0.0",
"y-protocols": "^1.0.5", "y-protocols": "^1.0.5"
"yjs": "^13.5.44"
},
"peerDependencies": {
"@blocksuite/blocks": "0.3.1-*",
"@blocksuite/store": "0.3.1-*"
} }
} }

View File

@@ -23,7 +23,7 @@ export class DataCenter {
private readonly _workspaceUnitCollection = new WorkspaceUnitCollection(); private readonly _workspaceUnitCollection = new WorkspaceUnitCollection();
private readonly _logger = getLogger('dc'); private readonly _logger = getLogger('dc');
private _workspaceInstances: Map<string, BlocksuiteWorkspace> = new Map(); private _workspaceInstances: Map<string, BlocksuiteWorkspace> = new Map();
private _messageCenter = new MessageCenter(); private _messageCenter = MessageCenter.getInstance();
/** /**
* A mainProvider must exist as the only data trustworthy source. * A mainProvider must exist as the only data trustworthy source.
*/ */
@@ -159,7 +159,9 @@ export class DataCenter {
* @param {string} workspaceId workspace id * @param {string} workspaceId workspace id
* @returns {Promise<WorkspaceUnit>} * @returns {Promise<WorkspaceUnit>}
*/ */
public async loadWorkspace(workspaceId: string) { public async loadWorkspace(
workspaceId: string
): Promise<WorkspaceUnit | null> {
const workspaceUnit = this._workspaceUnitCollection.find(workspaceId); const workspaceUnit = this._workspaceUnitCollection.find(workspaceId);
assert(workspaceUnit, 'Workspace not found'); assert(workspaceUnit, 'Workspace not found');
const currentProvider = this.providerMap.get(workspaceUnit.provider); const currentProvider = this.providerMap.get(workspaceUnit.provider);
@@ -425,13 +427,14 @@ export class DataCenter {
/** /**
* accept invitation * accept invitation
* @param {string} inviteCode * @param {string} inviteCode
* @returns {Promise<Permission | null>} permission
*/ */
async acceptInvitation(inviteCode: string, providerStr = 'affine') { async acceptInvitation(inviteCode: string, providerStr = 'affine') {
const provider = this.providerMap.get(providerStr); const provider = this.providerMap.get(providerStr);
if (provider) { if (provider) {
return await provider.acceptInvitation(inviteCode); return await provider.acceptInvitation(inviteCode);
} }
return []; return null;
} }
onMessage(cb: (message: Message) => void) { onMessage(cb: (message: Message) => void) {

View File

@@ -1,3 +1,61 @@
export enum MessageCode { export enum MessageCode {
loginError, loginError,
noPermission,
loadListFailed,
getDetailFailed,
createWorkspaceFailed,
getMembersFailed,
updateWorkspaceFailed,
deleteWorkspaceFailed,
inviteMemberFailed,
removeMemberFailed,
acceptInvitingFailed,
getBlobFailed,
leaveWorkspaceFailed,
downloadWorkspaceFailed,
} }
export const messages = {
[MessageCode.loginError]: {
message: 'Login failed',
},
[MessageCode.noPermission]: {
message: 'No permission',
},
[MessageCode.loadListFailed]: {
message: 'Load list failed',
},
[MessageCode.getDetailFailed]: {
message: 'Get detail failed',
},
[MessageCode.createWorkspaceFailed]: {
message: 'Create workspace failed',
},
[MessageCode.getMembersFailed]: {
message: 'Get members failed',
},
[MessageCode.updateWorkspaceFailed]: {
message: 'Update workspace failed',
},
[MessageCode.deleteWorkspaceFailed]: {
message: 'Delete workspace failed',
},
[MessageCode.inviteMemberFailed]: {
message: 'Invite member failed',
},
[MessageCode.removeMemberFailed]: {
message: 'Remove member failed',
},
[MessageCode.acceptInvitingFailed]: {
message: 'Accept inviting failed',
},
[MessageCode.getBlobFailed]: {
message: 'Get blob failed',
},
[MessageCode.leaveWorkspaceFailed]: {
message: 'Leave workspace failed',
},
[MessageCode.downloadWorkspaceFailed]: {
message: 'Download workspace failed',
},
};

View File

@@ -1,24 +1,36 @@
import { Observable } from 'lib0/observable'; import { Observable } from 'lib0/observable';
import { Message } from '../types'; import { Message } from '../types';
import { MessageCode } from './code.js'; import { MessageCode, messages } from './code.js';
export class MessageCenter extends Observable<string> { export class MessageCenter extends Observable<string> {
private _messages: Record<number, Omit<Message, 'provider' | 'code'>> =
messages;
constructor() { constructor() {
super(); super();
} }
public send(message: MessageCode) { static instance: MessageCenter;
this.emit('message', [message]);
static getInstance() {
if (!MessageCenter.instance) {
MessageCenter.instance = new MessageCenter();
}
return MessageCenter.instance;
}
static messageCode = MessageCode;
public getMessageSender(provider: string) {
return this._send.bind(this, provider);
}
private _send(provider: string, messageCode: MessageCode) {
this.emit('message', [
{ ...this._messages[messageCode], provider, code: messageCode },
]);
} }
public onMessage(callback: (message: Message) => void) { public onMessage(callback: (message: Message) => void) {
this.on('message', callback); this.on('message', callback);
} }
private messages: Record<number, Message> = {
[MessageCode.loginError]: {
code: MessageCode.loginError,
message: 'Login failed',
},
};
} }

View File

@@ -2,7 +2,6 @@ import { BaseProvider } from '../base.js';
import type { import type {
ProviderConstructorParams, ProviderConstructorParams,
CreateWorkspaceInfoParams, CreateWorkspaceInfoParams,
WorkspaceMeta0,
} from '../base'; } from '../base';
import type { User } from '../../types'; import type { User } from '../../types';
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
@@ -12,18 +11,13 @@ import { WebsocketProvider } from './sync.js';
// import { IndexedDBProvider } from '../local/indexeddb'; // import { IndexedDBProvider } from '../local/indexeddb';
import { getApis, Workspace } from './apis/index.js'; import { getApis, Workspace } from './apis/index.js';
import type { Apis, WorkspaceDetail, Callback } from './apis'; import type { Apis, WorkspaceDetail, Callback } from './apis';
import { setDefaultAvatar } from '../utils.js';
import { MessageCode } from '../../message/index.js';
import { token } from './apis/token.js'; import { token } from './apis/token.js';
import { WebsocketClient } from './channel'; import { WebsocketClient } from './channel';
import { import { loadWorkspaceUnit, createWorkspaceUnit } from './utils.js';
loadWorkspaceUnit,
createWorkspaceUnit,
syncToCloud,
} from './utils.js';
import { WorkspaceUnit } from '../../workspace-unit.js'; import { WorkspaceUnit } from '../../workspace-unit.js';
import { createBlocksuiteWorkspace, applyUpdate } from '../../utils/index.js'; import { createBlocksuiteWorkspace, applyUpdate } from '../../utils/index.js';
import type { SyncMode } from '../../workspace-unit'; import type { SyncMode } from '../../workspace-unit';
import { MessageCenter } from '../../message/index.js';
type ChannelMessage = { type ChannelMessage = {
ws_list: Workspace[]; ws_list: Workspace[];
@@ -42,9 +36,8 @@ const {
export class AffineProvider extends BaseProvider { export class AffineProvider extends BaseProvider {
public id = 'affine'; public id = 'affine';
private _workspacesCache: Map<string, BlocksuiteWorkspace> = new Map();
private _onTokenRefresh?: Callback = undefined; private _onTokenRefresh?: Callback = undefined;
private _wsMap: Map<string, WebsocketProvider> = new Map(); private _wsMap: Map<BlocksuiteWorkspace, WebsocketProvider> = new Map();
private _apis: Apis; private _apis: Apis;
private _channel?: WebsocketClient; private _channel?: WebsocketClient;
// private _idbMap: Map<string, IndexedDBProvider> = new Map(); // private _idbMap: Map<string, IndexedDBProvider> = new Map();
@@ -105,11 +98,23 @@ export class AffineProvider extends BaseProvider {
}); });
} }
private _handlerAffineListMessage({ ws_details, metadata }: ChannelMessage) { private async _handlerAffineListMessage({
ws_details,
metadata,
}: ChannelMessage) {
this._logger('receive server message'); this._logger('receive server message');
Object.entries(ws_details).forEach(async ([id, detail]) => { const addedWorkspaces: WorkspaceUnit[] = [];
const removeWorkspaceList = this._workspaces.list().map(w => w.id);
for (const [id, detail] of Object.entries(ws_details)) {
const { name, avatar } = metadata[id]; const { name, avatar } = metadata[id];
assert(name); const index = removeWorkspaceList.indexOf(id);
if (index !== -1) {
removeWorkspaceList.splice(index, 1);
}
assert(
name,
'workspace name not found by id when receive server message'
);
const workspace = { const workspace = {
name: name, name: name,
avatar, avatar,
@@ -121,26 +126,31 @@ export class AffineProvider extends BaseProvider {
}, },
published: detail.public, published: detail.public,
memberCount: detail.member_count, memberCount: detail.member_count,
provider: 'affine', provider: this.id,
syncMode: 'core' as SyncMode, syncMode: 'core' as SyncMode,
}; };
if (this._workspaces.get(id)) { if (this._workspaces.get(id)) {
// update workspaces
this._workspaces.update(id, workspace); this._workspaces.update(id, workspace);
} else { } else {
const workspaceUnit = await loadWorkspaceUnit( const workspaceUnit = await loadWorkspaceUnit(
{ id, ...workspace }, { id, ...workspace },
this._apis this._apis
); );
this._workspaces.add(workspaceUnit); addedWorkspaces.push(workspaceUnit);
} }
}); }
// add workspaces
this._workspaces.add(addedWorkspaces);
// remove workspaces
this._workspaces.remove(removeWorkspaceList);
} }
private _getWebsocketProvider(workspace: BlocksuiteWorkspace) { private _getWebsocketProvider(workspace: BlocksuiteWorkspace) {
const { doc, room } = workspace; const { doc, room } = workspace;
assert(room); assert(room);
assert(doc); assert(doc);
let ws = this._wsMap.get(room); let ws = this._wsMap.get(workspace);
if (!ws) { if (!ws) {
const wsUrl = `${ const wsUrl = `${
window.location.protocol === 'https:' ? 'wss' : 'ws' window.location.protocol === 'https:' ? 'wss' : 'ws'
@@ -148,7 +158,7 @@ export class AffineProvider extends BaseProvider {
ws = new WebsocketProvider(wsUrl, room, doc, { ws = new WebsocketProvider(wsUrl, room, doc, {
params: { token: this._apis.token.refresh }, params: { token: this._apis.token.refresh },
}); });
this._wsMap.set(room, ws); this._wsMap.set(workspace, ws);
} }
return ws; return ws;
} }
@@ -175,8 +185,8 @@ export class AffineProvider extends BaseProvider {
this.linkLocal(workspace); this.linkLocal(workspace);
const ws = this._getWebsocketProvider(workspace); const ws = this._getWebsocketProvider(workspace);
// close all websocket links // close all websocket links
Array.from(this._wsMap.entries()).forEach(([id, ws]) => { Array.from(this._wsMap.entries()).forEach(([blocksuiteWorkspace, ws]) => {
if (id !== room) { if (blocksuiteWorkspace !== workspace) {
ws.disconnect(); ws.disconnect();
} }
}); });
@@ -207,7 +217,7 @@ export class AffineProvider extends BaseProvider {
owner: undefined, owner: undefined,
published: w.public, published: w.public,
memberCount: 1, memberCount: 1,
provider: 'affine', provider: this.id,
syncMode: 'core', syncMode: 'core',
}, },
this._apis this._apis
@@ -232,14 +242,12 @@ export class AffineProvider extends BaseProvider {
this._connectChannel(); this._connectChannel();
} }
if (!user) { if (!user) {
this._messageCenter.send(MessageCode.loginError); this._sendMessage(MessageCenter.messageCode.loginError);
} }
} }
public override async getUserInfo(): Promise<User | undefined> { public override async getUserInfo(): Promise<User | undefined> {
await this.init();
const user = this._apis.token.user; const user = this._apis.token.user;
await this.init;
return user return user
? { ? {
id: user.id, id: user.id,
@@ -258,23 +266,29 @@ export class AffineProvider extends BaseProvider {
} }
public override async clear(): Promise<void> { public override async clear(): Promise<void> {
for (const w of this._workspacesCache.values()) { for (const w of this._workspaces.list()) {
if (w.room) { if (w.id) {
try { try {
await this.deleteWorkspace(w.room); await this.deleteWorkspace(w.id);
this._workspaces.remove(w.room); this._workspaces.remove(w.id);
} catch (e) { } catch (e) {
this._logger('has a problem of delete workspace ', e); this._logger('has a problem of delete workspace ', e);
} }
} }
} }
this._workspacesCache.clear(); this._workspaces.clear();
} }
public override async closeWorkspace(id: string) { public override async closeWorkspace(id: string) {
// const idb = this._idbMap.get(id); // const idb = this._idbMap.get(id);
// idb?.destroy(); // idb?.destroy();
const ws = this._wsMap.get(id); const workspaceUnit = this._workspaces.get(id);
const ws = workspaceUnit?.blocksuiteWorkspace
? this._wsMap.get(workspaceUnit?.blocksuiteWorkspace)
: null;
if (!ws) {
console.error('close workspace websocket which not exist.');
}
ws?.disconnect(); ws?.disconnect();
} }
@@ -305,7 +319,22 @@ export class AffineProvider extends BaseProvider {
public override async createWorkspace( public override async createWorkspace(
meta: CreateWorkspaceInfoParams meta: CreateWorkspaceInfoParams
): Promise<WorkspaceUnit | undefined> { ): Promise<WorkspaceUnit | undefined> {
const { id } = await this._apis.createWorkspace(meta); const workspaceUnitForUpload = await createWorkspaceUnit({
id: '',
name: meta.name,
avatar: undefined,
owner: await this.getUserInfo(),
published: false,
memberCount: 1,
provider: this.id,
syncMode: 'core',
});
const { id } = await this._apis.createWorkspace(
new Blob([
encodeStateAsUpdate(workspaceUnitForUpload.blocksuiteWorkspace!.doc)
.buffer,
])
);
const workspaceUnit = await createWorkspaceUnit({ const workspaceUnit = await createWorkspaceUnit({
id, id,
@@ -314,14 +343,10 @@ export class AffineProvider extends BaseProvider {
owner: await this.getUserInfo(), owner: await this.getUserInfo(),
published: false, published: false,
memberCount: 1, memberCount: 1,
provider: 'affine', provider: this.id,
syncMode: 'core', syncMode: 'core',
}); });
await syncToCloud(
workspaceUnit.blocksuiteWorkspace!,
this._apis.token.refresh
);
this._workspaces.add(workspaceUnit); this._workspaces.add(workspaceUnit);
return workspaceUnit; return workspaceUnit;
@@ -349,9 +374,11 @@ export class AffineProvider extends BaseProvider {
public override async extendWorkspace( public override async extendWorkspace(
workspaceUnit: WorkspaceUnit workspaceUnit: WorkspaceUnit
): Promise<WorkspaceUnit> { ): Promise<WorkspaceUnit> {
const { id } = await this._apis.createWorkspace({ const { id } = await this._apis.createWorkspace(
name: workspaceUnit.name, new Blob([
}); encodeStateAsUpdate(workspaceUnit.blocksuiteWorkspace!.doc).buffer,
])
);
const newWorkspaceUnit = new WorkspaceUnit({ const newWorkspaceUnit = new WorkspaceUnit({
id, id,
name: workspaceUnit.name, name: workspaceUnit.name,
@@ -359,7 +386,7 @@ export class AffineProvider extends BaseProvider {
owner: await this.getUserInfo(), owner: await this.getUserInfo(),
published: false, published: false,
memberCount: 1, memberCount: 1,
provider: 'affine', provider: this.id,
syncMode: 'core', syncMode: 'core',
}); });
@@ -370,8 +397,6 @@ export class AffineProvider extends BaseProvider {
encodeStateAsUpdate(workspaceUnit.blocksuiteWorkspace.doc) encodeStateAsUpdate(workspaceUnit.blocksuiteWorkspace.doc)
); );
await syncToCloud(blocksuiteWorkspace, this._apis.token.refresh);
newWorkspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace); newWorkspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
this._workspaces.add(newWorkspaceUnit); this._workspaces.add(newWorkspaceUnit);
@@ -382,6 +407,7 @@ export class AffineProvider extends BaseProvider {
token.clear(); token.clear();
this._channel?.disconnect(); this._channel?.disconnect();
this._wsMap.forEach(ws => ws.disconnect()); this._wsMap.forEach(ws => ws.disconnect());
this._workspaces.clear();
storage.removeItem('token'); storage.removeItem('token');
} }
@@ -389,7 +415,7 @@ export class AffineProvider extends BaseProvider {
return this._apis.getWorkspaceMembers({ id }); return this._apis.getWorkspaceMembers({ id });
} }
public override async acceptInvitation(invitingCode: string): Promise<void> { public override async acceptInvitation(invitingCode: string) {
await this._apis.acceptInviting({ invitingCode }); return await this._apis.acceptInviting({ invitingCode });
} }
} }

View File

@@ -1,9 +1,15 @@
import kyOrigin from 'ky';
import ky from 'ky-universal'; import ky from 'ky-universal';
import { MessageCenter } from '../../../message/index.js';
import { token } from './token.js'; import { token } from './token.js';
export const bareClient = ky.extend({ type KyInstance = typeof ky;
prefixUrl: 'http://localhost:8080',
const messageCenter = MessageCenter.getInstance();
const _sendMessage = messageCenter.getMessageSender('affine');
export const bareClient: KyInstance = ky.extend({
prefixUrl: '/',
retry: 1, retry: 1,
hooks: { hooks: {
// afterResponse: [ // afterResponse: [
@@ -22,7 +28,7 @@ export const bareClient = ky.extend({
}, },
}); });
export const client = bareClient.extend({ export const client: KyInstance = bareClient.extend({
hooks: { hooks: {
beforeRequest: [ beforeRequest: [
async request => { async request => {
@@ -41,5 +47,15 @@ export const client = bareClient.extend({
request.headers.set('Authorization', token.token); request.headers.set('Authorization', token.token);
}, },
], ],
beforeError: [
error => {
const { response } = error;
if (response.status === 401) {
_sendMessage(MessageCenter.messageCode.noPermission);
}
return error;
},
],
}, },
}); });

View File

@@ -70,9 +70,6 @@ class Token {
} }
async refreshToken(token?: string) { async refreshToken(token?: string) {
if (!this._refreshToken && !token) {
throw new Error('No authorization token.');
}
if (!this._padding) { if (!this._padding) {
this._padding = login({ this._padding = login({
type: 'Refresh', type: 'Refresh',
@@ -194,6 +191,7 @@ export const getAuthorizer = () => {
return [signInWithGoogle, onAuthStateChanged] as const; return [signInWithGoogle, onAuthStateChanged] as const;
} catch (e) { } catch (e) {
getLogger('getAuthorizer')(e); getLogger('getAuthorizer')(e);
console.error('getAuthorizer', e);
return [] as const; return [] as const;
} }
}; };

View File

@@ -1,6 +1,20 @@
import { MessageCenter } from '../../../message/index.js';
import { bareClient, client } from './request.js'; import { bareClient, client } from './request.js';
import type { User } from './user'; import type { User } from './user';
const messageCenter = MessageCenter.getInstance();
const sendMessage = messageCenter.getMessageSender('affine');
const { messageCode } = MessageCenter;
class RequestError extends Error {
constructor(message: string, cause: unknown | null = null) {
super(message);
this.name = 'RequestError';
this.cause = cause;
}
}
export interface GetWorkspaceDetailParams { export interface GetWorkspaceDetailParams {
id: string; id: string;
} }
@@ -26,13 +40,18 @@ export interface Workspace {
} }
export async function getWorkspaces(): Promise<Workspace[]> { export async function getWorkspaces(): Promise<Workspace[]> {
return client try {
.get('api/workspace', { return client
headers: { .get('api/workspace', {
'Cache-Control': 'no-cache', headers: {
}, 'Cache-Control': 'no-cache',
}) },
.json(); })
.json();
} catch (error) {
sendMessage(messageCode.loadListFailed);
throw new RequestError('load list failed', error);
}
} }
export interface WorkspaceDetail extends Workspace { export interface WorkspaceDetail extends Workspace {
@@ -43,7 +62,13 @@ export interface WorkspaceDetail extends Workspace {
export async function getWorkspaceDetail( export async function getWorkspaceDetail(
params: GetWorkspaceDetailParams params: GetWorkspaceDetailParams
): Promise<WorkspaceDetail | null> { ): Promise<WorkspaceDetail | null> {
return client.get(`api/workspace/${params.id}`).json(); try {
const response = client.get(`api/workspace/${params.id}`);
return response.json();
} catch (error) {
sendMessage(messageCode.getDetailFailed);
throw new RequestError('get detail failed', error);
}
} }
export interface Permission { export interface Permission {
@@ -74,7 +99,12 @@ export interface GetWorkspaceMembersParams {
export async function getWorkspaceMembers( export async function getWorkspaceMembers(
params: GetWorkspaceDetailParams params: GetWorkspaceDetailParams
): Promise<Member[]> { ): Promise<Member[]> {
return client.get(`api/workspace/${params.id}/permission`).json(); try {
return client.get(`api/workspace/${params.id}/permission`).json();
} catch (error) {
sendMessage(messageCode.getMembersFailed);
throw new RequestError('get members failed', error);
}
} }
export interface CreateWorkspaceParams { export interface CreateWorkspaceParams {
@@ -82,12 +112,14 @@ export interface CreateWorkspaceParams {
} }
export async function createWorkspace( export async function createWorkspace(
params: CreateWorkspaceParams encodedYDoc: Blob
): Promise<{ id: string }> { ): Promise<{ id: string }> {
// FIXME: avatar should be optional try {
return client return client.post('api/workspace', { body: encodedYDoc }).json();
.post('api/workspace', { json: { ...params, avatar: '123' } }) } catch (error) {
.json(); sendMessage(messageCode.createWorkspaceFailed);
throw new RequestError('create workspace failed', error);
}
} }
export interface UpdateWorkspaceParams { export interface UpdateWorkspaceParams {
@@ -98,13 +130,18 @@ export interface UpdateWorkspaceParams {
export async function updateWorkspace( export async function updateWorkspace(
params: UpdateWorkspaceParams params: UpdateWorkspaceParams
): Promise<{ public: boolean | null }> { ): Promise<{ public: boolean | null }> {
return client try {
.post(`api/workspace/${params.id}`, { return client
json: { .post(`api/workspace/${params.id}`, {
public: params.public, json: {
}, public: params.public,
}) },
.json(); })
.json();
} catch (error) {
sendMessage(messageCode.updateWorkspaceFailed);
throw new RequestError('update workspace failed', error);
}
} }
export interface DeleteWorkspaceParams { export interface DeleteWorkspaceParams {
@@ -114,7 +151,10 @@ export interface DeleteWorkspaceParams {
export async function deleteWorkspace( export async function deleteWorkspace(
params: DeleteWorkspaceParams params: DeleteWorkspaceParams
): Promise<void> { ): Promise<void> {
await client.delete(`api/workspace/${params.id}`); await client.delete(`api/workspace/${params.id}`).catch(error => {
sendMessage(messageCode.deleteWorkspaceFailed);
throw new RequestError('delete workspace failed', error);
});
} }
export interface InviteMemberParams { export interface InviteMemberParams {
@@ -126,13 +166,18 @@ export interface InviteMemberParams {
* Notice: Only support normal(contrast to private) workspace. * Notice: Only support normal(contrast to private) workspace.
*/ */
export async function inviteMember(params: InviteMemberParams): Promise<void> { export async function inviteMember(params: InviteMemberParams): Promise<void> {
return client try {
.post(`api/workspace/${params.id}/permission`, { return client
json: { .post(`api/workspace/${params.id}/permission`, {
email: params.email, json: {
}, email: params.email,
}) },
.json(); })
.json();
} catch (error) {
sendMessage(messageCode.inviteMemberFailed);
throw new RequestError('invite member failed', error);
}
} }
export interface RemoveMemberParams { export interface RemoveMemberParams {
@@ -140,7 +185,10 @@ export interface RemoveMemberParams {
} }
export async function removeMember(params: RemoveMemberParams): Promise<void> { export async function removeMember(params: RemoveMemberParams): Promise<void> {
await client.delete(`api/permission/${params.permissionId}`); await client.delete(`api/permission/${params.permissionId}`).catch(error => {
sendMessage(messageCode.removeMemberFailed);
throw new RequestError('remove member failed', error);
});
} }
export interface AcceptInvitingParams { export interface AcceptInvitingParams {
@@ -149,8 +197,13 @@ export interface AcceptInvitingParams {
export async function acceptInviting( export async function acceptInviting(
params: AcceptInvitingParams params: AcceptInvitingParams
): Promise<void> { ): Promise<Permission> {
await bareClient.post(`api/invitation/${params.invitingCode}`); try {
return bareClient.post(`api/invitation/${params.invitingCode}`).json();
} catch (error) {
sendMessage(messageCode.acceptInvitingFailed);
throw new RequestError('accept inviting failed', error);
}
} }
export async function uploadBlob(params: { blob: Blob }): Promise<string> { export async function uploadBlob(params: { blob: Blob }): Promise<string> {
@@ -160,7 +213,12 @@ export async function uploadBlob(params: { blob: Blob }): Promise<string> {
export async function getBlob(params: { export async function getBlob(params: {
blobId: string; blobId: string;
}): Promise<ArrayBuffer> { }): Promise<ArrayBuffer> {
return client.get(`api/blob/${params.blobId}`).arrayBuffer(); try {
return client.get(`api/blob/${params.blobId}`).arrayBuffer();
} catch (error) {
sendMessage(messageCode.getBlobFailed);
throw new RequestError('get blob failed', error);
}
} }
export interface LeaveWorkspaceParams { export interface LeaveWorkspaceParams {
@@ -168,15 +226,26 @@ export interface LeaveWorkspaceParams {
} }
export async function leaveWorkspace({ id }: LeaveWorkspaceParams) { export async function leaveWorkspace({ id }: LeaveWorkspaceParams) {
await client.delete(`api/workspace/${id}/permission`).json(); await client
.delete(`api/workspace/${id}/permission`)
.json()
.catch(error => {
sendMessage(messageCode.leaveWorkspaceFailed);
throw new RequestError('leave workspace failed', error);
});
} }
export async function downloadWorkspace( export async function downloadWorkspace(
workspaceId: string, workspaceId: string,
published = false published = false
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
if (published) { try {
return bareClient.get(`api/public/doc/${workspaceId}`).arrayBuffer(); if (published) {
return bareClient.get(`api/public/doc/${workspaceId}`).arrayBuffer();
}
return client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer();
} catch (error) {
sendMessage(messageCode.downloadWorkspaceFailed);
throw new RequestError('download workspace failed', error);
} }
return client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer();
} }

View File

@@ -3,13 +3,13 @@ import { Logger } from 'src/types';
import { token } from './apis/token'; import { token } from './apis/token';
import * as url from 'lib0/url'; import * as url from 'lib0/url';
const RECONNECT_INTERVAL_TIME = 5000; const RECONNECT_INTERVAL_TIME = 500;
const MAX_RECONNECT_TIMES = 50; const MAX_RECONNECT_TIMES = 50;
export class WebsocketClient extends websocket.WebsocketClient { export class WebsocketClient extends websocket.WebsocketClient {
public shouldReconnect = false; public shouldReconnect = false;
private _reconnectInterval: number | null = null;
private _logger: Logger; private _logger: Logger;
private _retryTimes = 0;
constructor( constructor(
serverUrl: string, serverUrl: string,
logger: Logger, logger: Logger,
@@ -34,30 +34,28 @@ export class WebsocketClient extends websocket.WebsocketClient {
this.on('connect', () => { this.on('connect', () => {
this._logger('Affine channel connected'); this._logger('Affine channel connected');
this.shouldReconnect = true; this.shouldReconnect = true;
if (this._reconnectInterval) { this._retryTimes = 0;
window.clearInterval(this._reconnectInterval);
}
}); });
this.on('disconnect', ({ error }: { error: Error }) => { this.on('disconnect', ({ error }: { error: Error }) => {
if (error) { if (error) {
let times = 0;
// Try reconnect if connect error has occurred // Try reconnect if connect error has occurred
this._reconnectInterval = window.setInterval(() => { if (this.shouldReconnect && token.isLogin && !this.connected) {
if (this.shouldReconnect && token.isLogin && !this.connected) { try {
try { setTimeout(() => {
this.connect(); if (this._retryTimes <= MAX_RECONNECT_TIMES) {
this._logger(`try reconnect channel ${++times} times`); this.connect();
if (times > MAX_RECONNECT_TIMES) { this._logger(
`try reconnect channel ${++this._retryTimes} times`
);
} else {
this._logger('reconnect failed, max reconnect times reached'); this._logger('reconnect failed, max reconnect times reached');
this._reconnectInterval &&
window.clearInterval(this._reconnectInterval);
} }
} catch (e) { }, RECONNECT_INTERVAL_TIME);
this._logger('reconnect failed', e); } catch (e) {
} this._logger('reconnect failed', e);
} }
}, RECONNECT_INTERVAL_TIME); }
} }
}); });
} }

View File

@@ -1,10 +1,7 @@
import assert from 'assert';
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
import { WorkspaceUnit } from '../../workspace-unit.js'; import { WorkspaceUnit } from '../../workspace-unit.js';
import type { WorkspaceUnitCtorParams } from '../../workspace-unit'; import type { WorkspaceUnitCtorParams } from '../../workspace-unit';
import { createBlocksuiteWorkspace } from '../../utils/index.js'; import { createBlocksuiteWorkspace } from '../../utils/index.js';
import type { Apis } from './apis'; import type { Apis } from './apis';
import { WebsocketProvider } from './sync.js';
import { setDefaultAvatar } from '../utils.js'; import { setDefaultAvatar } from '../utils.js';
import { applyUpdate } from '../../utils/index.js'; import { applyUpdate } from '../../utils/index.js';
@@ -42,37 +39,6 @@ export const loadWorkspaceUnit = async (
return workspaceUnit; return workspaceUnit;
}; };
export const syncToCloud = async (
blocksuiteWorkspace: BlocksuiteWorkspace,
refreshToken: string
) => {
const workspaceId = blocksuiteWorkspace.room;
assert(workspaceId, 'Blocksuite workspace without room(workspaceId).');
const wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${
window.location.host
}/api/sync/`;
const ws = new WebsocketProvider(
wsUrl,
workspaceId,
blocksuiteWorkspace.doc,
{
params: { token: refreshToken },
}
);
await new Promise((resolve, reject) => {
ws.once('synced', () => {
// FIXME: we don't when send local data to cloud successfully, so hack to wait 1s.
// Server will support this by add a new api.
setTimeout(resolve, 1000);
});
ws.once('lost-connection', () => reject());
ws.once('connection-error', () => reject());
});
};
export const createWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => { export const createWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => {
const workspaceUnit = new WorkspaceUnit(params); const workspaceUnit = new WorkspaceUnit(params);

View File

@@ -4,6 +4,7 @@ import { Logger, User } from '../types';
import type { WorkspaceUnitCollectionScope } from '../workspace-unit-collection'; import type { WorkspaceUnitCollectionScope } from '../workspace-unit-collection';
import type { WorkspaceUnitCtorParams, WorkspaceUnit } from '../workspace-unit'; import type { WorkspaceUnitCtorParams, WorkspaceUnit } from '../workspace-unit';
import { Member } from './affine/apis'; import { Member } from './affine/apis';
import { Permission } from './affine/apis/workspace.js';
const defaultLogger = () => { const defaultLogger = () => {
return; return;
@@ -22,10 +23,15 @@ export type UpdateWorkspaceMetaParams = Partial<
>; >;
export class BaseProvider { export class BaseProvider {
/** provider id */
public readonly id: string = 'base'; public readonly id: string = 'base';
/** workspace unit collection */
protected _workspaces!: WorkspaceUnitCollectionScope; protected _workspaces!: WorkspaceUnitCollectionScope;
protected _logger!: Logger; protected _logger!: Logger;
protected _messageCenter!: MessageCenter; /** send message with message center */
protected _sendMessage!: ReturnType<
InstanceType<typeof MessageCenter>['getMessageSender']
>;
public constructor({ public constructor({
logger, logger,
@@ -34,7 +40,7 @@ export class BaseProvider {
}: ProviderConstructorParams) { }: ProviderConstructorParams) {
this._logger = (logger || defaultLogger) as Logger; this._logger = (logger || defaultLogger) as Logger;
this._workspaces = workspaces; this._workspaces = workspaces;
this._messageCenter = messageCenter; this._sendMessage = messageCenter.getMessageSender(this.id);
} }
/** /**
@@ -228,8 +234,10 @@ export class BaseProvider {
* @param {string} inviteCode * @param {string} inviteCode
* @returns * @returns
*/ */
public async acceptInvitation(inviteCode: string): Promise<void> { public async acceptInvitation(
inviteCode: string
): Promise<Permission | null> {
inviteCode; inviteCode;
return; return null;
} }
} }

View File

@@ -20,6 +20,7 @@ export const writeUpdatesToLocal = async (
if (updatesStore) { if (updatesStore) {
await idb.addAutoKey(updatesStore, currState); await idb.addAutoKey(updatesStore, currState);
} }
db.close();
}; };
export const applyLocalUpdates = async ( export const applyLocalUpdates = async (

View File

@@ -8,6 +8,7 @@ import type {
import { varStorage as storage } from 'lib0/storage'; import { varStorage as storage } from 'lib0/storage';
import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store'; import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store';
import { IndexedDBProvider } from './indexeddb/indexeddb.js'; import { IndexedDBProvider } from './indexeddb/indexeddb.js';
import { applyLocalUpdates } from './indexeddb/utils.js';
import assert from 'assert'; import assert from 'assert';
import { loadWorkspaceUnit, createWorkspaceUnit } from './utils.js'; import { loadWorkspaceUnit, createWorkspaceUnit } from './utils.js';
import type { WorkspaceUnit } from '../../workspace-unit'; import type { WorkspaceUnit } from '../../workspace-unit';
@@ -48,6 +49,7 @@ export class LocalProvider extends BaseProvider {
workspace: BlocksuiteWorkspace workspace: BlocksuiteWorkspace
): Promise<BlocksuiteWorkspace> { ): Promise<BlocksuiteWorkspace> {
assert(workspace.room); assert(workspace.room);
await applyLocalUpdates(workspace);
await this.linkLocal(workspace); await this.linkLocal(workspace);
return workspace; return workspace;
} }
@@ -101,7 +103,7 @@ export class LocalProvider extends BaseProvider {
owner: undefined, owner: undefined,
syncMode: 'core', syncMode: 'core',
memberCount: 1, memberCount: 1,
provider: 'local', provider: this.id,
}); });
this._workspaces.add(workspaceUnit); this._workspaces.add(workspaceUnit);
this._storeWorkspaces(this._workspaces.list()); this._storeWorkspaces(this._workspaces.list());

View File

@@ -32,4 +32,5 @@ export type Logger = ReturnType<typeof getLogger>;
export type Message = { export type Message = {
code: number; code: number;
message: string; message: string;
provider: string;
}; };

View File

@@ -8,7 +8,7 @@ export interface WorkspaceUnitCollectionScope {
get: (workspaceId: string) => WorkspaceUnit | undefined; get: (workspaceId: string) => WorkspaceUnit | undefined;
list: () => WorkspaceUnit[]; list: () => WorkspaceUnit[];
add: (workspace: WorkspaceUnit | WorkspaceUnit[]) => void; add: (workspace: WorkspaceUnit | WorkspaceUnit[]) => void;
remove: (workspaceId: string) => boolean; remove: (workspaceId: string | string[]) => boolean;
clear: () => void; clear: () => void;
update: ( update: (
workspaceId: string, workspaceId: string,
@@ -18,7 +18,7 @@ export interface WorkspaceUnitCollectionScope {
export interface WorkspaceUnitCollectionChangeEvent { export interface WorkspaceUnitCollectionChangeEvent {
added?: WorkspaceUnit[]; added?: WorkspaceUnit[];
deleted?: WorkspaceUnit; deleted?: WorkspaceUnit[];
updated?: WorkspaceUnit; updated?: WorkspaceUnit;
} }
@@ -62,16 +62,22 @@ export class WorkspaceUnitCollection {
const workspaceUnits = Array.isArray(workspaceUnit) const workspaceUnits = Array.isArray(workspaceUnit)
? workspaceUnit ? workspaceUnit
: [workspaceUnit]; : [workspaceUnit];
let added = false;
workspaceUnits.forEach(workspaceUnit => { workspaceUnits.forEach(workspaceUnit => {
if (this._workspaceUnitMap.has(workspaceUnit.id)) { if (this._workspaceUnitMap.has(workspaceUnit.id)) {
// FIXME: multiple add same workspace // FIXME: multiple add same workspace
return; return;
} }
added = true;
this._workspaceUnitMap.set(workspaceUnit.id, workspaceUnit); this._workspaceUnitMap.set(workspaceUnit.id, workspaceUnit);
scopedWorkspaceIds.add(workspaceUnit.id); scopedWorkspaceIds.add(workspaceUnit.id);
}); });
if (!added) {
return;
}
this._events.emit('change', [ this._events.emit('change', [
{ {
added: workspaceUnits, added: workspaceUnits,
@@ -79,34 +85,44 @@ export class WorkspaceUnitCollection {
]); ]);
}; };
const remove = (workspaceId: string) => { const remove = (workspaceId: string | string[]) => {
if (!scopedWorkspaceIds.has(workspaceId)) { const workspaceIds = Array.isArray(workspaceId)
return true; ? workspaceId
} : [workspaceId];
const workspaceUnits: WorkspaceUnit[] = [];
const workspaceUnit = this._workspaceUnitMap.get(workspaceId); workspaceIds.forEach(workspaceId => {
if (workspaceUnit) { if (!scopedWorkspaceIds.has(workspaceId)) {
const ret = this._workspaceUnitMap.delete(workspaceId); return;
// If deletion failed, return.
if (!ret) {
return ret;
} }
const workspaceUnit = this._workspaceUnitMap.get(workspaceId);
if (workspaceUnit) {
const ret = this._workspaceUnitMap.delete(workspaceId);
// If deletion failed, return.
if (!ret) {
return;
}
scopedWorkspaceIds.delete(workspaceId); workspaceUnits.push(workspaceUnit);
scopedWorkspaceIds.delete(workspaceId);
}
});
this._events.emit('change', [ if (!workspaceUnits.length) {
{ return false;
deleted: workspaceUnit,
} as WorkspaceUnitCollectionChangeEvent,
]);
} }
this._events.emit('change', [
{
deleted: workspaceUnits,
} as WorkspaceUnitCollectionChangeEvent,
]);
return true; return true;
}; };
const clear = () => { const clear = () => {
scopedWorkspaceIds.forEach(id => { remove(Array.from(scopedWorkspaceIds));
remove(id);
});
}; };
const update = (workspaceId: string, meta: UpdateWorkspaceUnitParams) => { const update = (workspaceId: string, meta: UpdateWorkspaceUnitParams) => {

View File

@@ -52,7 +52,7 @@ export class WorkspaceUnit {
} }
setBlocksuiteWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace | null) { setBlocksuiteWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace | null) {
if (blocksuiteWorkspace && blocksuiteWorkspace?.room !== this.id) { if (blocksuiteWorkspace && blocksuiteWorkspace.room !== this.id) {
throw new Error('Workspace id inconsistent.'); throw new Error('Workspace id inconsistent.');
} }
this._blocksuiteWorkspace = blocksuiteWorkspace; this._blocksuiteWorkspace = blocksuiteWorkspace;

62
packages/i18n/README.md Normal file
View File

@@ -0,0 +1,62 @@
# i18n
## Usages
- Update missing translations into the base resources, a.k.a the `src/resources/en.json`
- Replace literal text with translation keys
```tsx
import { useTranslation } from '@affine/i18n';
// src/resources/en.json
// {
// 'Text': 'some text',
// 'Switch to language': 'Switch to {{language}}', // <- you can interpolation by curly brackets
// };
const App = () => {
const { t } = useTranslation();
const changeLanguage = (language: string) => {
i18n.changeLanguage(language);
};
return (
<div>
<div>{t('Text')}</div>
<button onClick={() => changeLanguage('en')}>
{t('Switch to language', { language: 'en' })}
</button>
<button onClick={() => changeLanguage('zh-Hans')}>
{t('Switch to language', { language: 'zh-Hans' })}
</button>
</div>
);
};
```
## How the i18n workflow works?
- When the `src/resources/en.json`(base language) updated and merged to the develop branch, will trigger the `languages-sync` action.
- The `languages-sync` action will check the base language and add missing translations to the Tolgee platform.
- This way, partners from the community can update the translations.
## How to sync translations manually
- Set token as environment variable
```shell
export TOLGEE_API_KEY=tgpak_XXXXXXX
```
- Run the `sync-languages:check` to check all languages
- Run the `sync-languages` script to add new keys to the Tolgee platform
- Run the `download-resources` script to download the latest full-translation translation resources from the Tolgee platform
## References
- [AFFiNE | Tolgee](https://i18n.affine.pro/)
- [Tolgee Documentation](https://tolgee.io/docs/)
- [i18next](https://www.i18next.com/)
- [react-i18next](https://react.i18next.com/)

Some files were not shown because too many files have changed in this diff Show More