mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-24 19:02:28 +03:00
Merge pull request #761 from toeverything/feat/datacenter
feat: sync features with new data center
This commit is contained in:
commit
cb2e6f97d0
@ -196,27 +196,24 @@ Thanks a lot to the community for providing such powerful and simple libraries,
|
||||
|
||||
## Self-Host
|
||||
|
||||
Get started with Docker and deploy your own feature-rich, restriction-free deployment of AFFiNE - check the [latest packages](https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted).
|
||||
Get started with Docker and deploy your own feature-rich, restriction-free deployment of AFFiNE - check the [latest packages](https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted).
|
||||
|
||||
## Hiring
|
||||
|
||||
Some amazing companies including AFFiNE are looking for developers! Are you interested in helping build with AFFiNE and/or its partners? Check out some of the latest [jobs available](./docs/jobs/summary.md).
|
||||
|
||||
|
||||
## Upgrading
|
||||
|
||||
For upgrading information please see our [update page](https://affine.pro/blog?tag=Release%20Note).
|
||||
|
||||
|
||||
## Feature Request
|
||||
|
||||
For feature request please see https://community.affine.pro/c/feature-requests/
|
||||
For feature request please see https://community.affine.pro/c/feature-requests/
|
||||
|
||||
## Is it awesome?
|
||||
|
||||
[These people](https://twitter.com/AffineOfficial/followers) seem to like it.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](/LICENSE) for details.
|
||||
|
@ -13,7 +13,7 @@ Use the table of contents icon on the top left corner of this document to get to
|
||||
Currently we have two versions of AFFiNE:
|
||||
|
||||
- [AFFiNE Pre-Alpha](https://livedemo.affine.pro/). This version users the branch `Pre-Alpha`, it is no longer actively developed but contains some different functions and features.
|
||||
- [AFFiNE Alpha](https://pathfinder.affine.pro/). This version uses the `master` branch, this is the latest version under active development.
|
||||
- [AFFiNE Alpha](https://pathfinder.affine.pro/). This version uses the `master` branch, this is the latest version under active development.
|
||||
|
||||
To get an overview of the project, read the [README](../README.md). Here are some resources to help you get started with open source contributions:
|
||||
|
||||
|
@ -28,6 +28,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.26.0",
|
||||
"@jest/globals": "^29.3.1",
|
||||
"@playwright/test": "^1.29.1",
|
||||
"@types/eslint": "^8.4.10",
|
||||
"@types/node": "^18.11.17",
|
||||
@ -37,8 +38,10 @@
|
||||
"eslint-config-next": "12.3.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"fake-indexeddb": "4.0.1",
|
||||
"got": "^12.5.3",
|
||||
"husky": "^8.0.2",
|
||||
"jest": "^29.3.1",
|
||||
"lint-staged": "^13.1.0",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.9.3"
|
||||
@ -65,7 +68,8 @@
|
||||
"reportUnusedDisableDirectives": true,
|
||||
"ignorePatterns": [
|
||||
"src/**/*.test.ts",
|
||||
"package/**/dist/*"
|
||||
"package/**/dist/*",
|
||||
"package/**/sync.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,9 @@ const EDITOR_VERSION = enableDebugLocal
|
||||
const profileTarget = {
|
||||
ac: '100.85.73.88:12001',
|
||||
dev: '100.77.180.48:11001',
|
||||
test: '100.85.73.88:12001',
|
||||
stage: '',
|
||||
pro: 'http://pathfinder.affine.pro',
|
||||
local: '127.0.0.1:3000',
|
||||
};
|
||||
|
||||
|
@ -11,10 +11,10 @@
|
||||
"dependencies": {
|
||||
"@affine/datacenter": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@blocksuite/blocks": "0.4.0-20230113023110-28a7fdc",
|
||||
"@blocksuite/editor": "0.4.0-20230113023110-28a7fdc",
|
||||
"@blocksuite/blocks": "0.4.0-20230201063624-4e0463b",
|
||||
"@blocksuite/editor": "0.4.0-20230201063624-4e0463b",
|
||||
"@blocksuite/icons": "^2.0.2",
|
||||
"@blocksuite/store": "0.4.0-20230113023110-28a7fdc",
|
||||
"@blocksuite/store": "0.4.0-20230201063624-4e0463b",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/server": "^11.10.0",
|
||||
|
@ -166,9 +166,9 @@ a:visited {
|
||||
input {
|
||||
border: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none; /*解决ios上按钮的圆角问题*/
|
||||
border-radius: 0; /*解决ios上输入框圆角问题*/
|
||||
outline: medium; /*去掉鼠标点击的默认黄色边框*/
|
||||
-webkit-appearance: none; /*Solve the rounded corners of buttons on ios*/
|
||||
border-radius: 0; /*Solve the problem of rounded corners of the input box on ios*/
|
||||
outline: medium; /*Remove the default yellow border on mouse click*/
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,24 @@
|
||||
import { NotFoundTitle, PageContainer } from './styles';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { Button } from '@/ui/button';
|
||||
import { useRouter } from 'next/router';
|
||||
export const NotfoundPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<PageContainer>
|
||||
<NotFoundTitle>{t('404 - Page Not Found')}</NotFoundTitle>
|
||||
<NotFoundTitle>
|
||||
{t('404 - Page Not Found')}
|
||||
<p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
router.push('/workspace');
|
||||
}}
|
||||
>
|
||||
{t('Back Home')}
|
||||
</Button>
|
||||
</p>
|
||||
</NotFoundTitle>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
@ -91,8 +91,6 @@ export const ContactModal = ({
|
||||
<span>Alpha</span>
|
||||
</StyledModalHeaderLeft>
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
|
121
packages/app/src/components/create-workspace/index.tsx
Normal file
121
packages/app/src/components/create-workspace/index.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { styled } from '@/styles';
|
||||
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
||||
import { Button } from '@/ui/button';
|
||||
import { useState } from 'react';
|
||||
import Input from '@/ui/input';
|
||||
import { KeyboardEvent } from 'react';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
|
||||
import { useRouter } from 'next/router';
|
||||
import { toast } from '@/ui/toast';
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => {
|
||||
const [workspaceName, setWorkspaceName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { createWorkspace } = useWorkspaceHelper();
|
||||
const router = useRouter();
|
||||
const handleCreateWorkspace = async () => {
|
||||
setLoading(true);
|
||||
const workspace = await createWorkspace(workspaceName);
|
||||
|
||||
if (workspace && workspace.id) {
|
||||
setLoading(false);
|
||||
router.replace(`/workspace/${workspace.id}`);
|
||||
onClose();
|
||||
} else {
|
||||
toast('create error');
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
// 👇 Get input value
|
||||
handleCreateWorkspace();
|
||||
}
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper width={560} height={342} style={{ padding: '10px' }}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>{t('New Workspace')}</ContentTitle>
|
||||
<p>{t('Workspace description')}</p>
|
||||
<Input
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('Set a Workspace name')}
|
||||
onChange={value => {
|
||||
setWorkspaceName(value);
|
||||
}}
|
||||
></Input>
|
||||
<Button
|
||||
disabled={!workspaceName}
|
||||
style={{
|
||||
width: '260px',
|
||||
textAlign: 'center',
|
||||
marginTop: '16px',
|
||||
opacity: !workspaceName ? 0.5 : 1,
|
||||
}}
|
||||
loading={loading}
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
handleCreateWorkspace();
|
||||
}}
|
||||
>
|
||||
{t('Create')}
|
||||
</Button>
|
||||
</Content>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = styled('div')({
|
||||
position: 'relative',
|
||||
height: '44px',
|
||||
});
|
||||
|
||||
const Content = styled('div')(({ theme }) => {
|
||||
return {
|
||||
padding: '0 84px',
|
||||
textAlign: 'center',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
color: theme.colors.inputColor,
|
||||
p: {
|
||||
marginTop: '12px',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ContentTitle = styled('div')(() => {
|
||||
return {
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
paddingBottom: '16px',
|
||||
};
|
||||
});
|
||||
|
||||
// const Footer = styled('div')({
|
||||
// height: '70px',
|
||||
// paddingLeft: '24px',
|
||||
// marginTop: '32px',
|
||||
// textAlign: 'center',
|
||||
// });
|
@ -29,8 +29,6 @@ export const DeleteModal = ({
|
||||
<ModalWrapper width={620} height={334}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
|
@ -14,78 +14,82 @@ import {
|
||||
UndoIcon,
|
||||
RedoIcon,
|
||||
} from './Icons';
|
||||
import { MuiSlide } from '@/ui/mui';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import useHistoryUpdated from '@/hooks/use-history-update';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
|
||||
const toolbarList1 = [
|
||||
{
|
||||
flavor: 'select',
|
||||
icon: <SelectIcon />,
|
||||
toolTip: 'Select',
|
||||
disable: false,
|
||||
callback: () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine.switch-mouse-mode', {
|
||||
detail: {
|
||||
type: 'default',
|
||||
},
|
||||
})
|
||||
);
|
||||
const useToolbarList1 = () => {
|
||||
const { t } = useTranslation();
|
||||
return [
|
||||
{
|
||||
flavor: 'select',
|
||||
icon: <SelectIcon />,
|
||||
toolTip: t('Select'),
|
||||
disable: false,
|
||||
callback: () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine.switch-mouse-mode', {
|
||||
detail: {
|
||||
type: 'default',
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
flavor: 'text',
|
||||
icon: <TextIcon />,
|
||||
toolTip: 'Text (coming soon)',
|
||||
disable: true,
|
||||
},
|
||||
{
|
||||
flavor: 'shape',
|
||||
icon: <ShapeIcon />,
|
||||
toolTip: 'Shape',
|
||||
disable: false,
|
||||
callback: () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine.switch-mouse-mode', {
|
||||
detail: {
|
||||
type: 'shape',
|
||||
color: 'black',
|
||||
shape: 'rectangle',
|
||||
},
|
||||
})
|
||||
);
|
||||
{
|
||||
flavor: 'text',
|
||||
icon: <TextIcon />,
|
||||
toolTip: t('Text'),
|
||||
disable: true,
|
||||
},
|
||||
{
|
||||
flavor: 'shape',
|
||||
icon: <ShapeIcon />,
|
||||
toolTip: t('Shape'),
|
||||
disable: false,
|
||||
callback: () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine.switch-mouse-mode', {
|
||||
detail: {
|
||||
type: 'shape',
|
||||
color: 'black',
|
||||
shape: 'rectangle',
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
flavor: 'sticky',
|
||||
icon: <StickerIcon />,
|
||||
toolTip: t('Sticky'),
|
||||
disable: true,
|
||||
},
|
||||
{
|
||||
flavor: 'pen',
|
||||
icon: <PenIcon />,
|
||||
toolTip: t('Pen'),
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
flavor: 'sticky',
|
||||
icon: <StickerIcon />,
|
||||
toolTip: 'Sticky (coming soon)',
|
||||
disable: true,
|
||||
},
|
||||
{
|
||||
flavor: 'pen',
|
||||
icon: <PenIcon />,
|
||||
toolTip: 'Pen (coming soon)',
|
||||
disable: true,
|
||||
},
|
||||
|
||||
{
|
||||
flavor: 'connector',
|
||||
icon: <ConnectorIcon />,
|
||||
toolTip: 'Connector (coming soon)',
|
||||
disable: true,
|
||||
},
|
||||
];
|
||||
{
|
||||
flavor: 'connector',
|
||||
icon: <ConnectorIcon />,
|
||||
toolTip: t('Connector'),
|
||||
disable: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const UndoRedo = () => {
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const { currentPage } = useAppState();
|
||||
const onHistoryUpdated = useHistoryUpdated();
|
||||
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
onHistoryUpdated(page => {
|
||||
setCanUndo(page.canUndo);
|
||||
@ -95,7 +99,7 @@ const UndoRedo = () => {
|
||||
|
||||
return (
|
||||
<StyledToolbarWrapper>
|
||||
<Tooltip content="Undo" placement="right-start">
|
||||
<Tooltip content={t('Undo')} placement="right-start">
|
||||
<StyledToolbarItem
|
||||
disable={!canUndo}
|
||||
onClick={() => {
|
||||
@ -105,7 +109,7 @@ const UndoRedo = () => {
|
||||
<UndoIcon />
|
||||
</StyledToolbarItem>
|
||||
</Tooltip>
|
||||
<Tooltip content="Redo" placement="right-start">
|
||||
<Tooltip content={t('Redo')} placement="right-start">
|
||||
<StyledToolbarItem
|
||||
disable={!canRedo}
|
||||
onClick={() => {
|
||||
@ -123,7 +127,7 @@ export const EdgelessToolbar = () => {
|
||||
const { mode } = useCurrentPageMeta() || {};
|
||||
|
||||
return (
|
||||
<Slide
|
||||
<MuiSlide
|
||||
direction="right"
|
||||
in={mode === 'edgeless'}
|
||||
mountOnEnter
|
||||
@ -131,7 +135,7 @@ export const EdgelessToolbar = () => {
|
||||
>
|
||||
<StyledEdgelessToolbar aria-label="edgeless-toolbar">
|
||||
<StyledToolbarWrapper>
|
||||
{toolbarList1.map(
|
||||
{useToolbarList1().map(
|
||||
({ icon, toolTip, flavor, disable, callback }, index) => {
|
||||
return (
|
||||
<Tooltip key={index} content={toolTip} placement="right-start">
|
||||
@ -151,7 +155,7 @@ export const EdgelessToolbar = () => {
|
||||
</StyledToolbarWrapper>
|
||||
<UndoRedo />
|
||||
</StyledEdgelessToolbar>
|
||||
</Slide>
|
||||
</MuiSlide>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
69
packages/app/src/components/editor/index.tsx
Normal file
69
packages/app/src/components/editor/index.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import '@blocksuite/blocks';
|
||||
import { EditorContainer } from '@blocksuite/editor';
|
||||
import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-Downhill.md';
|
||||
import { styled } from '@/styles';
|
||||
|
||||
const StyledEditorContainer = styled('div')(() => {
|
||||
return {
|
||||
height: 'calc(100vh - 60px)',
|
||||
padding: '0 32px',
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
page: Page;
|
||||
workspace: Workspace;
|
||||
setEditor: (editor: EditorContainer) => void;
|
||||
};
|
||||
|
||||
export const Editor = ({ page, workspace, setEditor }: Props) => {
|
||||
const editorContainer = useRef<HTMLDivElement>(null);
|
||||
// const { currentWorkspace, currentPage, setEditor } = useAppState();
|
||||
useEffect(() => {
|
||||
const ret = () => {
|
||||
const node = editorContainer.current;
|
||||
while (node?.firstChild) {
|
||||
node.removeChild(node.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
const editor = new EditorContainer();
|
||||
editor.page = page;
|
||||
editorContainer.current?.appendChild(editor);
|
||||
if (page.isEmpty) {
|
||||
const isFirstPage = workspace?.meta.pageMetas.length === 1;
|
||||
// Can not use useCurrentPageMeta to get new title, cause meta title will trigger rerender, but the second time can not remove title
|
||||
const { title: metaTitle } = page.meta;
|
||||
const title = metaTitle
|
||||
? metaTitle
|
||||
: isFirstPage
|
||||
? 'Welcome to AFFiNE Alpha "Downhill"'
|
||||
: '';
|
||||
workspace?.setPageMeta(page.id, { title });
|
||||
const pageBlockId = page.addBlockByFlavour('affine:page', { title });
|
||||
page.addBlockByFlavour('affine:surface', {}, null);
|
||||
// Add frame block inside page block
|
||||
const frameId = page.addBlockByFlavour('affine:frame', {}, pageBlockId);
|
||||
// Add paragraph block inside frame block
|
||||
// If this is a first page in workspace, init an introduction markdown
|
||||
if (isFirstPage) {
|
||||
editor.clipboard.importMarkdown(exampleMarkdown, frameId);
|
||||
workspace.setPageMeta(page.id, { title });
|
||||
page.resetHistory();
|
||||
} else {
|
||||
page.addBlockByFlavour('affine:paragraph', {}, frameId);
|
||||
}
|
||||
page.resetHistory();
|
||||
}
|
||||
|
||||
setEditor(editor);
|
||||
document.title = page.meta.title || 'Untitled';
|
||||
return ret;
|
||||
}, [workspace, page, setEditor]);
|
||||
|
||||
return <StyledEditorContainer ref={editorContainer} />;
|
||||
};
|
||||
|
||||
export default Editor;
|
24
packages/app/src/components/enable-workspace/index.tsx
Normal file
24
packages/app/src/components/enable-workspace/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { Button } from '@/ui/button';
|
||||
import { FC, useRef, ChangeEvent, ReactElement } from 'react';
|
||||
import { styled } from '@/styles';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
interface Props {
|
||||
uploadType?: string;
|
||||
children?: ReactElement;
|
||||
@ -9,6 +10,7 @@ interface Props {
|
||||
}
|
||||
export const Upload: FC<Props> = props => {
|
||||
const { fileChange, accept } = props;
|
||||
const { t } = useTranslation();
|
||||
const input_ref = useRef<HTMLInputElement>(null);
|
||||
const _chooseFile = () => {
|
||||
if (input_ref.current) {
|
||||
@ -28,7 +30,7 @@ export const Upload: FC<Props> = props => {
|
||||
};
|
||||
return (
|
||||
<UploadStyle onClick={_chooseFile}>
|
||||
{props.children ?? <Button>Upload</Button>}
|
||||
{props.children ?? <Button>{t('Upload')}</Button>}
|
||||
<input
|
||||
ref={input_ref}
|
||||
type="file"
|
||||
|
@ -6,12 +6,12 @@ import {
|
||||
StyledTitleWrapper,
|
||||
} from './styles';
|
||||
import { Content } from '@/ui/layout';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import EditorModeSwitch from '@/components/editor-mode-switch';
|
||||
import QuickSearchButton from './QuickSearchButton';
|
||||
import Header from './Header';
|
||||
import usePropsUpdated from '@/hooks/use-props-updated';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
import QuickSearchButton from './QuickSearchButton';
|
||||
|
||||
export const EditorHeader = () => {
|
||||
const [title, setTitle] = useState('');
|
||||
|
@ -6,11 +6,12 @@ import {
|
||||
StyledBrowserWarning,
|
||||
StyledCloseButton,
|
||||
} from './styles';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { getWarningMessage, shouldShowWarning } from './utils';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import { useWarningMessage, shouldShowWarning } from './utils';
|
||||
import EditorOptionMenu from './header-right-items/EditorOptionMenu';
|
||||
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
|
||||
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
|
||||
import SyncUser from './header-right-items/SyncUser';
|
||||
|
||||
const BrowserWarning = ({
|
||||
show,
|
||||
@ -21,7 +22,7 @@ const BrowserWarning = ({
|
||||
}) => {
|
||||
return (
|
||||
<StyledBrowserWarning show={show}>
|
||||
{getWarningMessage()}
|
||||
{useWarningMessage()}
|
||||
<StyledCloseButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</StyledCloseButton>
|
||||
@ -39,11 +40,11 @@ const HeaderRightItems: Record<HeaderRightItemNames, ReactNode> = {
|
||||
editorOptionMenu: <EditorOptionMenu key="editorOptionMenu" />,
|
||||
trashButtonGroup: <TrashButtonGroup key="trashButtonGroup" />,
|
||||
themeModeSwitch: <ThemeModeSwitch key="themeModeSwitch" />,
|
||||
syncUser: null, //<SyncUser key="syncUser" />,
|
||||
syncUser: <SyncUser key="syncUser" />,
|
||||
};
|
||||
|
||||
export const Header = ({
|
||||
rightItems = ['syncUser'],
|
||||
rightItems = ['syncUser', 'themeModeSwitch'],
|
||||
children,
|
||||
}: PropsWithChildren<{ rightItems?: HeaderRightItemNames[] }>) => {
|
||||
const [showWarning, setShowWarning] = useState(shouldShowWarning());
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import Header from './Header';
|
||||
import { StyledPageListTittleWrapper } from './styles';
|
||||
import QuickSearchButton from './QuickSearchButton';
|
||||
// import QuickSearchButton from './QuickSearchButton';
|
||||
|
||||
export type PageListHeaderProps = PropsWithChildren<{
|
||||
icon?: ReactNode;
|
||||
@ -12,7 +12,7 @@ export const PageListHeader = ({ icon, children }: PageListHeaderProps) => {
|
||||
<StyledPageListTittleWrapper>
|
||||
{icon}
|
||||
{children}
|
||||
<QuickSearchButton style={{ marginLeft: '5px' }} />
|
||||
{/* <QuickSearchButton style={{ marginLeft: '5px' }} /> */}
|
||||
</StyledPageListTittleWrapper>
|
||||
</Header>
|
||||
);
|
||||
|
@ -2,13 +2,30 @@ import React from 'react';
|
||||
import { IconButton, IconButtonProps } from '@/ui/button';
|
||||
import { ArrowDownIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { styled } from '@/styles';
|
||||
|
||||
const StyledIconButtonWithAnimate = styled(IconButton)(({ theme }) => {
|
||||
return {
|
||||
svg: {
|
||||
transition: 'transform 0.15s ease-in-out',
|
||||
},
|
||||
':hover': {
|
||||
svg: {
|
||||
transform: 'translateY(3px)',
|
||||
},
|
||||
'::after': {
|
||||
background: theme.colors.pageBackground,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
export const QuickSearchButton = ({
|
||||
onClick,
|
||||
...props
|
||||
}: Omit<IconButtonProps, 'children'>) => {
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
return (
|
||||
<IconButton
|
||||
<StyledIconButtonWithAnimate
|
||||
data-testid="header-quickSearchButton"
|
||||
{...props}
|
||||
onClick={e => {
|
||||
@ -17,7 +34,7 @@ export const QuickSearchButton = ({
|
||||
}}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</IconButton>
|
||||
</StyledIconButtonWithAnimate>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { IconButton } from '@/ui/button';
|
||||
|
||||
export const SyncUser = () => {
|
||||
|
@ -4,15 +4,16 @@ import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useConfirm } from '@/providers/ConfirmProvider';
|
||||
import { useRouter } from 'next/router';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
|
||||
export const TrashButtonGroup = () => {
|
||||
const { permanentlyDeletePage } = usePageHelper();
|
||||
const { currentWorkspaceId } = useAppState();
|
||||
const { currentWorkspace } = useAppState();
|
||||
const { toggleDeletePage } = usePageHelper();
|
||||
const { confirm } = useConfirm();
|
||||
const router = useRouter();
|
||||
const { id = '' } = useCurrentPageMeta() || {};
|
||||
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
@ -23,7 +24,7 @@ export const TrashButtonGroup = () => {
|
||||
toggleDeletePage(id);
|
||||
}}
|
||||
>
|
||||
Restore it
|
||||
{t('Restore it')}
|
||||
</Button>
|
||||
<Button
|
||||
bold={true}
|
||||
@ -31,20 +32,19 @@ export const TrashButtonGroup = () => {
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Permanently delete',
|
||||
content:
|
||||
"Once deleted, you can't undo this action. Do you confirm?",
|
||||
confirmText: 'Delete',
|
||||
title: t('TrashButtonGroupTitle'),
|
||||
content: t('TrashButtonGroupDescription'),
|
||||
confirmText: t('Delete'),
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
if (confirm) {
|
||||
router.push(`/workspace/${currentWorkspaceId}/all`);
|
||||
router.push(`/workspace/${currentWorkspace?.id}/all`);
|
||||
permanentlyDeletePage(id);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete permanently
|
||||
{t('Delete permanently')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
@ -3,35 +3,25 @@ import { displayFlex, styled } from '@/styles';
|
||||
export const StyledHeaderContainer = styled.div<{ hasWarning: boolean }>(
|
||||
({ hasWarning }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
height: hasWarning ? '96px' : '60px',
|
||||
};
|
||||
}
|
||||
);
|
||||
export const StyledHeader = styled.div<{ hasWarning: boolean }>(
|
||||
({ hasWarning }) => {
|
||||
return {
|
||||
height: '60px',
|
||||
width: '100%',
|
||||
...displayFlex('flex-end', 'center'),
|
||||
background: 'var(--affine-page-background)',
|
||||
transition: 'background-color 0.5s',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: hasWarning ? '36px' : '0',
|
||||
padding: '0 22px',
|
||||
zIndex: 99,
|
||||
};
|
||||
}
|
||||
);
|
||||
export const StyledHeader = styled.div<{ hasWarning: boolean }>(() => {
|
||||
return {
|
||||
height: '60px',
|
||||
width: '100%',
|
||||
...displayFlex('flex-end', 'center'),
|
||||
background: 'var(--affine-page-background)',
|
||||
transition: 'background-color 0.5s',
|
||||
zIndex: 99,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledTitle = styled('div')(({ theme }) => ({
|
||||
width: '720px',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
|
||||
margin: 'auto',
|
||||
|
||||
...displayFlex('center', 'center'),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import getIsMobile from '@/utils/get-is-mobile';
|
||||
import { Trans, useTranslation } from '@affine/i18n';
|
||||
// Inspire by https://stackoverflow.com/a/4900484/8415727
|
||||
const getChromeVersion = () => {
|
||||
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()) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
if (getChromeVersion() < minimumChromeVersion) {
|
||||
return (
|
||||
<span>
|
||||
Please upgrade to the latest version of Chrome for the best experience.
|
||||
</span>
|
||||
);
|
||||
return <span>{t('upgradeBrowser')}</span>;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
StyledTransformIcon,
|
||||
} from './style';
|
||||
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import { MuiGrow } from '@/ui/mui';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
@ -35,7 +35,7 @@ export const HelpIsland = ({
|
||||
setShowContent(false);
|
||||
}}
|
||||
>
|
||||
<Grow in={showContent}>
|
||||
<MuiGrow in={showContent}>
|
||||
<StyledIslandWrapper>
|
||||
{showList.includes('contact') && (
|
||||
<Tooltip content={t('Contact Us')} placement="left-end">
|
||||
@ -66,7 +66,7 @@ export const HelpIsland = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledIslandWrapper>
|
||||
</Grow>
|
||||
</MuiGrow>
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
<StyledIconWrapper
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
||||
import { StyledButtonWrapper, StyledTitle } from './styles';
|
||||
import { Button } from '@/ui/button';
|
||||
import { Wrapper, Content } from '@/ui/layout';
|
||||
import { Content, FlexWrapper } from '@/ui/layout';
|
||||
import Loading from '@/components/loading';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
// import { Tooltip } from '@/ui/tooltip';
|
||||
@ -22,21 +22,28 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => {
|
||||
const { currentWorkspace } = useAppState();
|
||||
const { t } = useTranslation();
|
||||
const _applyTemplate = function (pageId: string, template: Template) {
|
||||
const page = currentWorkspace?.getPage(pageId);
|
||||
const page = currentWorkspace?.blocksuiteWorkspace?.getPage(pageId);
|
||||
|
||||
const title = template.name;
|
||||
if (page) {
|
||||
currentWorkspace?.setPageMeta(page.id, { title });
|
||||
currentWorkspace?.blocksuiteWorkspace?.setPageMeta(page.id, { title });
|
||||
if (page && page.root === null) {
|
||||
setTimeout(() => {
|
||||
const editor = document.querySelector('editor-container');
|
||||
if (editor) {
|
||||
page.addBlock({ flavour: 'affine:surface' }, null);
|
||||
const frameId = page.addBlock({ flavour: 'affine:frame' }, pageId);
|
||||
// TODO blocksuite should offer a method to import markdown from store
|
||||
editor.clipboard.importMarkdown(template.source, `${frameId}`);
|
||||
page.resetHistory();
|
||||
editor.requestUpdate();
|
||||
try {
|
||||
const editor = document.querySelector('editor-container');
|
||||
if (editor) {
|
||||
page.addBlock({ flavour: 'affine:surface' }, null);
|
||||
const frameId = page.addBlock(
|
||||
{ flavour: 'affine:frame' },
|
||||
pageId
|
||||
);
|
||||
// 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);
|
||||
}
|
||||
@ -98,18 +105,18 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => {
|
||||
>
|
||||
Markdown
|
||||
</Button>
|
||||
{/* <Button
|
||||
<Button
|
||||
onClick={() => {
|
||||
_handleAppleTemplateFromFilePicker();
|
||||
}}
|
||||
>
|
||||
HTML
|
||||
</Button> */}
|
||||
</Button>
|
||||
</StyledButtonWrapper>
|
||||
)}
|
||||
|
||||
{status === 'importing' && (
|
||||
<Wrapper
|
||||
<FlexWrapper
|
||||
wrap={true}
|
||||
justifyContent="center"
|
||||
style={{ marginTop: 22, paddingBottom: '32px' }}
|
||||
@ -119,7 +126,7 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => {
|
||||
OOOOPS! Sorry forgot to remind you that we are working on the
|
||||
import function
|
||||
</Content>
|
||||
</Wrapper>
|
||||
</FlexWrapper>
|
||||
)}
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { styled } from '@/styles';
|
||||
import Loading from './Loading';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
|
||||
// Used for the full page loading
|
||||
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 (
|
||||
<StyledLoadingContainer>
|
||||
<div className="wrapper">
|
||||
<Loading />
|
||||
<h1>{text}</h1>
|
||||
<h1>{text ? text : t('Loading')}</h1>
|
||||
</div>
|
||||
</StyledLoadingContainer>
|
||||
);
|
||||
|
@ -1,19 +1,22 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
// Inspired by https://codepen.io/graphilla/pen/rNvBMYY
|
||||
export const StyledLoadingWrapper = styled.div<{ size?: number }>(
|
||||
({ size = 40 }) => {
|
||||
return {
|
||||
width: size * 4,
|
||||
height: size * 4,
|
||||
position: 'relative',
|
||||
};
|
||||
}
|
||||
);
|
||||
export const StyledLoadingWrapper = styled('div', {
|
||||
shouldForwardProp: prop => {
|
||||
return !['size'].includes(prop);
|
||||
},
|
||||
})<{ size?: number }>(({ size = 40 }) => {
|
||||
return {
|
||||
width: size * 4,
|
||||
height: size * 4,
|
||||
position: 'relative',
|
||||
};
|
||||
});
|
||||
export const StyledLoading = styled.div`
|
||||
position: absolute;
|
||||
left: 25%;
|
||||
top: 25%;
|
||||
top: 50%;
|
||||
transform: rotateX(55deg) rotateZ(-45deg);
|
||||
@keyframes slide {
|
||||
0% {
|
||||
transform: translate(var(--sx), var(--sy));
|
||||
|
@ -13,8 +13,6 @@ export const GoogleIcon = () => {
|
||||
};
|
||||
|
||||
const GoogleIconWrapper = styled('div')(({ theme }) => ({
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
background: theme.colors.pageBackground,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
@ -1,38 +1,24 @@
|
||||
import { getDataCenter } from '@affine/datacenter';
|
||||
import { styled } from '@/styles';
|
||||
import { Button } from '@/ui/button';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { GoogleIcon, StayLogOutIcon } from './Icons';
|
||||
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
export const GoogleLoginButton = () => {
|
||||
const { triggerLoginModal } = useModal();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<StyledGoogleButton
|
||||
onClick={() => {
|
||||
getDataCenter()
|
||||
.then(dc => dc.apis.signInWithGoogle?.())
|
||||
.then(() => {
|
||||
triggerLoginModal();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('sign google error', error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StyledGoogleButton>
|
||||
<ButtonWrapper>
|
||||
<IconWrapper>
|
||||
<GoogleIcon />
|
||||
</IconWrapper>
|
||||
<TextWrapper>
|
||||
<Title>Continue with Google</Title>
|
||||
<Description>Set up an AFFiNE account to sync data</Description>
|
||||
</TextWrapper>
|
||||
<TextWrapper>{t('Continue with Google')}</TextWrapper>
|
||||
</ButtonWrapper>
|
||||
</StyledGoogleButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const StayLogOutButton = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StyledStayLogOutButton>
|
||||
<ButtonWrapper>
|
||||
@ -40,29 +26,26 @@ export const StayLogOutButton = () => {
|
||||
<StayLogOutIcon />
|
||||
</IconWrapper>
|
||||
<TextWrapper>
|
||||
<Title>Stay logged out</Title>
|
||||
<Description>All changes are saved locally</Description>
|
||||
<Title>{t('Stay logged out')}</Title>
|
||||
<Description>{t('All changes are saved locally')}</Description>
|
||||
</TextWrapper>
|
||||
</ButtonWrapper>
|
||||
</StyledStayLogOutButton>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledGoogleButton = styled(Button)(() => {
|
||||
const StyledGoogleButton = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '361px',
|
||||
height: '56px',
|
||||
padding: '4px',
|
||||
background: '#6880FF',
|
||||
color: '#fff',
|
||||
|
||||
'& > span': {
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
width: '284px',
|
||||
height: '40px',
|
||||
marginTop: '30px',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '40px',
|
||||
border: `1px solid ${theme.colors.iconColor}`,
|
||||
overflow: 'hidden',
|
||||
':hover': {
|
||||
background: '#516BF4',
|
||||
color: '#fff',
|
||||
border: `1px solid ${theme.colors.primaryColor}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
@ -72,11 +55,6 @@ const StyledStayLogOutButton = styled(Button)(() => {
|
||||
width: '361px',
|
||||
height: '56px',
|
||||
padding: '4px',
|
||||
|
||||
'& > span': {
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
':hover': {
|
||||
borderColor: '#6880FF',
|
||||
},
|
||||
@ -86,20 +64,22 @@ const StyledStayLogOutButton = styled(Button)(() => {
|
||||
const ButtonWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
const IconWrapper = styled('div')({
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
flex: '0 48px',
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
marginRight: '12px',
|
||||
marginTop: '8px',
|
||||
});
|
||||
|
||||
const TextWrapper = styled('div')({
|
||||
flex: 1,
|
||||
textAlign: 'left',
|
||||
height: '40px',
|
||||
lineHeight: '40px',
|
||||
});
|
||||
|
||||
const Title = styled('h1')(() => {
|
||||
|
@ -1,35 +1,38 @@
|
||||
import { ResetIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
||||
import { TextButton } from '@/ui/button';
|
||||
import { GoogleLoginButton, StayLogOutButton } from './LoginOptionButton';
|
||||
|
||||
import { GoogleLoginButton } from './LoginOptionButton';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
interface LoginModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const LoginModal = ({ open, onClose }: LoginModalProps) => {
|
||||
const { login } = useAppState();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} data-testid="login-modal">
|
||||
<ModalWrapper width={620} height={334}>
|
||||
<ModalWrapper width={560} height={292}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>Currently not logged in</ContentTitle>
|
||||
<GoogleLoginButton />
|
||||
<StayLogOutButton />
|
||||
<ContentTitle>{t('Sign in')}</ContentTitle>
|
||||
<SignDes>{t('Set up an AFFiNE account to sync data')}</SignDes>
|
||||
<span
|
||||
onClick={async () => {
|
||||
await login();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<GoogleLoginButton />
|
||||
</span>
|
||||
</Content>
|
||||
<Footer>
|
||||
<TextButton icon={<StyledResetIcon />}>Clear local data</TextButton>
|
||||
</Footer>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
@ -55,14 +58,10 @@ const ContentTitle = styled('h1')({
|
||||
paddingBottom: '16px',
|
||||
});
|
||||
|
||||
const Footer = styled('div')({
|
||||
height: '70px',
|
||||
paddingLeft: '24px',
|
||||
marginTop: '32px',
|
||||
});
|
||||
|
||||
const StyledResetIcon = styled(ResetIcon)({
|
||||
marginRight: '12px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
const SignDes = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontWeight: 400,
|
||||
color: theme.colors.textColor,
|
||||
fontSize: '16px',
|
||||
};
|
||||
});
|
||||
|
48
packages/app/src/components/logout-modal/icon.tsx
Normal file
48
packages/app/src/components/logout-modal/icon.tsx
Normal 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 clipPath="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>
|
||||
);
|
||||
};
|
118
packages/app/src/components/logout-modal/index.tsx
Normal file
118
packages/app/src/components/logout-modal/index.tsx
Normal 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';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
interface LoginModalProps {
|
||||
open: boolean;
|
||||
onClose: (wait: boolean) => void;
|
||||
}
|
||||
|
||||
export const LogoutModal = ({ open, onClose }: LoginModalProps) => {
|
||||
const [localCache, setLocalCache] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} data-testid="logout-modal">
|
||||
<ModalWrapper width={560} height={292}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
onClick={() => {
|
||||
onClose(true);
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>{t('Sign out')}?</ContentTitle>
|
||||
<SignDes>{t('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>
|
||||
)}
|
||||
{t('Retain local cached data')}
|
||||
</StyleTips>
|
||||
<div>
|
||||
<Button
|
||||
style={{ marginRight: '16px' }}
|
||||
shape="round"
|
||||
onClick={() => {
|
||||
onClose(true);
|
||||
}}
|
||||
>
|
||||
{t('Wait for Sync')}
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
shape="round"
|
||||
onClick={() => {
|
||||
onClose(false);
|
||||
}}
|
||||
>
|
||||
{t('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',
|
||||
};
|
||||
});
|
@ -3,8 +3,10 @@ import Modal, { ModalCloseButton, ModalWrapper } from '@/ui/modal';
|
||||
import getIsMobile from '@/utils/get-is-mobile';
|
||||
import { StyledButton, StyledContent, StyledTitle } from './styles';
|
||||
import bg from './bg.png';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
export const MobileModal = () => {
|
||||
const [showModal, setShowModal] = useState(getIsMobile());
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal
|
||||
open={showModal}
|
||||
@ -25,20 +27,17 @@ export const MobileModal = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<StyledTitle>Ooops!</StyledTitle>
|
||||
<StyledTitle>{t('Ooops!')}</StyledTitle>
|
||||
<StyledContent>
|
||||
<p>Looks like you are browsing on a mobile device.</p>
|
||||
<p>
|
||||
We are still working on mobile support and recommend you use a
|
||||
desktop device.
|
||||
</p>
|
||||
<p>{t('mobile device')}</p>
|
||||
<p>{t('mobile device description')}</p>
|
||||
</StyledContent>
|
||||
<StyledButton
|
||||
onClick={() => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
>
|
||||
Got it
|
||||
{t('Got it')}
|
||||
</StyledButton>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
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 (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Empty
|
||||
@ -8,8 +11,10 @@ export const PageListEmpty = () => {
|
||||
height={300}
|
||||
sx={{ marginTop: '100px', marginBottom: '30px' }}
|
||||
/>
|
||||
<p>Tips: Click Add to Favourites/Trash and the page will appear here.</p>
|
||||
<p>(Designer is grappling with designing)</p>
|
||||
{listType === 'all' && <p>{t('emptyAllPages')}</p>}
|
||||
{listType === 'favorite' && <p>{t('emptyFavourite')}</p>}
|
||||
{listType === 'trash' && <p>{t('emptyTrash')}</p>}
|
||||
<p>{t('still designed')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useConfirm } from '@/providers/ConfirmProvider';
|
||||
import { PageMeta } from '@/providers/app-state-provider';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { Wrapper } from '@/ui/layout';
|
||||
import { FlexWrapper } from '@/ui/layout';
|
||||
import { IconButton } from '@/ui/button';
|
||||
import {
|
||||
MoreVerticalIcon,
|
||||
@ -63,13 +63,13 @@ export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Wrapper alignItems="center" justifyContent="center">
|
||||
<FlexWrapper alignItems="center" justifyContent="center">
|
||||
<Menu content={OperationMenu} placement="bottom-end" disablePortal={true}>
|
||||
<IconButton darker={true}>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</Wrapper>
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@ -80,7 +80,7 @@ export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
const { confirm } = useConfirm();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Wrapper>
|
||||
<FlexWrapper>
|
||||
<IconButton
|
||||
darker={true}
|
||||
style={{ marginRight: '12px' }}
|
||||
@ -108,6 +108,6 @@ export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Wrapper>
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ import DateCell from '@/components/page-list/DateCell';
|
||||
import { IconButton } from '@/ui/button';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { toast } from '@/ui/toast';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
import { useTheme } from '@/providers/ThemeProvider';
|
||||
@ -51,7 +51,7 @@ const FavoriteTag = ({
|
||||
style={{
|
||||
color: favorite ? theme.colors.primaryColor : theme.colors.iconColor,
|
||||
}}
|
||||
className="favorite-button"
|
||||
className={favorite ? '' : 'favorite-button'}
|
||||
>
|
||||
{favorite ? (
|
||||
<FavouritedIcon data-testid="favourited-icon" />
|
||||
@ -67,16 +67,20 @@ export const PageList = ({
|
||||
pageList,
|
||||
showFavoriteTag = false,
|
||||
isTrash = false,
|
||||
isPublic = false,
|
||||
listType,
|
||||
}: {
|
||||
pageList: PageMeta[];
|
||||
showFavoriteTag?: boolean;
|
||||
isTrash?: boolean;
|
||||
isPublic?: boolean;
|
||||
listType?: 'all' | 'trash' | 'favorite';
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { currentWorkspaceId } = useAppState();
|
||||
const { currentWorkspace } = useAppState();
|
||||
const { t } = useTranslation();
|
||||
if (pageList.length === 0) {
|
||||
return <Empty />;
|
||||
return <Empty listType={listType} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -98,9 +102,15 @@ export const PageList = ({
|
||||
<StyledTableRow
|
||||
key={`${pageMeta.id}-${index}`}
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/workspace/${currentWorkspaceId}/${pageMeta.id}`
|
||||
);
|
||||
if (isPublic) {
|
||||
router.push(
|
||||
`/public-workspace/${router.query.workspaceId}/${pageMeta.id}`
|
||||
);
|
||||
} else {
|
||||
router.push(
|
||||
`/workspace/${currentWorkspace?.id}/${pageMeta.id}`
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
@ -124,19 +134,21 @@ export const PageList = ({
|
||||
dateKey={isTrash ? 'trashDate' : 'updatedDate'}
|
||||
backupKey={isTrash ? 'trashDate' : 'createDate'}
|
||||
/>
|
||||
<TableCell
|
||||
style={{ padding: 0 }}
|
||||
data-testid={`more-actions-${pageMeta.id}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{isTrash ? (
|
||||
<TrashOperationCell pageMeta={pageMeta} />
|
||||
) : (
|
||||
<OperationCell pageMeta={pageMeta} />
|
||||
)}
|
||||
</TableCell>
|
||||
{!isPublic ? (
|
||||
<TableCell
|
||||
style={{ padding: 0 }}
|
||||
data-testid={`more-actions-${pageMeta.id}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{isTrash ? (
|
||||
<TrashOperationCell pageMeta={pageMeta} />
|
||||
) : (
|
||||
<OperationCell pageMeta={pageMeta} />
|
||||
)}
|
||||
</TableCell>
|
||||
) : null}
|
||||
</StyledTableRow>
|
||||
);
|
||||
})}
|
||||
|
@ -8,20 +8,18 @@ import React, {
|
||||
import { SearchIcon } from '@blocksuite/icons';
|
||||
import { StyledInputContent, StyledLabel } from './style';
|
||||
import { Command } from 'cmdk';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
export const Input = (props: {
|
||||
query: string;
|
||||
setQuery: Dispatch<SetStateAction<string>>;
|
||||
setLoading: Dispatch<SetStateAction<boolean>>;
|
||||
isPublic: boolean;
|
||||
publishWorkspaceName: string | undefined;
|
||||
}) => {
|
||||
const [isComposition, setIsComposition] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { currentWorkspaceId, workspacesMeta, currentWorkspace } =
|
||||
useAppState();
|
||||
const isPublic = workspacesMeta.find(
|
||||
meta => String(meta.id) === String(currentWorkspaceId)
|
||||
)?.public;
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
inputRef.current?.addEventListener(
|
||||
'blur',
|
||||
@ -80,9 +78,11 @@ export const Input = (props: {
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
isPublic
|
||||
? `Search in ${currentWorkspace?.meta.name}`
|
||||
: 'Quick Search...'
|
||||
props.isPublic
|
||||
? t('Quick search placeholder2', {
|
||||
workspace: props.publishWorkspaceName,
|
||||
})
|
||||
: t('Quick search placeholder')
|
||||
}
|
||||
/>
|
||||
</StyledInputContent>
|
||||
|
@ -0,0 +1,95 @@
|
||||
import { Command } from 'cmdk';
|
||||
import { StyledListItem, StyledNotFound } from './style';
|
||||
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { useAppState, PageMeta } from '@/providers/app-state-provider';
|
||||
import { useRouter } from 'next/router';
|
||||
import { NoResultSVG } from './NoResultSVG';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import usePageHelper from '@/hooks/use-page-helper';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
|
||||
export const PublishedResults = (props: {
|
||||
query: string;
|
||||
loading: boolean;
|
||||
setLoading: Dispatch<SetStateAction<boolean>>;
|
||||
setPublishWorkspaceName: Dispatch<SetStateAction<string>>;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const [workspace, setWorkspace] = useState<Workspace>();
|
||||
const { query, loading, setLoading, onClose, setPublishWorkspaceName } =
|
||||
props;
|
||||
const { search } = usePageHelper();
|
||||
const [results, setResults] = useState(new Map<string, string | undefined>());
|
||||
const { dataCenter } = useAppState();
|
||||
const router = useRouter();
|
||||
const [pageList, setPageList] = useState<PageMeta[]>([]);
|
||||
useEffect(() => {
|
||||
dataCenter
|
||||
.loadPublicWorkspace(router.query.workspaceId as string)
|
||||
.then(data => {
|
||||
setPageList(data.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]);
|
||||
if (data.blocksuiteWorkspace) {
|
||||
setWorkspace(data.blocksuiteWorkspace);
|
||||
setPublishWorkspaceName(data.blocksuiteWorkspace.meta.name);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
router.push('/404');
|
||||
});
|
||||
}, [router, dataCenter, setPublishWorkspaceName]);
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
setResults(search(query, workspace));
|
||||
setLoading(false);
|
||||
//Save the Map<BlockId, PageId> obtained from the search as state
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query, setResults, setLoading]);
|
||||
const pageIds = [...results.values()];
|
||||
const resultsPageMeta = pageList.filter(
|
||||
page => pageIds.indexOf(page.id) > -1 && !page.trash
|
||||
);
|
||||
|
||||
return loading ? null : (
|
||||
<>
|
||||
{query ? (
|
||||
resultsPageMeta.length ? (
|
||||
<Command.Group
|
||||
heading={t('Find results', { number: resultsPageMeta.length })}
|
||||
>
|
||||
{resultsPageMeta.map(result => {
|
||||
return (
|
||||
<Command.Item
|
||||
key={result.id}
|
||||
onSelect={() => {
|
||||
router.push(
|
||||
`/public-workspace/${router.query.workspaceId}/${result.id}`
|
||||
);
|
||||
onClose();
|
||||
}}
|
||||
value={result.id}
|
||||
>
|
||||
<StyledListItem>
|
||||
{result.mode === 'edgeless' ? (
|
||||
<EdgelessIcon />
|
||||
) : (
|
||||
<PaperIcon />
|
||||
)}
|
||||
<span>{result.title}</span>
|
||||
</StyledListItem>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
) : (
|
||||
<StyledNotFound>
|
||||
<span>{t('Find 0 result')}</span>
|
||||
<NoResultSVG />
|
||||
</StyledNotFound>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -9,7 +9,6 @@ import { useSwitchToConfig } from './config';
|
||||
import { NoResultSVG } from './NoResultSVG';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import usePageHelper from '@/hooks/use-page-helper';
|
||||
import usePageMetaList from '@/hooks/use-page-meta-list';
|
||||
export const Results = (props: {
|
||||
query: string;
|
||||
loading: boolean;
|
||||
@ -21,12 +20,11 @@ export const Results = (props: {
|
||||
const setLoading = props.setLoading;
|
||||
const setShowCreatePage = props.setShowCreatePage;
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
const pageMetaList = usePageMetaList();
|
||||
const { openPage } = usePageHelper();
|
||||
const router = useRouter();
|
||||
const { currentWorkspaceId } = useAppState();
|
||||
const { currentWorkspace, pageList } = useAppState();
|
||||
const { search } = usePageHelper();
|
||||
const List = useSwitchToConfig(currentWorkspaceId);
|
||||
const List = useSwitchToConfig(currentWorkspace?.id);
|
||||
const [results, setResults] = useState(new Map<string, string | undefined>());
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
@ -37,12 +35,12 @@ export const Results = (props: {
|
||||
}, [query, setResults, setLoading]);
|
||||
const pageIds = [...results.values()];
|
||||
|
||||
const resultsPageMeta = pageMetaList.filter(
|
||||
const resultsPageMeta = pageList.filter(
|
||||
page => pageIds.indexOf(page.id) > -1 && !page.trash
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCreatePage(resultsPageMeta.length ? false : true);
|
||||
setShowCreatePage(!resultsPageMeta.length);
|
||||
//Determine whether to display the ‘+ New page’
|
||||
}, [resultsPageMeta, setShowCreatePage]);
|
||||
return loading ? null : (
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { FC, SVGProps } from 'react';
|
||||
import { AllPagesIcon, FavouritesIcon, TrashIcon } from '@blocksuite/icons';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
|
||||
export const useSwitchToConfig = (
|
||||
currentWorkspaceId: string
|
||||
currentWorkspaceId?: string
|
||||
): {
|
||||
title: string;
|
||||
href: string;
|
||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
icon: FC<SVGProps<SVGSVGElement>>;
|
||||
}[] => {
|
||||
const { t } = useTranslation();
|
||||
const List = [
|
||||
return [
|
||||
{
|
||||
title: t('All pages'),
|
||||
href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/all` : '',
|
||||
@ -28,5 +29,4 @@ export const useSwitchToConfig = (
|
||||
icon: TrashIcon,
|
||||
},
|
||||
];
|
||||
return List;
|
||||
};
|
||||
|
@ -13,7 +13,8 @@ import { Command } from 'cmdk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { getUaHelper } from '@/utils';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PublishedResults } from './PublishedResults';
|
||||
type TransitionsModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@ -22,16 +23,13 @@ const isMac = () => {
|
||||
return getUaHelper().isMacOs;
|
||||
};
|
||||
export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
|
||||
const router = useRouter();
|
||||
const [query, setQuery] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isPublic, setIsPublic] = useState(false);
|
||||
const [publishWorkspaceName, setPublishWorkspaceName] = useState('');
|
||||
const [showCreatePage, setShowCreatePage] = useState(true);
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
const { currentWorkspaceId, workspacesMeta } = useAppState();
|
||||
|
||||
const currentWorkspace = workspacesMeta.find(
|
||||
meta => String(meta.id) === String(currentWorkspaceId)
|
||||
);
|
||||
const isPublic = currentWorkspace?.public;
|
||||
|
||||
// Add ‘⌘+K’ shortcut keys as switches
|
||||
useEffect(() => {
|
||||
@ -55,6 +53,14 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
|
||||
document.removeEventListener('keydown', down, { capture: true });
|
||||
}, [open, triggerQuickSearchModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.pathname.startsWith('/public-workspace')) {
|
||||
return setIsPublic(true);
|
||||
} else {
|
||||
return setIsPublic(false);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@ -85,28 +91,44 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
|
||||
}}
|
||||
>
|
||||
<StyledModalHeader>
|
||||
<Input query={query} setQuery={setQuery} setLoading={setLoading} />
|
||||
<Input
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
setLoading={setLoading}
|
||||
isPublic={isPublic}
|
||||
publishWorkspaceName={publishWorkspaceName}
|
||||
/>
|
||||
<StyledShortcut>{isMac() ? '⌘ + K' : 'Ctrl + K'}</StyledShortcut>
|
||||
</StyledModalHeader>
|
||||
<StyledModalDivider />
|
||||
<Command.List>
|
||||
<StyledContent>
|
||||
<Results
|
||||
query={query}
|
||||
loading={loading}
|
||||
setLoading={setLoading}
|
||||
setShowCreatePage={setShowCreatePage}
|
||||
/>
|
||||
{!isPublic ? (
|
||||
<Results
|
||||
query={query}
|
||||
loading={loading}
|
||||
setLoading={setLoading}
|
||||
setShowCreatePage={setShowCreatePage}
|
||||
/>
|
||||
) : (
|
||||
<PublishedResults
|
||||
query={query}
|
||||
loading={loading}
|
||||
setLoading={setLoading}
|
||||
onClose={onClose}
|
||||
setPublishWorkspaceName={setPublishWorkspaceName}
|
||||
/>
|
||||
)}
|
||||
</StyledContent>
|
||||
{isPublic ? (
|
||||
<></>
|
||||
) : showCreatePage ? (
|
||||
<>
|
||||
<StyledModalDivider />
|
||||
<StyledModalFooter>
|
||||
<Footer query={query} />
|
||||
</StyledModalFooter>
|
||||
</>
|
||||
{!isPublic ? (
|
||||
showCreatePage ? (
|
||||
<>
|
||||
<StyledModalDivider />
|
||||
<StyledModalFooter>
|
||||
<Footer query={query} />
|
||||
</StyledModalFooter>
|
||||
</>
|
||||
) : null
|
||||
) : null}
|
||||
</Command.List>
|
||||
</Command>
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
useWindowsKeyboardShortcuts,
|
||||
useWinMarkdownShortcuts,
|
||||
} from '@/components/shortcuts-modal/config';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import { MuiSlide } from '@/ui/mui';
|
||||
import { ModalCloseButton } from '@/ui/modal';
|
||||
import { getUaHelper } from '@/utils';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
@ -40,7 +40,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
|
||||
: windowsKeyboardShortcuts;
|
||||
|
||||
return createPortal(
|
||||
<Slide direction="left" in={open} mountOnEnter unmountOnExit>
|
||||
<MuiSlide direction="left" in={open} mountOnEnter unmountOnExit>
|
||||
<StyledShortcutsModal data-testid="shortcuts-modal">
|
||||
<>
|
||||
<StyledModalHeader>
|
||||
@ -81,7 +81,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
|
||||
})}
|
||||
</>
|
||||
</StyledShortcutsModal>
|
||||
</Slide>,
|
||||
</MuiSlide>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
59
packages/app/src/components/workspace-avatar/Avatar.tsx
Normal file
59
packages/app/src/components/workspace-avatar/Avatar.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { stringToColour } from '@/utils';
|
||||
interface IWorkspaceAvatar {
|
||||
size: number;
|
||||
name: string;
|
||||
avatar: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const WorkspaceAvatar = (props: IWorkspaceAvatar) => {
|
||||
const size = props.size || 20;
|
||||
const sizeStr = size + 'px';
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.avatar ? (
|
||||
<div
|
||||
style={{
|
||||
...props.style,
|
||||
width: sizeStr,
|
||||
height: sizeStr,
|
||||
border: '1px solid #fff',
|
||||
color: '#fff',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
<picture>
|
||||
<img
|
||||
style={{ width: sizeStr, height: sizeStr }}
|
||||
src={props.avatar}
|
||||
alt=""
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
...props.style,
|
||||
width: sizeStr,
|
||||
height: sizeStr,
|
||||
border: '1px solid #fff',
|
||||
color: '#fff',
|
||||
fontSize: Math.ceil(0.5 * size) + 'px',
|
||||
background: stringToColour(props.name || 'AFFiNE'),
|
||||
borderRadius: '50%',
|
||||
textAlign: 'center',
|
||||
lineHeight: size + 'px',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
{(props.name || 'AFFiNE').substring(0, 1)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
2
packages/app/src/components/workspace-avatar/index.ts
Normal file
2
packages/app/src/components/workspace-avatar/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { WorkspaceAvatar } from './Avatar';
|
||||
export { WorkspaceUnitAvatar } from './WorkspaceUnitAvatar';
|
58
packages/app/src/components/workspace-modal/Footer.tsx
Normal file
58
packages/app/src/components/workspace-modal/Footer.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { CloudInsyncIcon, LogOutIcon } from '@blocksuite/icons';
|
||||
import { FlexWrapper } from '@/ui/layout';
|
||||
import { WorkspaceAvatar } from '@/components/workspace-avatar';
|
||||
import { IconButton } from '@/ui/button';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { StyledFooter, StyleUserInfo, StyleSignIn } from './styles';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
|
||||
export const Footer = ({
|
||||
onLogin,
|
||||
onLogout,
|
||||
}: {
|
||||
onLogin: () => void;
|
||||
onLogout: () => void;
|
||||
}) => {
|
||||
const { user } = useAppState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<StyledFooter>
|
||||
{user && (
|
||||
<>
|
||||
<FlexWrapper>
|
||||
<WorkspaceAvatar
|
||||
size={40}
|
||||
name={user.name}
|
||||
avatar={user.avatar}
|
||||
></WorkspaceAvatar>
|
||||
<StyleUserInfo>
|
||||
<p>{user.name}</p>
|
||||
<p>{user.email}</p>
|
||||
</StyleUserInfo>
|
||||
</FlexWrapper>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onLogout();
|
||||
}}
|
||||
>
|
||||
<LogOutIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!user && (
|
||||
<StyleSignIn
|
||||
onClick={async () => {
|
||||
onLogin();
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<CloudInsyncIcon fontSize={16} />
|
||||
</span>
|
||||
{t('Sign in')}
|
||||
</StyleSignIn>
|
||||
)}
|
||||
</StyledFooter>
|
||||
);
|
||||
};
|
@ -0,0 +1,61 @@
|
||||
import { LOCALES } from '@affine/i18n';
|
||||
import { styled } from '@/styles';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { ArrowDownIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@/ui/button';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
|
||||
const LanguageMenuContent = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const changeLanguage = (event: string) => {
|
||||
i18n.changeLanguage(event);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{LOCALES.map(option => {
|
||||
return (
|
||||
<ListItem
|
||||
key={option.name}
|
||||
title={option.name}
|
||||
onClick={() => {
|
||||
changeLanguage(option.tag);
|
||||
}}
|
||||
>
|
||||
{option.originalName}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const LanguageMenu = () => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
content={<LanguageMenuContent />}
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
disablePortal={true}
|
||||
>
|
||||
<Button
|
||||
icon={<ArrowDownIcon />}
|
||||
iconPosition="end"
|
||||
noBorder={true}
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
{currentLanguage?.originalName}
|
||||
</Button>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const ListItem = styled(MenuItem)(({ theme }) => ({
|
||||
height: '38px',
|
||||
color: theme.colors.popoverColor,
|
||||
fontSize: theme.font.sm,
|
||||
textTransform: 'capitalize',
|
||||
padding: '0 24px',
|
||||
}));
|
@ -0,0 +1,72 @@
|
||||
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
|
||||
import {
|
||||
CloudIcon,
|
||||
LocalIcon,
|
||||
OfflineIcon,
|
||||
PublishedIcon,
|
||||
} from '@/components/workspace-modal/icons';
|
||||
import { UsersIcon } from '@blocksuite/icons';
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { StyleWorkspaceInfo, StyleWorkspaceTitle, StyledCard } from './styles';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { FlexWrapper } from '@/ui/layout';
|
||||
|
||||
export const WorkspaceCard = ({
|
||||
workspaceData,
|
||||
onClick,
|
||||
}: {
|
||||
workspaceData: WorkspaceUnit;
|
||||
onClick: (data: WorkspaceUnit) => void;
|
||||
}) => {
|
||||
const { currentWorkspace, isOwner } = useAppState();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StyledCard
|
||||
onClick={() => {
|
||||
onClick(workspaceData);
|
||||
}}
|
||||
active={workspaceData.id === currentWorkspace?.id}
|
||||
>
|
||||
<FlexWrapper>
|
||||
<WorkspaceUnitAvatar size={58} workspaceUnit={workspaceData} />
|
||||
</FlexWrapper>
|
||||
|
||||
<StyleWorkspaceInfo>
|
||||
<StyleWorkspaceTitle>
|
||||
{workspaceData.name || 'AFFiNE'}
|
||||
</StyleWorkspaceTitle>
|
||||
{isOwner ? (
|
||||
workspaceData.provider === 'local' ? (
|
||||
<p>
|
||||
<LocalIcon />
|
||||
{t('Local Workspace')}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
<CloudIcon />
|
||||
{t('Cloud Workspace')}
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<p>
|
||||
<UsersIcon fontSize={20} color={'#FF646B'} />
|
||||
{t('Joined Workspace')}
|
||||
</p>
|
||||
)}
|
||||
{workspaceData.provider === 'local' && (
|
||||
<p>
|
||||
<OfflineIcon />
|
||||
{t('Available Offline')}
|
||||
</p>
|
||||
)}
|
||||
{workspaceData.published && (
|
||||
<p>
|
||||
<PublishedIcon />
|
||||
{t('Published to Web')}
|
||||
</p>
|
||||
)}
|
||||
</StyleWorkspaceInfo>
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
99
packages/app/src/components/workspace-modal/icons/index.tsx
Normal file
99
packages/app/src/components/workspace-modal/icons/index.tsx
Normal 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"
|
||||
strokeWidth="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>
|
||||
);
|
||||
};
|
148
packages/app/src/components/workspace-modal/index.tsx
Normal file
148
packages/app/src/components/workspace-modal/index.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
||||
import { FlexWrapper } from '@/ui/layout';
|
||||
import { useState } from 'react';
|
||||
import { CreateWorkspaceModal } from '../create-workspace';
|
||||
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
|
||||
import { AddIcon, HelpCenterIcon } from '@blocksuite/icons';
|
||||
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { LanguageMenu } from './SelectLanguageMenu';
|
||||
|
||||
import { LoginModal } from '../login-modal';
|
||||
import { LogoutModal } from '../logout-modal';
|
||||
import {
|
||||
StyledCard,
|
||||
StyledSplitLine,
|
||||
StyleWorkspaceInfo,
|
||||
StyleWorkspaceTitle,
|
||||
StyledModalHeaderLeft,
|
||||
StyledModalTitle,
|
||||
StyledHelperContainer,
|
||||
StyledModalContent,
|
||||
StyledOperationWrapper,
|
||||
StyleWorkspaceAdd,
|
||||
StyledModalHeader,
|
||||
} from './styles';
|
||||
import { WorkspaceCard } from './WorkspaceCard';
|
||||
import { Footer } from './Footer';
|
||||
interface WorkspaceModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => {
|
||||
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
|
||||
const { workspaceList, logout } = useAppState();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [loginOpen, setLoginOpen] = useState(false);
|
||||
const [logoutOpen, setLogoutOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper
|
||||
width={720}
|
||||
height={690}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<StyledModalHeader>
|
||||
<StyledModalHeaderLeft>
|
||||
<StyledModalTitle>{t('My Workspaces')}</StyledModalTitle>
|
||||
<Tooltip
|
||||
content={t('Workspace description')}
|
||||
placement="top-start"
|
||||
disablePortal={true}
|
||||
>
|
||||
<StyledHelperContainer>
|
||||
<HelpCenterIcon />
|
||||
</StyledHelperContainer>
|
||||
</Tooltip>
|
||||
</StyledModalHeaderLeft>
|
||||
|
||||
<StyledOperationWrapper>
|
||||
<LanguageMenu />
|
||||
<StyledSplitLine />
|
||||
<ModalCloseButton
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
absolute={false}
|
||||
/>
|
||||
</StyledOperationWrapper>
|
||||
</StyledModalHeader>
|
||||
|
||||
<StyledModalContent>
|
||||
{workspaceList.map((item, index) => {
|
||||
return (
|
||||
<WorkspaceCard
|
||||
workspaceData={item}
|
||||
onClick={workspaceData => {
|
||||
router.replace(`/workspace/${workspaceData.id}`);
|
||||
onClose();
|
||||
}}
|
||||
key={index}
|
||||
></WorkspaceCard>
|
||||
);
|
||||
})}
|
||||
<StyledCard
|
||||
onClick={() => {
|
||||
setCreateWorkspaceOpen(true);
|
||||
}}
|
||||
>
|
||||
<FlexWrapper>
|
||||
<StyleWorkspaceAdd className="add-icon">
|
||||
<AddIcon fontSize={18} />
|
||||
</StyleWorkspaceAdd>
|
||||
</FlexWrapper>
|
||||
|
||||
<StyleWorkspaceInfo>
|
||||
<StyleWorkspaceTitle>{t('New Workspace')}</StyleWorkspaceTitle>
|
||||
<p>{t('Create Or Import')}</p>
|
||||
</StyleWorkspaceInfo>
|
||||
</StyledCard>
|
||||
</StyledModalContent>
|
||||
|
||||
<Footer
|
||||
onLogin={() => {
|
||||
setLoginOpen(true);
|
||||
}}
|
||||
onLogout={() => {
|
||||
setLogoutOpen(true);
|
||||
}}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
|
||||
<LoginModal
|
||||
open={loginOpen}
|
||||
onClose={() => {
|
||||
setLoginOpen(false);
|
||||
}}
|
||||
/>
|
||||
<LogoutModal
|
||||
open={logoutOpen}
|
||||
onClose={async wait => {
|
||||
if (!wait) {
|
||||
await logout();
|
||||
router.replace(`/workspace`);
|
||||
}
|
||||
setLogoutOpen(false);
|
||||
}}
|
||||
/>
|
||||
<CreateWorkspaceModal
|
||||
open={createWorkspaceOpen}
|
||||
onClose={() => {
|
||||
setCreateWorkspaceOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
162
packages/app/src/components/workspace-modal/styles.ts
Normal file
162
packages/app/src/components/workspace-modal/styles.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { displayFlex, styled } from '@/styles';
|
||||
|
||||
export const StyledSplitLine = styled.div(({ theme }) => {
|
||||
return {
|
||||
width: '1px',
|
||||
height: '20px',
|
||||
background: theme.colors.iconColor,
|
||||
marginRight: '24px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceInfo = styled.div(({ theme }) => {
|
||||
return {
|
||||
marginLeft: '15px',
|
||||
p: {
|
||||
height: '20px',
|
||||
fontSize: theme.font.xs,
|
||||
...displayFlex('flex-start', 'center'),
|
||||
},
|
||||
svg: {
|
||||
marginRight: '10px',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceTitle = styled.div(({ theme }) => {
|
||||
return {
|
||||
fontSize: theme.font.base,
|
||||
fontWeight: 600,
|
||||
lineHeight: '24px',
|
||||
marginBottom: '10px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCard = styled.div<{
|
||||
active?: boolean;
|
||||
}>(({ theme, active }) => {
|
||||
const borderColor = active ? theme.colors.primaryColor : 'transparent';
|
||||
return {
|
||||
width: '310px',
|
||||
height: '124px',
|
||||
cursor: 'pointer',
|
||||
padding: '16px',
|
||||
boxShadow: '0px 0px 8px rgba(0, 0, 0, 0.1)',
|
||||
borderRadius: '12px',
|
||||
border: `1px solid ${borderColor}`,
|
||||
...displayFlex('flex-start', 'flex-start'),
|
||||
marginBottom: '24px',
|
||||
':hover': {
|
||||
background: theme.colors.hoverBackground,
|
||||
'.add-icon': {
|
||||
border: `1.5px dashed ${theme.colors.primaryColor}`,
|
||||
svg: {
|
||||
fill: theme.colors.primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledFooter = styled('div')({
|
||||
height: '84px',
|
||||
padding: '0 40px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
});
|
||||
|
||||
export const StyleUserInfo = styled.div(({ theme }) => {
|
||||
return {
|
||||
textAlign: 'left',
|
||||
marginLeft: '16px',
|
||||
flex: 1,
|
||||
p: {
|
||||
lineHeight: '24px',
|
||||
color: theme.colors.iconColor,
|
||||
},
|
||||
'p:nth-child(1)': {
|
||||
color: theme.colors.textColor,
|
||||
fontWeight: 600,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeaderLeft = styled.div(() => {
|
||||
return { ...displayFlex('flex-start', 'center') };
|
||||
});
|
||||
export const StyledModalTitle = styled.div(({ theme }) => {
|
||||
return {
|
||||
fontWeight: 600,
|
||||
fontSize: theme.font.h6,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledHelperContainer = styled.div(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
marginLeft: '15px',
|
||||
fontWeight: 400,
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalContent = styled('div')({
|
||||
height: '534px',
|
||||
padding: '8px 40px',
|
||||
marginTop: '72px',
|
||||
overflow: 'auto',
|
||||
...displayFlex('space-between', 'flex-start', 'flex-start'),
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
export const StyledOperationWrapper = styled.div(() => {
|
||||
return {
|
||||
...displayFlex('flex-end', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceAdd = styled.div(() => {
|
||||
return {
|
||||
width: '58px',
|
||||
height: '58px',
|
||||
borderRadius: '100%',
|
||||
textAlign: 'center',
|
||||
background: '#f4f5fa',
|
||||
border: '1.5px dashed #f4f5fa',
|
||||
lineHeight: '58px',
|
||||
marginTop: '2px',
|
||||
};
|
||||
});
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '72px',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
background: theme.colors.pageBackground,
|
||||
borderRadius: '24px 24px 0 0',
|
||||
padding: '0 40px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
24
packages/app/src/components/workspace-setting/ExportPage.tsx
Normal file
24
packages/app/src/components/workspace-setting/ExportPage.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { styled } from '@/styles';
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
import { Trans } from '@affine/i18n';
|
||||
export const ExportPageTitleContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
|
||||
fontWeight: '500',
|
||||
flex: 1,
|
||||
};
|
||||
});
|
||||
export const ExportPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
|
||||
return (
|
||||
<ExportPageTitleContainer>
|
||||
<Trans i18nKey="Export Workspace">
|
||||
Export Workspace
|
||||
<code style={{ margin: '0 10px' }}>
|
||||
{{ workspace: workspace.name }}
|
||||
</code>
|
||||
Is Comming
|
||||
</Trans>
|
||||
</ExportPageTitleContainer>
|
||||
);
|
||||
};
|
114
packages/app/src/components/workspace-setting/PublishPage.tsx
Normal file
114
packages/app/src/components/workspace-setting/PublishPage.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import {
|
||||
StyledButtonContainer,
|
||||
StyledPublishContent,
|
||||
StyledPublishCopyContainer,
|
||||
StyledPublishExplanation,
|
||||
StyledSettingH2,
|
||||
StyledStopPublishContainer,
|
||||
} from './style';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/ui/button';
|
||||
import Input from '@/ui/input';
|
||||
import { toast } from '@/ui/toast';
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { EnableWorkspaceButton } from '../enable-workspace';
|
||||
export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
|
||||
const shareUrl = window.location.host + '/public-workspace/' + workspace.id;
|
||||
const { publishWorkspace } = useWorkspaceHelper();
|
||||
const { t } = useTranslation();
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const togglePublic = async (flag: boolean) => {
|
||||
try {
|
||||
await publishWorkspace(workspace.id.toString(), flag);
|
||||
setLoaded(false);
|
||||
} catch (e) {
|
||||
toast(t('Failed to publish workspace'));
|
||||
}
|
||||
};
|
||||
|
||||
const copyUrl = () => {
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
toast(t('Copied link to clipboard'));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspace.provider === 'affine' ? (
|
||||
<div
|
||||
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
<StyledPublishContent>
|
||||
{workspace.published ? (
|
||||
<>
|
||||
<StyledPublishExplanation>
|
||||
{t('Published Description')}
|
||||
</StyledPublishExplanation>
|
||||
|
||||
<StyledPublishCopyContainer>
|
||||
<StyledSettingH2 marginBottom={16}>
|
||||
{t('Share with link')}
|
||||
</StyledSettingH2>
|
||||
<Input width={500} value={shareUrl} disabled={true}></Input>
|
||||
<StyledButtonContainer>
|
||||
<Button onClick={copyUrl} type="primary" shape="circle">
|
||||
{t('Copy Link')}
|
||||
</Button>
|
||||
</StyledButtonContainer>
|
||||
</StyledPublishCopyContainer>
|
||||
</>
|
||||
) : (
|
||||
<StyledPublishExplanation>
|
||||
{t('Publishing Description')}
|
||||
<div style={{ marginTop: '64px' }}>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setLoaded(true);
|
||||
await togglePublic(true);
|
||||
}}
|
||||
loading={loaded}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
{t('Publish to web')}
|
||||
</Button>
|
||||
</div>
|
||||
</StyledPublishExplanation>
|
||||
)}
|
||||
</StyledPublishContent>
|
||||
|
||||
{workspace.published ? (
|
||||
<StyledStopPublishContainer>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setLoaded(true);
|
||||
await togglePublic(false);
|
||||
}}
|
||||
loading={false}
|
||||
type="danger"
|
||||
shape="circle"
|
||||
>
|
||||
{t('Stop publishing')}
|
||||
</Button>
|
||||
</StyledStopPublishContainer>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<StyledPublishContent>
|
||||
<>
|
||||
<StyledPublishExplanation>
|
||||
{t('Publishing')}
|
||||
</StyledPublishExplanation>
|
||||
|
||||
<div style={{ marginTop: '72px' }}>
|
||||
<EnableWorkspaceButton></EnableWorkspaceButton>
|
||||
</div>
|
||||
</>
|
||||
</StyledPublishContent>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
97
packages/app/src/components/workspace-setting/SyncPage.tsx
Normal file
97
packages/app/src/components/workspace-setting/SyncPage.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import {
|
||||
StyledButtonContainer,
|
||||
StyledPublishContent,
|
||||
StyledPublishExplanation,
|
||||
StyledWorkspaceName,
|
||||
StyledEmail,
|
||||
} from './style';
|
||||
import { DownloadIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@/ui/button';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
import { useTranslation, Trans } from '@affine/i18n';
|
||||
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
|
||||
import { EnableWorkspaceButton } from '../enable-workspace';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
export const SyncPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAppState();
|
||||
return (
|
||||
<div>
|
||||
<StyledPublishContent>
|
||||
{workspace.provider === 'local' ? (
|
||||
<>
|
||||
<StyledPublishExplanation>
|
||||
<WorkspaceUnitAvatar
|
||||
size={32}
|
||||
name={workspace.name}
|
||||
workspaceUnit={workspace}
|
||||
style={{ marginRight: '12px' }}
|
||||
/>
|
||||
<StyledWorkspaceName>{workspace.name} </StyledWorkspaceName>
|
||||
<span>{t('is a Local Workspace')}</span>
|
||||
</StyledPublishExplanation>
|
||||
<div>{t('Local Workspace Description')}</div>
|
||||
<StyledButtonContainer>
|
||||
<EnableWorkspaceButton></EnableWorkspaceButton>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<StyledPublishExplanation>
|
||||
<WorkspaceUnitAvatar
|
||||
size={32}
|
||||
name={workspace.name}
|
||||
workspaceUnit={workspace}
|
||||
style={{ marginRight: '12px' }}
|
||||
/>
|
||||
<StyledWorkspaceName>{workspace.name} </StyledWorkspaceName>
|
||||
<span>{t('is a Cloud Workspace')}</span>
|
||||
</StyledPublishExplanation>
|
||||
<div>
|
||||
<Trans i18nKey="Cloud Workspace Description">
|
||||
All data will be synchronised and saved to the AFFiNE account
|
||||
<StyledEmail>
|
||||
{{
|
||||
email: '{' + user?.email + '}.',
|
||||
}}
|
||||
</StyledEmail>
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<StyledButtonContainer>
|
||||
<Menu
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
// deleteMember(workspace.id, 0);
|
||||
}}
|
||||
icon={<DownloadIcon />}
|
||||
>
|
||||
{t('Download data', { CoreOrAll: t('core') })}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
// deleteMember(workspace.id, 0);
|
||||
}}
|
||||
icon={<DownloadIcon />}
|
||||
>
|
||||
{t('Download data', { CoreOrAll: t('all') })}
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
>
|
||||
<Button type="primary">
|
||||
{t('Download data', { CoreOrAll: '' })}
|
||||
</Button>
|
||||
</Menu>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
)}
|
||||
</StyledPublishContent>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,366 +0,0 @@
|
||||
import Modal, { ModalCloseButton } from '@/ui/modal';
|
||||
import {
|
||||
StyledCopyButtonContainer,
|
||||
StyledMemberAvatar,
|
||||
StyledMemberButtonContainer,
|
||||
StyledMemberEmail,
|
||||
StyledMemberInfo,
|
||||
StyledMemberListContainer,
|
||||
StyledMemberListItem,
|
||||
StyledMemberName,
|
||||
StyledMemberNameContainer,
|
||||
StyledMemberRoleContainer,
|
||||
StyledMemberTitleContainer,
|
||||
StyledMoreVerticalButton,
|
||||
StyledPublishContent,
|
||||
StyledPublishCopyContainer,
|
||||
StyledPublishExplanation,
|
||||
StyledSettingContainer,
|
||||
StyledSettingContent,
|
||||
StyledSettingH2,
|
||||
StyledSettingSidebar,
|
||||
StyledSettingSidebarHeader,
|
||||
StyledSettingTabContainer,
|
||||
StyledSettingTagIconContainer,
|
||||
WorkspaceSettingTagItem,
|
||||
} from './style';
|
||||
import {
|
||||
EditIcon,
|
||||
UsersIcon,
|
||||
PublishIcon,
|
||||
MoreVerticalIcon,
|
||||
EmailIcon,
|
||||
TrashIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, IconButton } from '@/ui/button';
|
||||
import Input from '@/ui/input';
|
||||
import { InviteMembers } from '../invite-members/index';
|
||||
import { Workspace, Member, getDataCenter } from '@affine/datacenter';
|
||||
import { Avatar } from '@mui/material';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { toast } from '@/ui/toast';
|
||||
import { Empty } from '@/ui/empty';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { WorkspaceDetails } from '../workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
|
||||
import { GeneralPage } from './general';
|
||||
|
||||
enum ActiveTab {
|
||||
'general' = 'general',
|
||||
'members' = 'members',
|
||||
'publish' = 'publish',
|
||||
}
|
||||
|
||||
type SettingTabProps = {
|
||||
activeTab: ActiveTab;
|
||||
onTabChange?: (tab: ActiveTab) => void;
|
||||
};
|
||||
|
||||
type WorkspaceSettingProps = {
|
||||
isShow: boolean;
|
||||
onClose?: () => void;
|
||||
workspace: Workspace;
|
||||
owner: WorkspaceDetails[string]['owner'];
|
||||
};
|
||||
|
||||
const WorkspaceSettingTab = ({ activeTab, onTabChange }: SettingTabProps) => {
|
||||
return (
|
||||
<StyledSettingTabContainer>
|
||||
<WorkspaceSettingTagItem
|
||||
isActive={activeTab === ActiveTab.general}
|
||||
onClick={() => {
|
||||
onTabChange && onTabChange(ActiveTab.general);
|
||||
}}
|
||||
>
|
||||
<StyledSettingTagIconContainer>
|
||||
<EditIcon />
|
||||
</StyledSettingTagIconContainer>
|
||||
General
|
||||
</WorkspaceSettingTagItem>
|
||||
<WorkspaceSettingTagItem
|
||||
isActive={activeTab === ActiveTab.members}
|
||||
onClick={() => {
|
||||
onTabChange && onTabChange(ActiveTab.members);
|
||||
}}
|
||||
>
|
||||
<StyledSettingTagIconContainer>
|
||||
<UsersIcon />
|
||||
</StyledSettingTagIconContainer>
|
||||
Members
|
||||
</WorkspaceSettingTagItem>
|
||||
<WorkspaceSettingTagItem
|
||||
isActive={activeTab === ActiveTab.publish}
|
||||
onClick={() => {
|
||||
onTabChange && onTabChange(ActiveTab.publish);
|
||||
}}
|
||||
>
|
||||
<StyledSettingTagIconContainer>
|
||||
<PublishIcon />
|
||||
</StyledSettingTagIconContainer>
|
||||
Publish
|
||||
</WorkspaceSettingTagItem>
|
||||
</StyledSettingTabContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceSetting = ({
|
||||
isShow,
|
||||
onClose,
|
||||
workspace,
|
||||
owner,
|
||||
}: WorkspaceSettingProps) => {
|
||||
const { user, workspaces } = useAppState();
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>(ActiveTab.general);
|
||||
const handleTabChange = (tab: ActiveTab) => {
|
||||
setActiveTab(tab);
|
||||
};
|
||||
const handleClickClose = () => {
|
||||
onClose && onClose();
|
||||
};
|
||||
const isOwner = user && owner.id === user.id;
|
||||
useEffect(() => {
|
||||
// reset tab when modal is closed
|
||||
if (!isShow) {
|
||||
setActiveTab(ActiveTab.general);
|
||||
}
|
||||
}, [isShow]);
|
||||
return (
|
||||
<Modal open={isShow}>
|
||||
<StyledSettingContainer>
|
||||
<ModalCloseButton onClick={handleClickClose} />
|
||||
{isOwner ? (
|
||||
<StyledSettingSidebar>
|
||||
<StyledSettingSidebarHeader>
|
||||
Workspace Settings
|
||||
</StyledSettingSidebarHeader>
|
||||
<WorkspaceSettingTab
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
</StyledSettingSidebar>
|
||||
) : null}
|
||||
<StyledSettingContent>
|
||||
{activeTab === ActiveTab.general && (
|
||||
<GeneralPage
|
||||
workspace={workspace}
|
||||
owner={owner}
|
||||
workspaces={workspaces}
|
||||
/>
|
||||
)}
|
||||
{activeTab === ActiveTab.members && workspace && (
|
||||
<MembersPage workspace={workspace} />
|
||||
)}
|
||||
{activeTab === ActiveTab.publish && (
|
||||
<PublishPage workspace={workspace} />
|
||||
)}
|
||||
</StyledSettingContent>
|
||||
</StyledSettingContainer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const MembersPage = ({ workspace }: { workspace: Workspace }) => {
|
||||
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const refreshMembers = useCallback(() => {
|
||||
getDataCenter()
|
||||
.then(dc =>
|
||||
dc.apis.getWorkspaceMembers({
|
||||
id: workspace.id,
|
||||
})
|
||||
)
|
||||
.then(data => {
|
||||
setMembers(data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
}, [workspace.id]);
|
||||
useEffect(() => {
|
||||
refreshMembers();
|
||||
}, [refreshMembers]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledMemberTitleContainer>
|
||||
<StyledMemberNameContainer>
|
||||
Users({members.length})
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>Access level</StyledMemberRoleContainer>
|
||||
</StyledMemberTitleContainer>
|
||||
<StyledMemberListContainer>
|
||||
{members.length === 0 && (
|
||||
<Empty width={648} sx={{ marginTop: '60px' }} height={300}></Empty>
|
||||
)}
|
||||
{members.length ? (
|
||||
members.map(member => {
|
||||
return (
|
||||
<StyledMemberListItem key={member.id}>
|
||||
<StyledMemberNameContainer>
|
||||
{member.user.type === 'Registered' ? (
|
||||
<Avatar src={member.user.avatar_url}></Avatar>
|
||||
) : (
|
||||
<StyledMemberAvatar alt="member avatar">
|
||||
<EmailIcon></EmailIcon>
|
||||
</StyledMemberAvatar>
|
||||
)}
|
||||
|
||||
<StyledMemberInfo>
|
||||
{member.user.type === 'Registered' ? (
|
||||
<StyledMemberName>{member.user.name}</StyledMemberName>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<StyledMemberEmail>{member.user.email}</StyledMemberEmail>
|
||||
</StyledMemberInfo>
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>
|
||||
{member.accepted
|
||||
? member.type !== 99
|
||||
? 'Member'
|
||||
: 'Workspace Owner'
|
||||
: 'Pending'}
|
||||
</StyledMemberRoleContainer>
|
||||
<StyledMoreVerticalButton>
|
||||
<Menu
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
// confirm({
|
||||
// title: 'Delete Member?',
|
||||
// content: `will delete member`,
|
||||
// confirmText: 'Delete',
|
||||
// confirmType: 'danger',
|
||||
// }).then(confirm => {
|
||||
getDataCenter()
|
||||
.then(dc =>
|
||||
dc.apis.removeMember({
|
||||
permissionId: member.id,
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
// console.log('data: ', data);
|
||||
toast('Moved to Trash');
|
||||
refreshMembers();
|
||||
});
|
||||
// });
|
||||
}}
|
||||
icon={<TrashIcon />}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
>
|
||||
<IconButton>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</StyledMoreVerticalButton>
|
||||
</StyledMemberListItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</StyledMemberListContainer>
|
||||
<StyledMemberButtonContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsInviteModalShow(true);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
Invite Members
|
||||
</Button>
|
||||
<InviteMembers
|
||||
onClose={() => {
|
||||
setIsInviteModalShow(false);
|
||||
}}
|
||||
onInviteSuccess={() => {
|
||||
refreshMembers();
|
||||
}}
|
||||
workspaceId={workspace.id}
|
||||
open={isInviteModalShow}
|
||||
></InviteMembers>
|
||||
</StyledMemberButtonContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PublishPage = ({ workspace }: { workspace: Workspace }) => {
|
||||
const shareUrl = window.location.host + '/workspace/' + workspace.id;
|
||||
const [publicStatus, setPublicStatus] = useState<boolean | null>(
|
||||
workspace.public
|
||||
);
|
||||
const togglePublic = (flag: boolean) => {
|
||||
getDataCenter()
|
||||
.then(dc =>
|
||||
dc.apis.updateWorkspace({
|
||||
id: workspace.id,
|
||||
public: flag,
|
||||
})
|
||||
)
|
||||
.then(data => {
|
||||
setPublicStatus(data?.public);
|
||||
toast('Updated Public Status Success');
|
||||
});
|
||||
};
|
||||
const copyUrl = () => {
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
toast('Copied url to clipboard');
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<StyledPublishContent>
|
||||
{publicStatus ? (
|
||||
<>
|
||||
<StyledPublishExplanation>
|
||||
The current workspace has been published to the web, everyone can
|
||||
view the contents of this workspace through the link.
|
||||
</StyledPublishExplanation>
|
||||
<StyledSettingH2 marginTop={48}>Share with link</StyledSettingH2>
|
||||
<StyledPublishCopyContainer>
|
||||
<Input width={500} value={shareUrl} disabled={true}></Input>
|
||||
<StyledCopyButtonContainer>
|
||||
<Button onClick={copyUrl} type="primary" shape="circle">
|
||||
Copy Link
|
||||
</Button>
|
||||
</StyledCopyButtonContainer>
|
||||
</StyledPublishCopyContainer>
|
||||
</>
|
||||
) : (
|
||||
<StyledPublishExplanation>
|
||||
After publishing to the web, everyone can view the content of this
|
||||
workspace through the link.
|
||||
</StyledPublishExplanation>
|
||||
)}
|
||||
</StyledPublishContent>
|
||||
{!publicStatus ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
togglePublic(true);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
Publish to web
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
togglePublic(false);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
Stop publishing
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,168 +1,169 @@
|
||||
import {
|
||||
StyledDeleteButtonContainer,
|
||||
StyledSettingAvatar,
|
||||
// StyledSettingAvatar,
|
||||
StyledSettingAvatarContent,
|
||||
StyledSettingInputContainer,
|
||||
StyledDoneButtonContainer,
|
||||
StyledInput,
|
||||
StyledProviderInfo,
|
||||
StyleGeneral,
|
||||
StyledAvatar,
|
||||
} from './style';
|
||||
import { StyledSettingH2 } from '../style';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/ui/button';
|
||||
import Input from '@/ui/input';
|
||||
import { getDataCenter, Workspace, WorkspaceType } from '@affine/datacenter';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { WorkspaceDetails } from '@/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
|
||||
import { WorkspaceDelete } from './delete';
|
||||
import { Workspace as StoreWorkspace } from '@blocksuite/store';
|
||||
import { debounce } from '@/utils';
|
||||
import { WorkspaceLeave } from './leave';
|
||||
import { DoneIcon, UsersIcon } from '@blocksuite/icons';
|
||||
// import { Upload } from '@/components/file-upload';
|
||||
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
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,
|
||||
owner,
|
||||
}: {
|
||||
workspace: Workspace;
|
||||
owner: WorkspaceDetails[string]['owner'];
|
||||
workspaces: Record<string, StoreWorkspace | null>;
|
||||
}) => {
|
||||
const {
|
||||
user,
|
||||
currentWorkspace,
|
||||
workspacesMeta,
|
||||
workspaces,
|
||||
refreshWorkspacesMeta,
|
||||
} = useAppState();
|
||||
export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
|
||||
const [showDelete, setShowDelete] = useState<boolean>(false);
|
||||
const [showLeave, setShowLeave] = useState<boolean>(false);
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
const [workspaceName, setWorkspaceName] = useState<string>(
|
||||
workspaces[workspace.id]?.meta.name ||
|
||||
(workspace.type === WorkspaceType.Private && user ? user.name : '')
|
||||
);
|
||||
const debouncedRefreshWorkspacesMeta = debounce(() => {
|
||||
refreshWorkspacesMeta();
|
||||
}, 100);
|
||||
const isOwner = user && owner.id === user.id;
|
||||
const [workspaceName, setWorkspaceName] = useState<string>(workspace.name);
|
||||
const { currentWorkspace, isOwner } = useAppState();
|
||||
const { updateWorkspace } = useWorkspaceHelper();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChangeWorkSpaceName = (newName: string) => {
|
||||
setWorkspaceName(newName);
|
||||
currentWorkspace?.meta.setName(newName);
|
||||
workspaces[workspace.id]?.meta.setName(newName);
|
||||
debouncedRefreshWorkspacesMeta();
|
||||
};
|
||||
const currentWorkspaceIndex = workspacesMeta.findIndex(
|
||||
meta => meta.id === workspace.id
|
||||
);
|
||||
const nextWorkSpaceId =
|
||||
currentWorkspaceIndex === workspacesMeta.length - 1
|
||||
? workspacesMeta[currentWorkspaceIndex - 1]?.id
|
||||
: workspacesMeta[currentWorkspaceIndex + 1]?.id;
|
||||
const handleClickDelete = () => {
|
||||
setShowDelete(true);
|
||||
};
|
||||
const handleCloseDelete = () => {
|
||||
setShowDelete(false);
|
||||
};
|
||||
|
||||
const handleClickLeave = () => {
|
||||
setShowLeave(true);
|
||||
};
|
||||
const handleCloseLeave = () => {
|
||||
setShowLeave(false);
|
||||
const handleUpdateWorkspaceName = () => {
|
||||
currentWorkspace &&
|
||||
updateWorkspace({ name: workspaceName }, currentWorkspace);
|
||||
};
|
||||
|
||||
const fileChange = async (file: File) => {
|
||||
setUploading(true);
|
||||
const blob = new Blob([file], { type: file.type });
|
||||
const blobId = await getDataCenter()
|
||||
.then(dc => dc.apis.uploadBlob({ blob }))
|
||||
.finally(() => {
|
||||
setUploading(false);
|
||||
});
|
||||
if (blobId) {
|
||||
currentWorkspace?.meta.setAvatar(blobId);
|
||||
workspaces[workspace.id]?.meta.setAvatar(blobId);
|
||||
setUploading(false);
|
||||
debouncedRefreshWorkspacesMeta();
|
||||
}
|
||||
currentWorkspace &&
|
||||
(await updateWorkspace({ avatarBlob: blob }, currentWorkspace));
|
||||
};
|
||||
|
||||
return workspace ? (
|
||||
<div>
|
||||
<StyledSettingH2 marginTop={56}>Workspace Avatar</StyledSettingH2>
|
||||
<StyledSettingAvatarContent>
|
||||
<StyledSettingAvatar
|
||||
alt="workspace avatar"
|
||||
src={
|
||||
workspaces[workspace.id]?.meta.avatar
|
||||
? '/api/blob/' + workspaces[workspace.id]?.meta.avatar
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{workspaces[workspace.id]?.meta.name[0]}
|
||||
</StyledSettingAvatar>
|
||||
<Upload
|
||||
<StyleGeneral>
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
<StyledSettingH2>{t('Workspace Avatar')}</StyledSettingH2>
|
||||
<StyledSettingAvatarContent>
|
||||
<StyledAvatar>
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={fileChange}
|
||||
>
|
||||
<>
|
||||
<div className="camera-icon">
|
||||
<CameraIcon></CameraIcon>
|
||||
</div>
|
||||
<WorkspaceUnitAvatar
|
||||
size={60}
|
||||
name={workspace.name}
|
||||
workspaceUnit={workspace}
|
||||
/>
|
||||
</>
|
||||
</Upload>
|
||||
</StyledAvatar>
|
||||
{/* TODO: Wait for image sync to complete */}
|
||||
{/* <Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={fileChange}
|
||||
>
|
||||
<Button loading={uploading}>Upload</Button>
|
||||
</Upload>
|
||||
{/* TODO: add upload logic */}
|
||||
{/* {isOwner ? (
|
||||
<StyledAvatarUploadBtn shape="round">upload</StyledAvatarUploadBtn>
|
||||
) : null} */}
|
||||
{/* <Button shape="round">remove</Button> */}
|
||||
</StyledSettingAvatarContent>
|
||||
<StyledSettingH2 marginTop={36}>Workspace Name</StyledSettingH2>
|
||||
<StyledSettingInputContainer>
|
||||
<Input
|
||||
width={327}
|
||||
value={workspaceName}
|
||||
placeholder="Workspace Name"
|
||||
disabled={!isOwner}
|
||||
maxLength={14}
|
||||
minLength={1}
|
||||
onChange={handleChangeWorkSpaceName}
|
||||
></Input>
|
||||
</StyledSettingInputContainer>
|
||||
<StyledSettingH2 marginTop={36}>Workspace Owner</StyledSettingH2>
|
||||
<StyledSettingInputContainer>
|
||||
<Input
|
||||
width={327}
|
||||
disabled
|
||||
value={owner.name}
|
||||
placeholder="Workspace Owner"
|
||||
></Input>
|
||||
</StyledSettingInputContainer>
|
||||
<Button loading={uploading}>{t('Upload')}</Button>
|
||||
</Upload> */}
|
||||
{/* TODO: add upload logic */}
|
||||
</StyledSettingAvatarContent>
|
||||
<StyledSettingH2 marginTop={20}>{t('Workspace Name')}</StyledSettingH2>
|
||||
<StyledSettingInputContainer>
|
||||
<StyledInput
|
||||
width={284}
|
||||
height={32}
|
||||
value={workspaceName}
|
||||
placeholder={t('Workspace Name')}
|
||||
maxLength={14}
|
||||
minLength={1}
|
||||
disabled={!isOwner}
|
||||
onChange={handleChangeWorkSpaceName}
|
||||
></StyledInput>
|
||||
{isOwner ? (
|
||||
<StyledDoneButtonContainer
|
||||
onClick={() => {
|
||||
handleUpdateWorkspaceName();
|
||||
}}
|
||||
>
|
||||
<DoneIcon />
|
||||
</StyledDoneButtonContainer>
|
||||
) : null}
|
||||
</StyledSettingInputContainer>
|
||||
<StyledSettingH2 marginTop={20}>{t('Workspace Type')}</StyledSettingH2>
|
||||
<StyledSettingInputContainer>
|
||||
{isOwner ? (
|
||||
currentWorkspace?.provider === 'local' ? (
|
||||
<StyledProviderInfo>
|
||||
<LocalIcon />
|
||||
{t('Local Workspace')}
|
||||
</StyledProviderInfo>
|
||||
) : (
|
||||
<StyledProviderInfo>
|
||||
<CloudIcon />
|
||||
{t('Available Offline')}
|
||||
</StyledProviderInfo>
|
||||
)
|
||||
) : (
|
||||
<StyledProviderInfo>
|
||||
<UsersIcon fontSize={20} color={'#FF646B'} />
|
||||
{t('Joined Workspace')}
|
||||
</StyledProviderInfo>
|
||||
)}
|
||||
</StyledSettingInputContainer>
|
||||
</div>
|
||||
|
||||
<StyledDeleteButtonContainer>
|
||||
{isOwner ? (
|
||||
<>
|
||||
<Button type="danger" shape="circle" onClick={handleClickDelete}>
|
||||
Delete Workspace
|
||||
<Button
|
||||
type="danger"
|
||||
shape="circle"
|
||||
style={{ borderRadius: '40px' }}
|
||||
onClick={() => {
|
||||
setShowDelete(true);
|
||||
}}
|
||||
>
|
||||
{t('Delete Workspace')}
|
||||
</Button>
|
||||
<WorkspaceDelete
|
||||
open={showDelete}
|
||||
onClose={handleCloseDelete}
|
||||
workspaceName={workspaceName}
|
||||
workspaceId={workspace.id}
|
||||
nextWorkSpaceId={nextWorkSpaceId}
|
||||
onClose={() => {
|
||||
setShowDelete(false);
|
||||
}}
|
||||
workspace={workspace}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button type="danger" shape="circle" onClick={handleClickLeave}>
|
||||
Leave Workspace
|
||||
<Button
|
||||
type="danger"
|
||||
shape="circle"
|
||||
onClick={() => {
|
||||
setShowLeave(true);
|
||||
}}
|
||||
>
|
||||
{t('Leave Workspace')}
|
||||
</Button>
|
||||
<WorkspaceLeave
|
||||
open={showLeave}
|
||||
onClose={handleCloseLeave}
|
||||
workspaceName={workspaceName}
|
||||
workspaceId={workspace.id}
|
||||
nextWorkSpaceId={nextWorkSpaceId}
|
||||
onClose={() => {
|
||||
setShowLeave(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StyledDeleteButtonContainer>
|
||||
</div>
|
||||
</StyleGeneral>
|
||||
) : null;
|
||||
};
|
||||
|
@ -11,61 +11,77 @@ import {
|
||||
import { useState } from 'react';
|
||||
import { ModalCloseButton } from '@/ui/modal';
|
||||
import { Button } from '@/ui/button';
|
||||
import { getDataCenter } from '@affine/datacenter';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
import { Trans, useTranslation } from '@affine/i18n';
|
||||
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
|
||||
|
||||
interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspaceName: string;
|
||||
workspaceId: string;
|
||||
nextWorkSpaceId: string;
|
||||
workspace: WorkspaceUnit;
|
||||
}
|
||||
|
||||
export const WorkspaceDelete = ({
|
||||
open,
|
||||
onClose,
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
nextWorkSpaceId,
|
||||
workspace,
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const [deleteStr, setDeleteStr] = useState<string>('');
|
||||
const { refreshWorkspacesMeta } = useAppState();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const { deleteWorkSpace } = useWorkspaceHelper();
|
||||
const handlerInputChange = (workspaceName: string) => {
|
||||
setDeleteStr(workspaceName);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
const dc = await getDataCenter();
|
||||
await dc.apis.deleteWorkspace({ id: workspaceId });
|
||||
router.push(`/workspace/${nextWorkSpaceId}`);
|
||||
refreshWorkspacesMeta();
|
||||
await deleteWorkSpace();
|
||||
onClose();
|
||||
router.push(`/workspace`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledModalHeader>Delete Workspace</StyledModalHeader>
|
||||
<StyledTextContent>
|
||||
This action cannot be undone. This will permanently delete (
|
||||
<StyledWorkspaceName>{workspaceName}</StyledWorkspaceName>) along with
|
||||
all its content.
|
||||
</StyledTextContent>
|
||||
<StyledModalHeader>{t('Delete Workspace')}?</StyledModalHeader>
|
||||
{workspace.provider === 'local' ? (
|
||||
<StyledTextContent>
|
||||
<Trans i18nKey="Delete Workspace Description">
|
||||
Deleting (
|
||||
<StyledWorkspaceName>
|
||||
{{ workspace: workspace.name }}
|
||||
</StyledWorkspaceName>
|
||||
) cannot be undone, please proceed with caution. along with all
|
||||
its content.
|
||||
</Trans>
|
||||
</StyledTextContent>
|
||||
) : (
|
||||
<StyledTextContent>
|
||||
<Trans i18nKey="Delete Workspace Description2">
|
||||
Deleting (
|
||||
<StyledWorkspaceName>
|
||||
{{ workspace: workspace.name }}
|
||||
</StyledWorkspaceName>
|
||||
) will delete both local and cloud data, this operation cannot be
|
||||
undone, please proceed with caution.
|
||||
</Trans>
|
||||
</StyledTextContent>
|
||||
)}
|
||||
<StyledInputContent>
|
||||
<Input
|
||||
onChange={handlerInputChange}
|
||||
placeholder="Please type “Delete” to confirm"
|
||||
placeholder={t('Delete Workspace placeholder')}
|
||||
value={deleteStr}
|
||||
width={284}
|
||||
height={42}
|
||||
></Input>
|
||||
</StyledInputContent>
|
||||
<StyledButtonContent>
|
||||
<Button shape="circle" onClick={onClose}>
|
||||
Cancel
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={deleteStr.toLowerCase() !== 'delete'}
|
||||
@ -74,7 +90,7 @@ export const WorkspaceDelete = ({
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
Delete
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
|
@ -1 +1 @@
|
||||
export * from './Delete';
|
||||
export { WorkspaceDelete } from './Delete';
|
||||
|
@ -4,16 +4,17 @@ export const StyledModalWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '460px',
|
||||
width: '560px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
height: '312px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '460px',
|
||||
width: '560px',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px;',
|
||||
textAlign: 'center',
|
||||
@ -32,25 +33,29 @@ export const StyledTextContent = styled('div')(() => {
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
textAlign: 'left',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledInputContent = styled('div')(() => {
|
||||
export const StyledInputContent = styled('div')(({ theme }) => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '40px 0 24px 0',
|
||||
margin: '32px 0',
|
||||
fontSize: theme.font.base,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
bottom: '32px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '0px 0 32px 0',
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -1 +1 @@
|
||||
export * from './General';
|
||||
export { GeneralPage } from './General';
|
||||
|
@ -7,31 +7,21 @@ import {
|
||||
} from './style';
|
||||
import { ModalCloseButton } from '@/ui/modal';
|
||||
import { Button } from '@/ui/button';
|
||||
import { getDataCenter } from '@affine/datacenter';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
|
||||
// import { getDataCenter } from '@affine/datacenter';
|
||||
// import { useAppState } from '@/providers/app-state-provider';
|
||||
|
||||
interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspaceName: string;
|
||||
workspaceId: string;
|
||||
nextWorkSpaceId: string;
|
||||
}
|
||||
|
||||
export const WorkspaceLeave = ({
|
||||
open,
|
||||
onClose,
|
||||
nextWorkSpaceId,
|
||||
workspaceId,
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const router = useRouter();
|
||||
const { refreshWorkspacesMeta } = useAppState();
|
||||
export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
|
||||
const { leaveWorkSpace } = useWorkspaceHelper();
|
||||
const { t } = useTranslation();
|
||||
const handleLeave = async () => {
|
||||
const dc = await getDataCenter();
|
||||
await dc.apis.leaveWorkspace({ id: workspaceId });
|
||||
router.push(`/workspace/${nextWorkSpaceId}`);
|
||||
refreshWorkspacesMeta();
|
||||
await leaveWorkSpace();
|
||||
onClose();
|
||||
};
|
||||
|
||||
@ -39,14 +29,13 @@ export const WorkspaceLeave = ({
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledModalHeader>Leave Workspace</StyledModalHeader>
|
||||
<StyledModalHeader>{t('Leave Workspace')}</StyledModalHeader>
|
||||
<StyledTextContent>
|
||||
After you leave, you will not be able to access all the contents of
|
||||
this workspace.
|
||||
{t('Leave Workspace Description')}
|
||||
</StyledTextContent>
|
||||
<StyledButtonContent>
|
||||
<Button shape="circle" onClick={onClose}>
|
||||
Cancel
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleLeave}
|
||||
@ -54,7 +43,7 @@ export const WorkspaceLeave = ({
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
Leave
|
||||
{t('Leave')}
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
|
@ -1 +1 @@
|
||||
export * from './Leave';
|
||||
export { WorkspaceLeave } from './Leave';
|
||||
|
@ -1,15 +1,45 @@
|
||||
import { styled } from '@/styles';
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
import { displayFlex, styled } from '@/styles';
|
||||
import { MuiAvatar } from '@/ui/mui';
|
||||
import IconButton from '@/ui/button/IconButton';
|
||||
import Input from '@/ui/input';
|
||||
|
||||
export const StyledSettingInputContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '12px',
|
||||
width: '100%',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledDeleteButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '154px',
|
||||
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)(() => {
|
||||
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',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -1 +1,5 @@
|
||||
export * from './WorkspaceSetting';
|
||||
export * from './general';
|
||||
export * from './ExportPage';
|
||||
export * from './member';
|
||||
export * from './SyncPage';
|
||||
export * from './PublishPage';
|
||||
|
@ -4,8 +4,10 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
||||
import { Button } from '@/ui/button';
|
||||
import Input from '@/ui/input';
|
||||
import { useState } from 'react';
|
||||
import { getDataCenter } from '@affine/datacenter';
|
||||
import { Avatar } from '@mui/material';
|
||||
import { MuiAvatar } from '@/ui/mui';
|
||||
import useMembers from '@/hooks/use-members';
|
||||
import { User } from '@affine/datacenter';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
interface LoginModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@ -42,41 +44,28 @@ export const debounce = <T extends (...args: any) => any>(
|
||||
};
|
||||
|
||||
const gmailReg = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@gmail\.com$/;
|
||||
export const InviteMembers = ({
|
||||
export const InviteMemberModal = ({
|
||||
open,
|
||||
onClose,
|
||||
workspaceId,
|
||||
onInviteSuccess,
|
||||
}: LoginModalProps) => {
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [showMember, setShowMember] = useState<boolean>(false);
|
||||
const [showTip, setShowTip] = useState<boolean>(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [userData, setUserData] = useState<any>({});
|
||||
const [userData, setUserData] = useState<User | null>(null);
|
||||
const { inviteMember, getUserByEmail } = useMembers();
|
||||
const { t } = useTranslation();
|
||||
const inputChange = (value: string) => {
|
||||
setEmail(value);
|
||||
setShowMember(true);
|
||||
if (gmailReg.test(value)) {
|
||||
setEmail(value);
|
||||
setShowTip(false);
|
||||
debounce(
|
||||
() => {
|
||||
getDataCenter()
|
||||
.then(dc =>
|
||||
dc.apis.getUserByEmail({
|
||||
email: value,
|
||||
workspace_id: workspaceId,
|
||||
})
|
||||
)
|
||||
.then(data => {
|
||||
if (data?.name) {
|
||||
setUserData(data);
|
||||
setShowTip(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
300,
|
||||
true
|
||||
)();
|
||||
getUserByEmail(value).then(data => {
|
||||
if (data?.name) {
|
||||
setUserData(data);
|
||||
setShowTip(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setShowTip(true);
|
||||
}
|
||||
@ -87,15 +76,14 @@ export const InviteMembers = ({
|
||||
<ModalWrapper width={460} height={236}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setEmail('');
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>Invite members</ContentTitle>
|
||||
<ContentTitle>{t('Invite Members')}</ContentTitle>
|
||||
<InviteBox>
|
||||
<Input
|
||||
width={360}
|
||||
@ -104,16 +92,16 @@ export const InviteMembers = ({
|
||||
onBlur={() => {
|
||||
setShowMember(false);
|
||||
}}
|
||||
placeholder="Search mail (Gmail support only)"
|
||||
placeholder={t('Invite placeholder')}
|
||||
></Input>
|
||||
{showMember ? (
|
||||
<Members>
|
||||
{showTip ? (
|
||||
<NoFind>Non-Gmail is not supported</NoFind>
|
||||
<NoFind>{t('Non-Gmail')}</NoFind>
|
||||
) : (
|
||||
<Member>
|
||||
{userData?.avatar_url ? (
|
||||
<Avatar src={userData?.avatar_url}></Avatar>
|
||||
{userData?.avatar ? (
|
||||
<MuiAvatar src={userData?.avatar}></MuiAvatar>
|
||||
) : (
|
||||
<MemberIcon>
|
||||
<EmailIcon></EmailIcon>
|
||||
@ -133,20 +121,14 @@ export const InviteMembers = ({
|
||||
<Button
|
||||
shape="circle"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
getDataCenter()
|
||||
.then(dc => dc.apis.inviteMember({ id: workspaceId, email }))
|
||||
.then(() => {
|
||||
onClose();
|
||||
onInviteSuccess && onInviteSuccess();
|
||||
})
|
||||
.catch(err => {
|
||||
// toast('Invite failed');
|
||||
console.log(err);
|
||||
});
|
||||
style={{ width: '364px', height: '38px', borderRadius: '40px' }}
|
||||
onClick={async () => {
|
||||
await inviteMember(email);
|
||||
setEmail('');
|
||||
onInviteSuccess();
|
||||
}}
|
||||
>
|
||||
Invite
|
||||
{t('Invite')}
|
||||
</Button>
|
||||
</Footer>
|
||||
</ModalWrapper>
|
||||
@ -162,10 +144,8 @@ const Header = styled('div')({
|
||||
|
||||
const Content = styled('div')({
|
||||
display: 'flex',
|
||||
padding: '0 48px',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
});
|
||||
|
||||
const ContentTitle = styled('h1')({
|
||||
@ -177,9 +157,8 @@ const ContentTitle = styled('h1')({
|
||||
});
|
||||
|
||||
const Footer = styled('div')({
|
||||
height: '70px',
|
||||
paddingLeft: '24px',
|
||||
marginTop: '32px',
|
||||
height: '102px',
|
||||
margin: '32px 0',
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
@ -0,0 +1,173 @@
|
||||
import {
|
||||
StyledMemberAvatar,
|
||||
StyledMemberButtonContainer,
|
||||
StyledMemberEmail,
|
||||
StyledMemberInfo,
|
||||
StyledMemberListContainer,
|
||||
StyledMemberListItem,
|
||||
StyledMemberName,
|
||||
StyledMemberNameContainer,
|
||||
StyledMemberRoleContainer,
|
||||
StyledMemberTitleContainer,
|
||||
StyledMoreVerticalButton,
|
||||
StyledPublishExplanation,
|
||||
StyledMemberWarp,
|
||||
StyledMemberContainer,
|
||||
} from './style';
|
||||
import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons';
|
||||
import { useState } from 'react';
|
||||
import { Button, IconButton } from '@/ui/button';
|
||||
import { InviteMemberModal } from './InviteMemberModal';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { Empty } from '@/ui/empty';
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
import { useConfirm } from '@/providers/ConfirmProvider';
|
||||
import { toast } from '@/ui/toast';
|
||||
import useMembers from '@/hooks/use-members';
|
||||
import Loading from '@/components/loading';
|
||||
import { FlexWrapper } from '@/ui/layout';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { EnableWorkspaceButton } from '@/components/enable-workspace';
|
||||
|
||||
export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
|
||||
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
||||
const { members, removeMember, loaded } = useMembers();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
if (workspace.provider === 'affine') {
|
||||
return (
|
||||
<StyledMemberContainer>
|
||||
<StyledMemberListContainer>
|
||||
{!loaded && (
|
||||
<FlexWrapper justifyContent="center">
|
||||
<Loading size={25} />
|
||||
</FlexWrapper>
|
||||
)}
|
||||
{loaded && members.length === 0 && (
|
||||
<Empty width={648} sx={{ marginTop: '60px' }} height={300} />
|
||||
)}
|
||||
{loaded && members.length > 0 && (
|
||||
<>
|
||||
<StyledMemberTitleContainer>
|
||||
<StyledMemberNameContainer>
|
||||
{t('Users')} ({members.length})
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>
|
||||
{t('Access level')}
|
||||
</StyledMemberRoleContainer>
|
||||
<div style={{ width: '24px', paddingRight: '48px' }}></div>
|
||||
</StyledMemberTitleContainer>
|
||||
{members.map((member, index) => {
|
||||
const user = Object.assign(
|
||||
{
|
||||
avatar_url: '',
|
||||
email: '',
|
||||
id: '',
|
||||
name: '',
|
||||
},
|
||||
member.user
|
||||
);
|
||||
return (
|
||||
<StyledMemberListItem key={index}>
|
||||
<StyledMemberNameContainer>
|
||||
<StyledMemberAvatar
|
||||
alt="member avatar"
|
||||
src={user.avatar_url}
|
||||
>
|
||||
<EmailIcon />
|
||||
</StyledMemberAvatar>
|
||||
|
||||
<StyledMemberInfo>
|
||||
<StyledMemberName>{user.name}</StyledMemberName>
|
||||
<StyledMemberEmail>
|
||||
{member.user.email}
|
||||
</StyledMemberEmail>
|
||||
</StyledMemberInfo>
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>
|
||||
{member.accepted
|
||||
? member.type !== 99
|
||||
? t('Member')
|
||||
: t('Owner')
|
||||
: t('Pending')}
|
||||
</StyledMemberRoleContainer>
|
||||
<StyledMoreVerticalButton>
|
||||
<Menu
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
const confirmRemove = await confirm({
|
||||
title: t('Delete Member?'),
|
||||
content: t('will delete member'),
|
||||
confirmText: t('Delete'),
|
||||
confirmType: 'danger',
|
||||
});
|
||||
|
||||
if (!confirmRemove) {
|
||||
return;
|
||||
}
|
||||
await removeMember(member.id);
|
||||
toast(
|
||||
t('Member has been removed', {
|
||||
name: user.name,
|
||||
})
|
||||
);
|
||||
}}
|
||||
icon={<TrashIcon />}
|
||||
>
|
||||
{t('Delete')}
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
>
|
||||
<IconButton>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</StyledMoreVerticalButton>
|
||||
</StyledMemberListItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</StyledMemberListContainer>
|
||||
<StyledMemberButtonContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsInviteModalShow(true);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
{t('Invite Members')}
|
||||
</Button>
|
||||
<InviteMemberModal
|
||||
onClose={() => {
|
||||
setIsInviteModalShow(false);
|
||||
}}
|
||||
onInviteSuccess={() => {
|
||||
setIsInviteModalShow(false);
|
||||
// refreshMembers();
|
||||
}}
|
||||
workspaceId={workspace.id}
|
||||
open={isInviteModalShow}
|
||||
></InviteMemberModal>
|
||||
</StyledMemberButtonContainer>
|
||||
</StyledMemberContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledMemberWarp>
|
||||
{t('Collaboration Description')}
|
||||
<StyledPublishExplanation>
|
||||
<EnableWorkspaceButton></EnableWorkspaceButton>
|
||||
</StyledPublishExplanation>
|
||||
</StyledMemberWarp>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './MembersPage';
|
120
packages/app/src/components/workspace-setting/member/style.ts
Normal file
120
packages/app/src/components/workspace-setting/member/style.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { styled } from '@/styles';
|
||||
import { MuiAvatar } from '@/ui/mui';
|
||||
|
||||
export const StyledMemberTitleContainer = styled('li')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
fontWeight: '500',
|
||||
marginBottom: '32px',
|
||||
flex: 1,
|
||||
};
|
||||
});
|
||||
export const StyledMemberContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberAvatar = styled(MuiAvatar)(() => {
|
||||
return { height: '40px', width: '40px' };
|
||||
});
|
||||
|
||||
export const StyledMemberNameContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: '2 0 402px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberRoleContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: '1 0 222px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListContainer = styled('ul')(() => {
|
||||
return {
|
||||
overflowY: 'scroll',
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListItem = styled('li')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '72px',
|
||||
width: '100%',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberInfo = styled('div')(() => {
|
||||
return {
|
||||
paddingLeft: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberName = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
color: theme.colors.textColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberEmail = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
color: theme.colors.iconColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
marginBottom: '20px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMoreVerticalButton = styled('button')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
paddingRight: '48px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishExplanation = styled('div')(() => {
|
||||
return {
|
||||
paddingRight: '48px',
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
flex: 1,
|
||||
marginTop: '64px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberWarp = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '0 0 48px 0',
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
};
|
||||
});
|
@ -1,35 +1,29 @@
|
||||
import { styled } from '@/styles';
|
||||
import { Button } from '@/ui/button';
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
|
||||
export const StyledSettingContainer = styled('div')(({ theme }) => {
|
||||
export const StyledSettingContainer = styled('div')(() => {
|
||||
return {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
padding: '0px',
|
||||
width: '961px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'column',
|
||||
|
||||
padding: '0 34px 20px 48px',
|
||||
height: '100vh',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingSidebar = styled('div')(({ theme }) => {
|
||||
export const StyledSettingSidebar = styled('div')(() => {
|
||||
{
|
||||
return {
|
||||
width: '212px',
|
||||
height: '620px',
|
||||
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
// height: '48px',
|
||||
marginTop: '50px',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const StyledSettingContent = styled('div')(() => {
|
||||
return {
|
||||
paddingLeft: '48px',
|
||||
height: '620px',
|
||||
overflow: 'hidden',
|
||||
flex: 1,
|
||||
paddingTop: '48px',
|
||||
};
|
||||
});
|
||||
|
||||
@ -37,7 +31,6 @@ export const StyledSetting = styled('div')(({ theme }) => {
|
||||
{
|
||||
return {
|
||||
width: '236px',
|
||||
height: '620px',
|
||||
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
|
||||
};
|
||||
}
|
||||
@ -49,7 +42,7 @@ export const StyledSettingSidebarHeader = styled('div')(() => {
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
textAlign: 'left',
|
||||
marginTop: '37px',
|
||||
};
|
||||
}
|
||||
@ -59,8 +52,6 @@ export const StyledSettingTabContainer = styled('ul')(() => {
|
||||
{
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '25px',
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -70,14 +61,18 @@ export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
|
||||
{
|
||||
return {
|
||||
display: 'flex',
|
||||
marginBottom: '12px',
|
||||
padding: '0 24px',
|
||||
height: '32px',
|
||||
margin: '0 48px 0 0',
|
||||
height: '34px',
|
||||
color: isActive ? theme.colors.primaryColor : theme.colors.textColor,
|
||||
fontWeight: '400',
|
||||
fontSize: '16px',
|
||||
lineHeight: '32px',
|
||||
fontWeight: '500',
|
||||
fontSize: theme.font.base,
|
||||
lineHeight: theme.font.lineHeightBase,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
borderBottom: `2px solid ${
|
||||
isActive ? theme.colors.primaryColor : 'none'
|
||||
}`,
|
||||
':hover': { color: theme.colors.primaryColor },
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -87,144 +82,72 @@ export const StyledSettingTagIconContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginRight: '14.64px',
|
||||
width: '14.47px',
|
||||
fontSize: '14.47px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingH2 = styled('h2')<{ marginTop?: number }>(
|
||||
({ marginTop }) => {
|
||||
return {
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
marginTop: marginTop ? `${marginTop}px` : '0px',
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledAvatarUploadBtn = styled(Button)(({ theme }) => {
|
||||
export const StyledSettingH2 = styled('h2')<{
|
||||
marginTop?: number;
|
||||
marginBottom?: number;
|
||||
}>(({ marginTop, marginBottom, theme }) => {
|
||||
return {
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
color: theme.colors.primaryColor,
|
||||
margin: '0 12px 0 24px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberTitleContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
marginTop: '60px',
|
||||
fontWeight: '500',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberAvatar = styled(MuiAvatar)(() => {
|
||||
return { height: '40px', width: '40px' };
|
||||
});
|
||||
|
||||
export const StyledMemberNameContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '402px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberRoleContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '222px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListContainer = styled('ul')(() => {
|
||||
return {
|
||||
marginTop: '15px',
|
||||
height: '432px',
|
||||
overflowY: 'scroll',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListItem = styled('li')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '72px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberInfo = styled('div')(() => {
|
||||
return {
|
||||
paddingLeft: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberName = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '16px',
|
||||
color: theme.colors.textColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberEmail = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
color: theme.colors.iconColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '14px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMoreVerticalButton = styled('button')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
// fontWeight: '500',
|
||||
fontSize: theme.font.base,
|
||||
lineHeight: theme.font.lineHeightBase,
|
||||
marginTop: marginTop ? `${marginTop}px` : '0px',
|
||||
marginBottom: marginBottom ? `${marginBottom}px` : '0px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishExplanation = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '56px',
|
||||
paddingRight: '48px',
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
// fontWeight: '500',
|
||||
marginBottom: '22px',
|
||||
};
|
||||
});
|
||||
export const StyledWorkspaceName = styled('span')(() => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
};
|
||||
});
|
||||
export const StyledWorkspaceType = styled('span')(() => {
|
||||
return {
|
||||
// fontWeight: '500',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishCopyContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '12px',
|
||||
marginBottom: '20px',
|
||||
paddingTop: '20px',
|
||||
flex: 1,
|
||||
};
|
||||
});
|
||||
export const StyledStopPublishContainer = styled('div')(() => {
|
||||
return {
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '64px',
|
||||
};
|
||||
});
|
||||
export const StyledEmail = styled('span')(() => {
|
||||
return {
|
||||
color: '#E8178A',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishContent = styled('div')(({ theme }) => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: '38px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCopyButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishContent = styled('div')(() => {
|
||||
return {
|
||||
height: '494px',
|
||||
fontWeight: '500',
|
||||
flexDirection: 'column',
|
||||
fontSize: theme.font.base,
|
||||
lineHeight: theme.font.lineHeightBase,
|
||||
flex: 1,
|
||||
};
|
||||
});
|
||||
|
@ -1,185 +0,0 @@
|
||||
import { InformationIcon, LogOutIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { Divider } from '@/ui/divider';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { SelectorPopperContainer } from './styles';
|
||||
import {
|
||||
PrivateWorkspaceItem,
|
||||
WorkspaceItem,
|
||||
CreateWorkspaceItem,
|
||||
ListItem,
|
||||
LoginItem,
|
||||
} from './WorkspaceItem';
|
||||
import { WorkspaceSetting } from '@/components/workspace-setting';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { getDataCenter, WorkspaceType } 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, workspaces, refreshWorkspacesMeta } =
|
||||
useAppState();
|
||||
const [settingWorkspaceId, setSettingWorkspaceId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [workSpaceDetails, setWorkSpaceDetails] = useState<WorkspaceDetails>(
|
||||
{}
|
||||
);
|
||||
const { triggerContactModal } = useModal();
|
||||
|
||||
const handleClickSettingWorkspace = (workspaceId: string) => {
|
||||
setSettingWorkspaceId(workspaceId);
|
||||
};
|
||||
const handleCloseWorkSpace = () => {
|
||||
setSettingWorkspaceId(null);
|
||||
};
|
||||
const settingWorkspace = settingWorkspaceId
|
||||
? workspacesMeta.find(workspace => workspace.id === settingWorkspaceId)
|
||||
: undefined;
|
||||
|
||||
const refreshDetails = useCallback(async () => {
|
||||
const workspaceDetailList = await Promise.all(
|
||||
workspacesMeta.map(async ({ id, type }) => {
|
||||
if (user) {
|
||||
if (type === WorkspaceType.Private) {
|
||||
return { id, member_count: 1, owner: user };
|
||||
} else {
|
||||
const 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={
|
||||
(workspaces[workspace.id]?.meta.avatar &&
|
||||
`/api/blob/${workspaces[workspace.id]?.meta.avatar}`) ||
|
||||
`loading...`
|
||||
}
|
||||
onClickSetting={handleClickSettingWorkspace}
|
||||
name={workspaces[workspace.id]?.meta.name || `loading...`}
|
||||
memberCount={workSpaceDetails[workspace.id]?.memberCount || 1}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</WorkspaceWrapper>
|
||||
<CreateWorkspaceItem />
|
||||
{settingWorkspace ? (
|
||||
<WorkspaceSetting
|
||||
isShow={Boolean(settingWorkspaceId)}
|
||||
onClose={handleCloseWorkSpace}
|
||||
workspace={settingWorkspace}
|
||||
owner={
|
||||
(settingWorkspaceId &&
|
||||
workSpaceDetails[settingWorkspaceId]?.owner) || {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
}
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<StyledDivider />
|
||||
<ListItem
|
||||
icon={<InformationIcon />}
|
||||
name="About AFFiNE"
|
||||
onClick={() => triggerContactModal()}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<LogOutIcon />}
|
||||
name="Sign out"
|
||||
onClick={() => {
|
||||
console.log('Sign out');
|
||||
// FIXME: remove token from local storage and reload the page
|
||||
localStorage.removeItem('affine_token');
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
</SelectorPopperContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledDivider = styled(Divider)({
|
||||
margin: '8px 12px',
|
||||
width: 'calc(100% - 24px)',
|
||||
});
|
||||
|
||||
const WorkspaceGroupTitle = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
lineHeight: '30px',
|
||||
height: '30px',
|
||||
padding: '0 12px',
|
||||
};
|
||||
});
|
||||
|
||||
const WorkspaceWrapper = styled('div')(() => {
|
||||
return {
|
||||
maxHeight: '200px',
|
||||
overflow: 'auto',
|
||||
};
|
||||
});
|
@ -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',
|
||||
};
|
||||
});
|
@ -1 +0,0 @@
|
||||
export * from './CreateWorkspaceItem';
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export * from './WorkspaceCreate';
|
@ -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',
|
||||
};
|
||||
});
|
@ -1,60 +0,0 @@
|
||||
import { styled } from '@/styles';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import {
|
||||
WorkspaceItemAvatar,
|
||||
PrivateWorkspaceWrapper,
|
||||
WorkspaceItemContent,
|
||||
} from './styles';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type PrivateWorkspaceItemProps = {
|
||||
privateWorkspaceId?: string;
|
||||
};
|
||||
|
||||
export const PrivateWorkspaceItem = ({
|
||||
privateWorkspaceId,
|
||||
}: PrivateWorkspaceItemProps) => {
|
||||
const { user } = useAppState();
|
||||
const router = useRouter();
|
||||
const handleClick = () => {
|
||||
if (privateWorkspaceId) {
|
||||
router.push(`/workspace/${privateWorkspaceId}`);
|
||||
}
|
||||
};
|
||||
if (user) {
|
||||
const Username = user.name;
|
||||
return (
|
||||
<PrivateWorkspaceWrapper onClick={handleClick}>
|
||||
<WorkspaceItemAvatar alt={Username} src={user.avatar_url}>
|
||||
{Username}
|
||||
</WorkspaceItemAvatar>
|
||||
<WorkspaceItemContent>
|
||||
<Name title={Username}>{Username}</Name>
|
||||
<Email title={user.email}>{user.email}</Email>
|
||||
</WorkspaceItemContent>
|
||||
</PrivateWorkspaceWrapper>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.base,
|
||||
fontWeight: 500,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
const Email = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
@ -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',
|
||||
};
|
||||
});
|
@ -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',
|
||||
});
|
@ -1,96 +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',
|
||||
});
|
@ -1 +0,0 @@
|
||||
export * from './WorkspaceItem';
|
@ -1,5 +1,2 @@
|
||||
export * from './PrivateWorkspaceItem';
|
||||
export * from './WorkspaceItem';
|
||||
export * from './CreateWorkspaceItem';
|
||||
export * from './ListItem';
|
||||
export * from './LoginItem';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
import { MuiAvatar } from '@/ui/mui';
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const WorkspaceItemWrapper = styled('div')(({ theme }) => ({
|
||||
|
@ -1,53 +1,52 @@
|
||||
import { Popper } from '@/ui/popper';
|
||||
import { Avatar, WorkspaceName, SelectorWrapper } from './styles';
|
||||
import { SelectorPopperContent } from './SelectorPopperContent';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { WorkspaceModal } from '@/components/workspace-modal';
|
||||
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { WorkspaceType } from '@affine/datacenter';
|
||||
import { AffineIcon } from '../icons/Icons';
|
||||
|
||||
export const WorkspaceSelector = () => {
|
||||
const [isShow, setIsShow] = useState(false);
|
||||
const { currentWorkspace, workspacesMeta, currentWorkspaceId, user } =
|
||||
useAppState();
|
||||
const workspaceMeta = workspacesMeta.find(
|
||||
meta => String(meta.id) === String(currentWorkspaceId)
|
||||
);
|
||||
const [workspaceListShow, setWorkspaceListShow] = useState(false);
|
||||
const { currentWorkspace, workspaceList } = useAppState();
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceList.length === 0) {
|
||||
setWorkspaceListShow(true);
|
||||
}
|
||||
}, [workspaceList]);
|
||||
return (
|
||||
<Popper
|
||||
content={<SelectorPopperContent isShow={isShow} />}
|
||||
zIndex={1000}
|
||||
placement="bottom-start"
|
||||
trigger="click"
|
||||
onVisibleChange={setIsShow}
|
||||
>
|
||||
<SelectorWrapper data-testid="current-workspace">
|
||||
<>
|
||||
<SelectorWrapper
|
||||
onClick={() => {
|
||||
setWorkspaceListShow(true);
|
||||
}}
|
||||
data-testid="current-workspace"
|
||||
>
|
||||
<Avatar
|
||||
alt="Affine"
|
||||
data-testid="workspace-avatar"
|
||||
src={
|
||||
workspaceMeta?.type === WorkspaceType.Private
|
||||
? user
|
||||
? user.avatar_url
|
||||
: ''
|
||||
: currentWorkspace?.meta.avatar &&
|
||||
`/api/blob/${currentWorkspace?.meta.avatar}`
|
||||
}
|
||||
// src={currentWorkspace?.avatar}
|
||||
>
|
||||
{workspaceMeta?.type === WorkspaceType.Private && user ? (
|
||||
user?.name[0]
|
||||
) : (
|
||||
<AffineIcon />
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
float: 'left',
|
||||
}}
|
||||
>
|
||||
<WorkspaceUnitAvatar
|
||||
size={28}
|
||||
name={currentWorkspace?.name ?? 'AFFiNE'}
|
||||
workspaceUnit={currentWorkspace}
|
||||
/>
|
||||
</div>
|
||||
</Avatar>
|
||||
<WorkspaceName data-testid="workspace-name">
|
||||
{workspaceMeta?.type === WorkspaceType.Private
|
||||
? user
|
||||
? user.name
|
||||
: 'AFFiNE'
|
||||
: currentWorkspace?.meta.name || 'AFFiNE'}
|
||||
{currentWorkspace?.name ?? 'AFFiNE'}
|
||||
</WorkspaceName>
|
||||
</SelectorWrapper>
|
||||
</Popper>
|
||||
<WorkspaceModal
|
||||
open={workspaceListShow}
|
||||
onClose={() => {
|
||||
setWorkspaceListShow(false);
|
||||
}}
|
||||
></WorkspaceModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
import { MuiAvatar } from '@/ui/mui';
|
||||
import { styled } from '@/styles';
|
||||
import { StyledPopperContainer } from '@/ui/shared/Container';
|
||||
|
||||
|
@ -4,42 +4,41 @@ import {
|
||||
StyledArrowButton,
|
||||
StyledLink,
|
||||
StyledListItem,
|
||||
// StyledListItemForWorkspace,
|
||||
StyledListItemForWorkspace,
|
||||
StyledNewPageButton,
|
||||
StyledSliderBar,
|
||||
StyledSliderBarWrapper,
|
||||
StyledSubListItem,
|
||||
} from './style';
|
||||
import { Arrow } from './icons';
|
||||
// import { WorkspaceSelector } from './WorkspaceSelector';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
SearchIcon,
|
||||
AllPagesIcon,
|
||||
FavouritesIcon,
|
||||
ImportIcon,
|
||||
TrashIcon,
|
||||
AddIcon,
|
||||
SettingsIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import Link from 'next/link';
|
||||
import { MuiCollapse } from '@/ui/mui';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { IconButton } from '@/ui/button';
|
||||
import useLocalStorage from '@/hooks/use-local-storage';
|
||||
import usePageMetaList from '@/hooks/use-page-meta-list';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { WorkspaceSelector } from './WorkspaceSelector/WorkspaceSelector';
|
||||
|
||||
const FavoriteList = ({ showList }: { showList: boolean }) => {
|
||||
const { openPage } = usePageHelper();
|
||||
const pageList = usePageMetaList();
|
||||
const { pageList } = useAppState();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const favoriteList = pageList.filter(p => p.favorite && !p.trash);
|
||||
return (
|
||||
<Collapse in={showList}>
|
||||
<MuiCollapse in={showList}>
|
||||
{favoriteList.map((pageMeta, index) => {
|
||||
const active = router.query.pageId === pageMeta.id;
|
||||
return (
|
||||
@ -61,27 +60,29 @@ const FavoriteList = ({ showList }: { showList: boolean }) => {
|
||||
{favoriteList.length === 0 && (
|
||||
<StyledSubListItem disable={true}>{t('No item')}</StyledSubListItem>
|
||||
)}
|
||||
</Collapse>
|
||||
</MuiCollapse>
|
||||
);
|
||||
};
|
||||
export const WorkSpaceSliderBar = () => {
|
||||
const { triggerQuickSearchModal, triggerImportModal } = useModal();
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
const [showSubFavorite, setShowSubFavorite] = useState(true);
|
||||
const { currentWorkspaceId } = useAppState();
|
||||
const { currentWorkspace } = useAppState();
|
||||
const { openPage, createPage } = usePageHelper();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [showTip, setShowTip] = useState(false);
|
||||
const [show, setShow] = useLocalStorage('AFFiNE_SLIDE_BAR', false, true);
|
||||
|
||||
const currentWorkspaceId = currentWorkspace?.id;
|
||||
const paths = {
|
||||
all: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/all` : '',
|
||||
favorite: currentWorkspaceId
|
||||
? `/workspace/${currentWorkspaceId}/favorite`
|
||||
: '',
|
||||
trash: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/trash` : '',
|
||||
setting: currentWorkspaceId
|
||||
? `/workspace/${currentWorkspaceId}/setting`
|
||||
: '',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledSliderBar show={show}>
|
||||
@ -109,9 +110,9 @@ export const WorkSpaceSliderBar = () => {
|
||||
</Tooltip>
|
||||
|
||||
<StyledSliderBarWrapper data-testid="sliderBar">
|
||||
{/* <StyledListItemForWorkspace>
|
||||
<StyledListItemForWorkspace>
|
||||
<WorkspaceSelector />
|
||||
</StyledListItemForWorkspace> */}
|
||||
</StyledListItemForWorkspace>
|
||||
<StyledListItem
|
||||
data-testid="sliderBar-quickSearchButton"
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -146,14 +147,27 @@ export const WorkSpaceSliderBar = () => {
|
||||
</IconButton>
|
||||
</StyledListItem>
|
||||
<FavoriteList showList={showSubFavorite} />
|
||||
<StyledListItem active={router.asPath === paths.setting}>
|
||||
<StyledLink href={{ pathname: paths.setting }}>
|
||||
<SettingsIcon />
|
||||
{t('Settings')}
|
||||
</StyledLink>
|
||||
</StyledListItem>
|
||||
|
||||
<StyledListItem
|
||||
{/* <WorkspaceSetting
|
||||
isShow={showWorkspaceSetting}
|
||||
onClose={() => {
|
||||
setShowWorkspaceSetting(false);
|
||||
}}
|
||||
/> */}
|
||||
{/* TODO: will finish the feature next version */}
|
||||
{/* <StyledListItem
|
||||
onClick={() => {
|
||||
triggerImportModal();
|
||||
}}
|
||||
>
|
||||
<ImportIcon /> {t('Import')}
|
||||
</StyledListItem>
|
||||
</StyledListItem> */}
|
||||
|
||||
<Link href={{ pathname: paths.trash }}>
|
||||
<StyledListItem active={router.asPath === paths.trash}>
|
||||
|
@ -62,6 +62,7 @@ export const StyledListItem = styled.div<{
|
||||
color: active ? theme.colors.primaryColor : theme.colors.popoverColor,
|
||||
paddingLeft: '12px',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
...(disabled
|
||||
? {
|
||||
|
@ -11,7 +11,7 @@ export const useChangePageMeta = () => {
|
||||
|
||||
return useCallback<ChangePageMeta>(
|
||||
(pageId, pageMeta) => {
|
||||
currentWorkspace?.setPageMeta(pageId, {
|
||||
currentWorkspace?.blocksuiteWorkspace?.setPageMeta(pageId, {
|
||||
...pageMeta,
|
||||
});
|
||||
},
|
||||
|
@ -9,7 +9,7 @@ export const useCurrentPageMeta = (): PageMeta | null => {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
(currentWorkspace.meta.pageMetas.find(
|
||||
(currentWorkspace.blocksuiteWorkspace?.meta.pageMetas.find(
|
||||
p => p.id === currentPage.id
|
||||
) as PageMeta) ?? null
|
||||
);
|
||||
@ -22,9 +22,11 @@ export const useCurrentPageMeta = (): PageMeta | null => {
|
||||
useEffect(() => {
|
||||
setCurrentPageMeta(pageMetaHandler);
|
||||
|
||||
const dispose = currentWorkspace?.meta.pagesUpdated.on(() => {
|
||||
setCurrentPageMeta(pageMetaHandler);
|
||||
}).dispose;
|
||||
const dispose = currentWorkspace?.blocksuiteWorkspace?.meta.pagesUpdated.on(
|
||||
() => {
|
||||
setCurrentPageMeta(pageMetaHandler);
|
||||
}
|
||||
).dispose;
|
||||
|
||||
return () => {
|
||||
dispose?.();
|
||||
|
@ -1,28 +1,26 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useRouter } from 'next/router';
|
||||
const defaultOutLineWorkspaceId = 'affine';
|
||||
// 'local-first-' + '85b4ca0b9081421d903bbc2501ea280f';
|
||||
// It is a fully effective hook
|
||||
// Cause it not just ensure workspace loaded, but also have router change.
|
||||
export const useEnsureWorkspace = () => {
|
||||
const [workspaceLoaded, setWorkspaceLoaded] = useState(false);
|
||||
const { workspacesMeta, loadWorkspace, synced, user } = useAppState();
|
||||
const { workspaceList, loadWorkspace, user } = useAppState();
|
||||
const router = useRouter();
|
||||
const [activeWorkspaceId, setActiveWorkspaceId] = useState(
|
||||
router.query.workspaceId as string
|
||||
);
|
||||
|
||||
// const defaultOutLineWorkspaceId = '99ce7eb7';
|
||||
// console.log(defaultOutLineWorkspaceId);
|
||||
useEffect(() => {
|
||||
if (!synced) {
|
||||
setWorkspaceLoaded(false);
|
||||
return;
|
||||
}
|
||||
// If router.query.workspaceId is not in workspace list, jump to 404 page
|
||||
// If workspacesMeta is empty, we need to create a default workspace but not jump to 404
|
||||
// If workspaceList is empty, we need to create a default workspace but not jump to 404
|
||||
if (
|
||||
workspacesMeta.length &&
|
||||
router.query.workspaceId &&
|
||||
workspacesMeta.findIndex(
|
||||
workspaceList.length &&
|
||||
// FIXME: router is not ready when this hook is called
|
||||
location.pathname.startsWith(`/workspace/${router.query.workspaceId}`) &&
|
||||
workspaceList.findIndex(
|
||||
meta => meta.id.toString() === router.query.workspaceId
|
||||
) === -1
|
||||
) {
|
||||
@ -38,18 +36,17 @@ export const useEnsureWorkspace = () => {
|
||||
// router.push('/404');
|
||||
// return;
|
||||
// }
|
||||
|
||||
const workspaceId = user
|
||||
? (router.query.workspaceId as string) || workspacesMeta[0]?.id
|
||||
: (router.query.workspaceId as string) || defaultOutLineWorkspaceId;
|
||||
|
||||
const workspaceId =
|
||||
(router.query.workspaceId as string) || workspaceList[0]?.id;
|
||||
loadWorkspace(workspaceId).finally(() => {
|
||||
setWorkspaceLoaded(true);
|
||||
setActiveWorkspaceId(activeWorkspaceId);
|
||||
});
|
||||
}, [loadWorkspace, router, synced, user, workspacesMeta]);
|
||||
}, [loadWorkspace, router, user, workspaceList, activeWorkspaceId]);
|
||||
|
||||
return {
|
||||
workspaceLoaded,
|
||||
activeWorkspaceId,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,38 +0,0 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export const useInitWorkspace = (disabled?: boolean) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
// Do not set as a constant, or it will trigger a hell of re-rendering
|
||||
const defaultOutLineWorkspaceId = useRef(new Date().getTime().toString());
|
||||
|
||||
const router = useRouter();
|
||||
const {
|
||||
workspacesMeta,
|
||||
loadWorkspace,
|
||||
currentWorkspace,
|
||||
currentWorkspaceId,
|
||||
} = useAppState();
|
||||
const workspaceId =
|
||||
(router.query.workspaceId as string) ||
|
||||
workspacesMeta?.[0]?.id ||
|
||||
defaultOutLineWorkspaceId.current;
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
loadWorkspace(workspaceId).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [workspaceId, disabled, loadWorkspace]);
|
||||
|
||||
return {
|
||||
workspaceId,
|
||||
workspace: workspaceId === currentWorkspaceId ? currentWorkspace : null,
|
||||
loading,
|
||||
};
|
||||
};
|
51
packages/app/src/hooks/use-members.ts
Normal file
51
packages/app/src/hooks/use-members.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Member } from '@affine/datacenter';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
export const useMembers = () => {
|
||||
const { dataCenter, currentWorkspace } = useAppState();
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const refreshMembers = useCallback(async () => {
|
||||
if (!currentWorkspace || !dataCenter) return;
|
||||
const members = await dataCenter.getMembers(currentWorkspace.id);
|
||||
setMembers(members);
|
||||
}, [dataCenter, currentWorkspace]);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await refreshMembers();
|
||||
setLoaded(true);
|
||||
};
|
||||
init();
|
||||
}, [refreshMembers]);
|
||||
|
||||
const inviteMember = async (email: string) => {
|
||||
currentWorkspace &&
|
||||
dataCenter &&
|
||||
(await dataCenter.inviteMember(currentWorkspace.id, email));
|
||||
};
|
||||
|
||||
const removeMember = async (permissionId: number) => {
|
||||
if (!currentWorkspace || !dataCenter) {
|
||||
return;
|
||||
}
|
||||
setLoaded(false);
|
||||
await dataCenter.removeMember(currentWorkspace.id, permissionId);
|
||||
await refreshMembers();
|
||||
setLoaded(true);
|
||||
};
|
||||
|
||||
const getUserByEmail = async (email: string) => {
|
||||
if (!currentWorkspace) return null;
|
||||
return dataCenter?.getUserByEmail(currentWorkspace.id, email);
|
||||
};
|
||||
return {
|
||||
members,
|
||||
removeMember,
|
||||
inviteMember,
|
||||
getUserByEmail,
|
||||
loaded,
|
||||
};
|
||||
};
|
||||
|
||||
export default useMembers;
|
@ -1,9 +1,10 @@
|
||||
import { Workspace, uuidv4 } from '@blocksuite/store';
|
||||
import { uuidv4, Workspace } from '@blocksuite/store';
|
||||
import { QueryContent } from '@blocksuite/store/dist/workspace/search';
|
||||
import { PageMeta, useAppState } from '@/providers/app-state-provider';
|
||||
import { EditorContainer } from '@blocksuite/editor';
|
||||
import { useChangePageMeta } from '@/hooks/use-change-page-meta';
|
||||
import { useRouter } from 'next/router';
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
|
||||
export type EditorHandlers = {
|
||||
createPage: (params?: {
|
||||
@ -19,7 +20,10 @@ export type EditorHandlers = {
|
||||
toggleDeletePage: (pageId: string) => Promise<boolean>;
|
||||
toggleFavoritePage: (pageId: string) => Promise<boolean>;
|
||||
permanentlyDeletePage: (pageId: string) => void;
|
||||
search: (query: QueryContent) => Map<string, string | undefined>;
|
||||
search: (
|
||||
query: QueryContent,
|
||||
workspace?: Workspace
|
||||
) => Map<string, string | undefined>;
|
||||
// changeEditorMode: (pageId: string) => void;
|
||||
changePageMode: (
|
||||
pageId: string,
|
||||
@ -27,13 +31,15 @@ export type EditorHandlers = {
|
||||
) => Promise<EditorContainer['mode']>;
|
||||
};
|
||||
|
||||
const getPageMeta = (workspace: Workspace | null, pageId: string) => {
|
||||
return workspace?.meta.pageMetas.find(p => p.id === pageId);
|
||||
const getPageMeta = (workspace: WorkspaceUnit | null, pageId: string) => {
|
||||
return workspace?.blocksuiteWorkspace?.meta.pageMetas.find(
|
||||
p => p.id === pageId
|
||||
);
|
||||
};
|
||||
export const usePageHelper = (): EditorHandlers => {
|
||||
const router = useRouter();
|
||||
const changePageMeta = useChangePageMeta();
|
||||
const { currentWorkspace, editor, currentWorkspaceId } = useAppState();
|
||||
const { currentWorkspace, editor } = useAppState();
|
||||
|
||||
return {
|
||||
createPage: ({
|
||||
@ -44,11 +50,15 @@ export const usePageHelper = (): EditorHandlers => {
|
||||
if (!currentWorkspace) {
|
||||
return resolve(null);
|
||||
}
|
||||
currentWorkspace.createPage(pageId);
|
||||
currentWorkspace.signals.pageAdded.once(addedPageId => {
|
||||
currentWorkspace.setPageMeta(addedPageId, { title });
|
||||
resolve(addedPageId);
|
||||
});
|
||||
currentWorkspace.blocksuiteWorkspace?.createPage(pageId);
|
||||
currentWorkspace.blocksuiteWorkspace?.signals.pageAdded.once(
|
||||
addedPageId => {
|
||||
currentWorkspace.blocksuiteWorkspace?.setPageMeta(addedPageId, {
|
||||
title,
|
||||
});
|
||||
resolve(addedPageId);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
toggleFavoritePage: async pageId => {
|
||||
@ -76,9 +86,16 @@ export const usePageHelper = (): EditorHandlers => {
|
||||
});
|
||||
return trash;
|
||||
},
|
||||
search: (query: QueryContent) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return currentWorkspace!.search(query);
|
||||
search: (query: QueryContent, workspace?: Workspace) => {
|
||||
if (workspace) {
|
||||
return workspace.search(query);
|
||||
}
|
||||
if (currentWorkspace) {
|
||||
if (currentWorkspace.blocksuiteWorkspace) {
|
||||
return currentWorkspace.blocksuiteWorkspace.search(query);
|
||||
}
|
||||
}
|
||||
return new Map();
|
||||
},
|
||||
changePageMode: async (pageId, mode) => {
|
||||
const pageMeta = getPageMeta(currentWorkspace, pageId);
|
||||
@ -96,17 +113,17 @@ export const usePageHelper = (): EditorHandlers => {
|
||||
permanentlyDeletePage: pageId => {
|
||||
// TODO: workspace.meta.removePage or workspace.removePage?
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
currentWorkspace!.meta.removePage(pageId);
|
||||
currentWorkspace!.blocksuiteWorkspace?.meta.removePage(pageId);
|
||||
},
|
||||
openPage: (pageId, query = {}, newTab = false) => {
|
||||
pageId = pageId.replace('space:', '');
|
||||
|
||||
if (newTab) {
|
||||
window.open(`/workspace/${currentWorkspaceId}/${pageId}`, '_blank');
|
||||
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return router.push({
|
||||
pathname: `/workspace/${currentWorkspaceId}/${pageId}`,
|
||||
pathname: `/workspace/${currentWorkspace?.id}/${pageId}`,
|
||||
query,
|
||||
});
|
||||
},
|
||||
@ -116,7 +133,7 @@ export const usePageHelper = (): EditorHandlers => {
|
||||
}
|
||||
|
||||
return (
|
||||
(currentWorkspace.meta.pageMetas.find(
|
||||
(currentWorkspace.blocksuiteWorkspace?.meta.pageMetas.find(
|
||||
page => page.id === pageId
|
||||
) as PageMeta) || null
|
||||
);
|
||||
|
@ -1,25 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PageMeta } from '@/providers/app-state-provider';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
|
||||
export const usePageMetaList = () => {
|
||||
const { currentWorkspace } = useAppState();
|
||||
const [pageList, setPageList] = useState<PageMeta[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
setPageList(currentWorkspace.meta.pageMetas as PageMeta[]);
|
||||
const dispose = currentWorkspace.meta.pagesUpdated.on(() => {
|
||||
setPageList(currentWorkspace.meta.pageMetas as PageMeta[]);
|
||||
}).dispose;
|
||||
return () => {
|
||||
dispose();
|
||||
};
|
||||
}, [currentWorkspace]);
|
||||
|
||||
return pageList;
|
||||
};
|
||||
|
||||
export default usePageMetaList;
|
87
packages/app/src/hooks/use-workspace-helper.ts
Normal file
87
packages/app/src/hooks/use-workspace-helper.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useConfirm } from '@/providers/ConfirmProvider';
|
||||
import { toast } from '@/ui/toast';
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
import router from 'next/router';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
|
||||
export const useWorkspaceHelper = () => {
|
||||
const { confirm } = useConfirm();
|
||||
const { t } = useTranslation();
|
||||
const { dataCenter, currentWorkspace, user, login } = useAppState();
|
||||
const createWorkspace = async (name: string) => {
|
||||
const workspaceInfo = await dataCenter.createWorkspace({
|
||||
name: name,
|
||||
});
|
||||
if (workspaceInfo && workspaceInfo.id) {
|
||||
return await dataCenter.loadWorkspace(workspaceInfo.id);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// const updateWorkspace = async (workspace: Workspace) => {};
|
||||
|
||||
const publishWorkspace = async (workspaceId: string, publish: boolean) => {
|
||||
await dataCenter.setWorkspacePublish(workspaceId, publish);
|
||||
};
|
||||
|
||||
const updateWorkspace = async (
|
||||
{ name, avatarBlob }: { name?: string; avatarBlob?: Blob },
|
||||
workspace: WorkspaceUnit
|
||||
) => {
|
||||
if (name) {
|
||||
await dataCenter.updateWorkspaceMeta({ name }, workspace);
|
||||
}
|
||||
if (avatarBlob) {
|
||||
const blobId = await dataCenter.setBlob(workspace, avatarBlob);
|
||||
await dataCenter.updateWorkspaceMeta({ avatar: blobId }, workspace);
|
||||
}
|
||||
};
|
||||
|
||||
const enableWorkspace = async () => {
|
||||
confirm({
|
||||
title: `${t('Enable AFFiNE Cloud')}?`,
|
||||
content: t('Enable AFFiNE Cloud Description'),
|
||||
confirmText: user ? t('Enable') : t('Sign in and Enable'),
|
||||
cancelText: t('Skip'),
|
||||
confirmType: 'primary',
|
||||
buttonDirection: 'column',
|
||||
}).then(async confirm => {
|
||||
if (confirm && currentWorkspace) {
|
||||
if (!user) {
|
||||
await login();
|
||||
}
|
||||
const workspace = await dataCenter.enableWorkspaceCloud(
|
||||
currentWorkspace
|
||||
);
|
||||
workspace && router.push(`/workspace/${workspace.id}/setting`);
|
||||
toast(t('Enabled success'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteWorkSpace = async () => {
|
||||
currentWorkspace && (await dataCenter.deleteWorkspace(currentWorkspace.id));
|
||||
};
|
||||
const leaveWorkSpace = async () => {
|
||||
currentWorkspace && (await dataCenter.leaveWorkspace(currentWorkspace.id));
|
||||
};
|
||||
|
||||
const acceptInvite = async (inviteCode: string) => {
|
||||
let inviteInfo;
|
||||
if (inviteCode) {
|
||||
inviteInfo = await dataCenter.acceptInvitation(inviteCode);
|
||||
}
|
||||
return inviteInfo;
|
||||
};
|
||||
|
||||
return {
|
||||
createWorkspace,
|
||||
publishWorkspace,
|
||||
updateWorkspace,
|
||||
enableWorkspace,
|
||||
deleteWorkSpace,
|
||||
leaveWorkSpace,
|
||||
acceptInvite,
|
||||
};
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import NotfoundPage from '@/components/404';
|
||||
|
||||
export default function Custom404() {
|
||||
return <NotfoundPage></NotfoundPage>;
|
||||
}
|
||||
|
@ -10,9 +10,11 @@ import '../utils/print-build-info';
|
||||
import ProviderComposer from '@/components/provider-composer';
|
||||
import type { PropsWithChildren, ReactElement, ReactNode } from 'react';
|
||||
import type { NextPage } from 'next';
|
||||
import { AppStateProvider } from '@/providers/app-state-provider/Provider';
|
||||
import { AppStateProvider } from '@/providers/app-state-provider';
|
||||
import ConfirmProvider from '@/providers/ConfirmProvider';
|
||||
import { ModalProvider } from '@/providers/GlobalModalProvider';
|
||||
// import AppStateProvider2 from '@/providers/app-state-provider2/provider';
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
@ -65,9 +67,8 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => {
|
||||
};
|
||||
|
||||
const AppDefender = ({ children }: PropsWithChildren) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { synced } = useAppState();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (router.pathname === '/') {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { NextPage } from 'next';
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return <div>Home Page</div>;
|
||||
return <div title="Home Page"></div>;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
@ -1,43 +1,34 @@
|
||||
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
|
||||
import { styled } from '@/styles';
|
||||
import { Empty } from '@/ui/empty';
|
||||
import { Avatar } from '@mui/material';
|
||||
import { getDataCenter } from '@affine/datacenter';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const User = ({ name, avatar }: { name: string; avatar?: string }) => {
|
||||
return (
|
||||
<UserContent>
|
||||
{avatar ? (
|
||||
<Avatar src={avatar}></Avatar>
|
||||
) : (
|
||||
<UserIcon>{name.slice(0, 1)}</UserIcon>
|
||||
)}
|
||||
<span>{name}</span>
|
||||
</UserContent>
|
||||
);
|
||||
};
|
||||
// const User = ({ name, avatar }: { name: string; avatar?: string }) => {
|
||||
// return (
|
||||
// <UserContent>
|
||||
// {avatar ? (
|
||||
// <Avatar src={avatar}></Avatar>
|
||||
// ) : (
|
||||
// <UserIcon>{name.slice(0, 1)}</UserIcon>
|
||||
// )}
|
||||
// <span>{name}</span>
|
||||
// </UserContent>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default function DevPage() {
|
||||
const router = useRouter();
|
||||
const [successInvited, setSuccessInvited] = useState<boolean>(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [inviteData, setInviteData] = useState<any>(null);
|
||||
const { acceptInvite } = useWorkspaceHelper();
|
||||
useEffect(() => {
|
||||
getDataCenter()
|
||||
.then(dc =>
|
||||
dc.apis.acceptInviting({
|
||||
invitingCode: router.query.invite_code as string,
|
||||
})
|
||||
)
|
||||
.then(data => {
|
||||
setSuccessInvited(true);
|
||||
setInviteData(data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('err: ', err);
|
||||
router.query.invite_code &&
|
||||
acceptInvite(router.query.invite_code as string).then(data => {
|
||||
if (data && data.accepted) {
|
||||
setSuccessInvited(true);
|
||||
}
|
||||
});
|
||||
}, [router.query.invite_code]);
|
||||
}, [router, acceptInvite]);
|
||||
|
||||
return (
|
||||
<Invited>
|
||||
@ -45,11 +36,12 @@ export default function DevPage() {
|
||||
<Empty width={310} height={310}></Empty>
|
||||
|
||||
<Content>
|
||||
<User name={inviteData?.name ? inviteData.name : '-'}></User> invited
|
||||
you to join
|
||||
<User
|
||||
{/* TODO add inviteInfo*/}
|
||||
{/* <User name={inviteData?.name ? inviteData.name : '-'}></User> invited */}
|
||||
{/* you to join */}
|
||||
{/* <User
|
||||
name={inviteData?.workspaceName ? inviteData.workspaceName : '-'}
|
||||
></User>
|
||||
></User> */}
|
||||
{successInvited ? (
|
||||
<Status>
|
||||
<svg
|
||||
@ -102,16 +94,16 @@ export default function DevPage() {
|
||||
</Invited>
|
||||
);
|
||||
}
|
||||
const UserIcon = styled('div')({
|
||||
display: 'inline-block',
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#FFF5AB',
|
||||
textAlign: 'center',
|
||||
color: '#896406',
|
||||
lineHeight: '28px',
|
||||
});
|
||||
// const UserIcon = styled('div')({
|
||||
// display: 'inline-block',
|
||||
// width: '28px',
|
||||
// height: '28px',
|
||||
// borderRadius: '50%',
|
||||
// backgroundColor: '#FFF5AB',
|
||||
// textAlign: 'center',
|
||||
// color: '#896406',
|
||||
// lineHeight: '28px',
|
||||
// });
|
||||
|
||||
const Invited = styled('div')(({ theme }) => {
|
||||
return {
|
||||
@ -130,14 +122,6 @@ const Content = styled('div')({
|
||||
marginTop: '35px',
|
||||
});
|
||||
|
||||
const UserContent = styled('span')({
|
||||
fontSize: '18px',
|
||||
marginLeft: '12px',
|
||||
span: {
|
||||
padding: '0 12px',
|
||||
},
|
||||
});
|
||||
|
||||
const Status = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '16px',
|
||||
|
@ -4,7 +4,7 @@ import exampleMarkdown1 from '@/templates/Welcome-to-the-AFFiNE-Alpha.md';
|
||||
import exampleMarkdown2 from '@/templates/AFFiNE-Docs.md';
|
||||
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { Button } from '@/ui/button';
|
||||
interface Template {
|
||||
name: string;
|
||||
@ -37,11 +37,11 @@ const All = () => {
|
||||
const { openPage, createPage } = usePageHelper();
|
||||
const { currentWorkspace } = useAppState();
|
||||
const _applyTemplate = function (pageId: string, template: Template) {
|
||||
const page = currentWorkspace?.getPage(pageId);
|
||||
const page = currentWorkspace?.blocksuiteWorkspace?.getPage(pageId);
|
||||
|
||||
const title = template.name;
|
||||
if (page) {
|
||||
currentWorkspace?.setPageMeta(page.id, { title });
|
||||
currentWorkspace?.blocksuiteWorkspace?.setPageMeta(page.id, { title });
|
||||
if (page && page.root === null) {
|
||||
setTimeout(async () => {
|
||||
const editor = document.querySelector('editor-container');
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user