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

Conflicts:
	package.json
	packages/data-center/package.json
	pnpm-lock.yaml
This commit is contained in:
linonetwo
2023-01-09 14:59:34 +08:00
132 changed files with 2338 additions and 3219 deletions
-1
View File
@@ -1,5 +1,4 @@
**/webpack.config.js **/webpack.config.js
**/jest.config.js
**/scripts/*.js **/scripts/*.js
**/node_modules/** **/node_modules/**
.github/** .github/**
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
scope: '@toeverything' scope: '@toeverything'
cache: 'pnpm' cache: 'pnpm'
- run: node scripts/module-resolve/ci.js - run: node scripts/module-resolve/ci.cjs
- name: Restore cache - name: Restore cache
uses: actions/cache@v3 uses: actions/cache@v3
+1 -1
View File
@@ -244,7 +244,7 @@ jobs:
scope: '@toeverything' scope: '@toeverything'
cache: 'pnpm' cache: 'pnpm'
- run: node scripts/module-resolve/ci.js - run: node scripts/module-resolve/ci.cjs
- name: Restore cache - name: Restore cache
uses: actions/cache@v3 uses: actions/cache@v3
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
scope: '@toeverything' scope: '@toeverything'
cache: 'pnpm' cache: 'pnpm'
- run: node scripts/module-resolve/ci.js - run: node scripts/module-resolve/ci.cjs
- name: Install dependencies - name: Install dependencies
run: pnpm install --no-frozen-lockfile run: pnpm install --no-frozen-lockfile
+1
View File
@@ -48,6 +48,7 @@ Thumbs.db
out/ out/
module-resolve.js module-resolve.js
module-resolve.cjs
/test-results/ /test-results/
/playwright-report/ /playwright-report/
/playwright/.cache/ /playwright/.cache/
+2 -2
View File
@@ -1,11 +1,11 @@
const fs = require('fs'); const fs = require('fs');
function getCustomize() { function getCustomize() {
const customed = fs.existsSync('./module-resolve.js'); const customed = fs.existsSync('./module-resolve.cjs');
if (!customed) { if (!customed) {
return null; return null;
} }
const script = require('./module-resolve.js'); const script = require('./module-resolve.cjs');
return script && script.resolve; return script && script.resolve;
} }
+6 -1
View File
@@ -1,3 +1,8 @@
{ {
"recommendations": ["ms-playwright.playwright", "esbenp.prettier-vscode"] "recommendations": [
"ms-playwright.playwright",
"esbenp.prettier-vscode",
"deepscan.vscode-deepscan",
"streetsidesoftware.code-spell-checker"
]
} }
+2 -2
View File
@@ -4,6 +4,7 @@
"private": true, "private": true,
"author": "toeverything", "author": "toeverything",
"license": "MPL-2.0", "license": "MPL-2.0",
"type": "module",
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development pnpm --filter=!@affine/app build && pnpm --filter @affine/app dev", "dev": "cross-env NODE_ENV=development pnpm --filter=!@affine/app build && pnpm --filter @affine/app dev",
"dev:ac": "pnpm --filter=!@affine/app build && cross-env NODE_API_SERVER=ac pnpm --filter @affine/app dev", "dev:ac": "pnpm --filter=!@affine/app build && cross-env NODE_API_SERVER=ac pnpm --filter @affine/app dev",
@@ -47,8 +48,7 @@
"lint-staged": "^13.1.0", "lint-staged": "^13.1.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"typescript": "^4.9.3", "typescript": "^4.9.3"
"vitest": "^0.26.3"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,
-5
View File
@@ -1,5 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
+4 -4
View File
@@ -10,10 +10,10 @@
}, },
"dependencies": { "dependencies": {
"@affine/datacenter": "workspace:*", "@affine/datacenter": "workspace:*",
"@blocksuite/blocks": "=0.3.1-20230106060050-1aad55d", "@blocksuite/blocks": "0.3.1-20230109032243-37ad3ba",
"@blocksuite/editor": "=0.3.1-20230106060050-1aad55d", "@blocksuite/editor": "0.3.1-20230109032243-37ad3ba",
"@blocksuite/icons": "^2.0.2", "@blocksuite/icons": "^2.0.2",
"@blocksuite/store": "=0.3.1-20230106060050-1aad55d", "@blocksuite/store": "0.3.1-20230109032243-37ad3ba",
"@emotion/css": "^11.10.0", "@emotion/css": "^11.10.0",
"@emotion/react": "^11.10.4", "@emotion/react": "^11.10.4",
"@emotion/server": "^11.10.0", "@emotion/server": "^11.10.0",
@@ -49,8 +49,8 @@
"eslint-config-next": "12.3.1", "eslint-config-next": "12.3.1",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"raw-loader": "^4.0.2",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"raw-loader": "^4.0.2",
"typescript": "4.8.3" "typescript": "4.8.3"
}, },
"eslintConfig": { "eslintConfig": {
@@ -8,7 +8,7 @@ import {
TelegramIcon, TelegramIcon,
RedditIcon, RedditIcon,
LinkIcon, LinkIcon,
} from './icons'; } from './Icons';
import logo from './affine-text-logo.png'; import logo from './affine-text-logo.png';
import { import {
StyledBigLink, StyledBigLink,
@@ -0,0 +1,94 @@
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 { useTemporaryHelper } from '@/providers/temporary-helper-provider';
import { KeyboardEvent } from 'react';
import { useTranslation } from 'react-i18next';
interface ICloseParams {
workspaceId?: string;
}
interface ModalProps {
open: boolean;
onClose: (opts: ICloseParams) => void;
}
export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => {
const [workspaceName, setWorkspaceName] = useState('');
const { createWorkspace, setActiveWorkspace } = useTemporaryHelper();
const handleCreateWorkspace = () => {
const workspace = createWorkspace(workspaceName);
onClose({ workspaceId: workspace.id });
setActiveWorkspace(workspace);
};
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={620} height={334} style={{ padding: '10px' }}>
<Header>
<ContentTitle>{t('New Workspace')}</ContentTitle>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose({});
}}
/>
</Header>
<Content>
<p>{t('Workspace description')}</p>
<Input
onKeyDown={handleKeyDown}
onChange={value => {
setWorkspaceName(value);
}}
></Input>
<Button
onClick={() => {
handleCreateWorkspace();
}}
>
{t('Create')}
</Button>
</Content>
</ModalWrapper>
</Modal>
</div>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')({
display: 'flex',
padding: '0 48px',
flexDirection: 'column',
alignItems: 'center',
gap: '16px',
});
const ContentTitle = styled('span')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'left',
paddingBottom: '16px',
});
// const Footer = styled('div')({
// height: '70px',
// paddingLeft: '24px',
// marginTop: '32px',
// textAlign: 'center',
// });
@@ -13,79 +13,83 @@ import {
ConnectorIcon, ConnectorIcon,
UndoIcon, UndoIcon,
RedoIcon, RedoIcon,
} from './icons'; } from './Icons';
import { Tooltip } from '@/ui/tooltip'; import { Tooltip } from '@/ui/tooltip';
import Slide from '@mui/material/Slide'; import Slide from '@mui/material/Slide';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import useHistoryUpdated from '@/hooks/use-history-update'; import useHistoryUpdated from '@/hooks/use-history-update';
import { useTranslation } from 'react-i18next';
const toolbarList1 = [ const useToolbarList1 = () => {
{ const { t } = useTranslation();
flavor: 'select', return [
icon: <SelectIcon />, {
toolTip: 'Select', flavor: 'select',
disable: false, icon: <SelectIcon />,
callback: () => { toolTip: t('Select'),
window.dispatchEvent( disable: false,
new CustomEvent('affine.switch-mouse-mode', { callback: () => {
detail: { window.dispatchEvent(
type: 'default', new CustomEvent('affine.switch-mouse-mode', {
}, detail: {
}) type: 'default',
); },
})
);
},
}, },
}, {
{ flavor: 'text',
flavor: 'text', icon: <TextIcon />,
icon: <TextIcon />, toolTip: t('Text'),
toolTip: 'Text (coming soon)', disable: true,
disable: true, },
}, {
{ flavor: 'shape',
flavor: 'shape', icon: <ShapeIcon />,
icon: <ShapeIcon />, toolTip: t('Shape'),
toolTip: 'Shape', disable: false,
disable: false, callback: () => {
callback: () => { window.dispatchEvent(
window.dispatchEvent( new CustomEvent('affine.switch-mouse-mode', {
new CustomEvent('affine.switch-mouse-mode', { detail: {
detail: { type: 'shape',
type: 'shape', color: 'black',
color: 'black', shape: 'rectangle',
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', flavor: 'connector',
icon: <ConnectorIcon />, icon: <ConnectorIcon />,
toolTip: 'Connector (coming soon)', toolTip: t('Connector'),
disable: true, disable: true,
}, },
]; ];
};
const UndoRedo = () => { const UndoRedo = () => {
const [canUndo, setCanUndo] = useState(false); const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false); const [canRedo, setCanRedo] = useState(false);
const { currentPage } = useAppState(); const { currentPage } = useAppState();
const onHistoryUpdated = useHistoryUpdated(); const onHistoryUpdated = useHistoryUpdated();
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
onHistoryUpdated(page => { onHistoryUpdated(page => {
setCanUndo(page.canUndo); setCanUndo(page.canUndo);
@@ -95,7 +99,7 @@ const UndoRedo = () => {
return ( return (
<StyledToolbarWrapper> <StyledToolbarWrapper>
<Tooltip content="Undo" placement="right-start"> <Tooltip content={t('Undo')} placement="right-start">
<StyledToolbarItem <StyledToolbarItem
disable={!canUndo} disable={!canUndo}
onClick={() => { onClick={() => {
@@ -105,7 +109,7 @@ const UndoRedo = () => {
<UndoIcon /> <UndoIcon />
</StyledToolbarItem> </StyledToolbarItem>
</Tooltip> </Tooltip>
<Tooltip content="Redo" placement="right-start"> <Tooltip content={t('Redo')} placement="right-start">
<StyledToolbarItem <StyledToolbarItem
disable={!canRedo} disable={!canRedo}
onClick={() => { onClick={() => {
@@ -131,7 +135,7 @@ export const EdgelessToolbar = () => {
> >
<StyledEdgelessToolbar aria-label="edgeless-toolbar"> <StyledEdgelessToolbar aria-label="edgeless-toolbar">
<StyledToolbarWrapper> <StyledToolbarWrapper>
{toolbarList1.map( {useToolbarList1().map(
({ icon, toolTip, flavor, disable, callback }, index) => { ({ icon, toolTip, flavor, disable, callback }, index) => {
return ( return (
<Tooltip key={index} content={toolTip} placement="right-start"> <Tooltip key={index} content={toolTip} placement="right-start">
@@ -11,8 +11,8 @@ import type {
AnimateRadioProps, AnimateRadioProps,
AnimateRadioItemProps, AnimateRadioItemProps,
} from './type'; } from './type';
import { useTheme } from '@/providers/themeProvider'; import { useTheme } from '@/providers/ThemeProvider';
import { EdgelessIcon, PaperIcon } from './icons'; import { EdgelessIcon, PaperIcon } from './Icons';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -1,6 +1,7 @@
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { FC, useRef, ChangeEvent, ReactElement } from 'react'; import { FC, useRef, ChangeEvent, ReactElement } from 'react';
import { styled } from '@/styles'; import { styled } from '@/styles';
import { useTranslation } from 'react-i18next';
interface Props { interface Props {
uploadType?: string; uploadType?: string;
children?: ReactElement; children?: ReactElement;
@@ -9,6 +10,7 @@ interface Props {
} }
export const Upload: FC<Props> = props => { export const Upload: FC<Props> = props => {
const { fileChange, accept } = props; const { fileChange, accept } = props;
const { t } = useTranslation();
const input_ref = useRef<HTMLInputElement>(null); const input_ref = useRef<HTMLInputElement>(null);
const _chooseFile = () => { const _chooseFile = () => {
if (input_ref.current) { if (input_ref.current) {
@@ -28,7 +30,7 @@ export const Upload: FC<Props> = props => {
}; };
return ( return (
<UploadStyle onClick={_chooseFile}> <UploadStyle onClick={_chooseFile}>
{props.children ?? <Button>Upload</Button>} {props.children ?? <Button>{t('Upload')}</Button>}
<input <input
ref={input_ref} ref={input_ref}
type="file" type="file"
@@ -8,8 +8,8 @@ import {
import { Content } from '@/ui/layout'; import { Content } from '@/ui/layout';
import { useAppState } from '@/providers/app-state-provider/context'; import { useAppState } from '@/providers/app-state-provider/context';
import EditorModeSwitch from '@/components/editor-mode-switch'; import EditorModeSwitch from '@/components/editor-mode-switch';
import QuickSearchButton from './quick-search-button'; import QuickSearchButton from './QuickSearchButton';
import Header from './header'; import Header from './Header';
import usePropsUpdated from '@/hooks/use-props-updated'; import usePropsUpdated from '@/hooks/use-props-updated';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';
@@ -8,10 +8,10 @@ import {
} from './styles'; } from './styles';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { getWarningMessage, shouldShowWarning } from './utils'; import { getWarningMessage, shouldShowWarning } from './utils';
import EditorOptionMenu from './header-right-items/editor-option-menu'; import EditorOptionMenu from './header-right-items/EditorOptionMenu';
import TrashButtonGroup from './header-right-items/trash-button-group'; import TrashButtonGroup from './header-right-items/TrashButtonGroup';
import ThemeModeSwitch from './header-right-items/theme-mode-switch'; import ThemeModeSwitch from './header-right-items/theme-mode-switch';
import SyncUser from './header-right-items/sync-user'; import SyncUser from './header-right-items/SyncUser';
const BrowserWarning = ({ const BrowserWarning = ({
show, show,
@@ -1,7 +1,7 @@
import { PropsWithChildren, ReactNode } from 'react'; import { PropsWithChildren, ReactNode } from 'react';
import Header from './header'; import Header from './Header';
import { StyledPageListTittleWrapper } from './styles'; import { StyledPageListTittleWrapper } from './styles';
import QuickSearchButton from './quick-search-button'; import QuickSearchButton from './QuickSearchButton';
export type PageListHeaderProps = PropsWithChildren<{ export type PageListHeaderProps = PropsWithChildren<{
icon?: ReactNode; icon?: ReactNode;
@@ -2,7 +2,7 @@ import React from 'react';
import { IconButton, IconButtonProps } from '@/ui/button'; import { IconButton, IconButtonProps } from '@/ui/button';
import { Tooltip } from '@/ui/tooltip'; import { Tooltip } from '@/ui/tooltip';
import { ArrowDownIcon } from '@blocksuite/icons'; import { ArrowDownIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/GlobalModalProvider';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const QuickSearchButton = ({ export const QuickSearchButton = ({
onClick, onClick,
@@ -13,7 +13,7 @@ import {
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useConfirm } from '@/providers/confirm-provider'; import { useConfirm } from '@/providers/ConfirmProvider';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { toast } from '@/ui/toast'; import { toast } from '@/ui/toast';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -1,5 +1,5 @@
import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons'; import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/GlobalModalProvider';
import { useAppState } from '@/providers/app-state-provider/context'; import { useAppState } from '@/providers/app-state-provider/context';
import { IconButton } from '@/ui/button'; import { IconButton } from '@/ui/button';
@@ -1,9 +1,10 @@
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { useConfirm } from '@/providers/confirm-provider'; import { useConfirm } from '@/providers/ConfirmProvider';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { useTranslation } from 'react-i18next';
export const TrashButtonGroup = () => { export const TrashButtonGroup = () => {
const { permanentlyDeletePage } = usePageHelper(); const { permanentlyDeletePage } = usePageHelper();
@@ -12,7 +13,7 @@ export const TrashButtonGroup = () => {
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const router = useRouter(); const router = useRouter();
const { id = '' } = useCurrentPageMeta() || {}; const { id = '' } = useCurrentPageMeta() || {};
const { t } = useTranslation();
return ( return (
<> <>
<Button <Button
@@ -23,7 +24,7 @@ export const TrashButtonGroup = () => {
toggleDeletePage(id); toggleDeletePage(id);
}} }}
> >
Restore it {t('Restore it')}
</Button> </Button>
<Button <Button
bold={true} bold={true}
@@ -31,10 +32,9 @@ export const TrashButtonGroup = () => {
type="danger" type="danger"
onClick={() => { onClick={() => {
confirm({ confirm({
title: 'Permanently delete', title: t('TrashButtonGroupTitle'),
content: content: t('TrashButtonGroupDescription'),
"Once deleted, you can't undo this action. Do you confirm?", confirmText: t('Delete'),
confirmText: 'Delete',
confirmType: 'danger', confirmType: 'danger',
}).then(confirm => { }).then(confirm => {
if (confirm) { if (confirm) {
@@ -44,7 +44,7 @@ export const TrashButtonGroup = () => {
}); });
}} }}
> >
Delete permanently {t('Delete permanently')}
</Button> </Button>
</> </>
); );
@@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTheme } from '@/providers/themeProvider'; import { useTheme } from '@/providers/ThemeProvider';
import { MoonIcon, SunIcon } from './icons'; import { MoonIcon, SunIcon } from './Icons';
import { StyledThemeModeSwitch, StyledSwitchItem } from './style'; import { StyledThemeModeSwitch, StyledSwitchItem } from './style';
export const ThemeModeSwitch = () => { export const ThemeModeSwitch = () => {
+3 -3
View File
@@ -1,3 +1,3 @@
export * from './header'; export * from './Header';
export * from './editor-header'; export * from './EditorHeader';
export * from './page-list-header'; export * from './PageListHeader';
@@ -5,12 +5,12 @@ import {
StyledIslandWrapper, StyledIslandWrapper,
StyledTransformIcon, StyledTransformIcon,
} from './style'; } from './style';
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './icons'; import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
import Grow from '@mui/material/Grow'; import Grow from '@mui/material/Grow';
import { Tooltip } from '@/ui/tooltip'; import { Tooltip } from '@/ui/tooltip';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/GlobalModalProvider';
import { useTheme } from '@/providers/themeProvider'; import { useTheme } from '@/providers/ThemeProvider';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';
export type IslandItemNames = 'contact' | 'shortcuts'; export type IslandItemNames = 'contact' | 'shortcuts';
export const HelpIsland = ({ export const HelpIsland = ({
@@ -4,8 +4,9 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import Input from '@/ui/input'; import Input from '@/ui/input';
import { useState } from 'react'; import { useState } from 'react';
import { getDataCenter } from '@affine/datacenter'; // import { getDataCenter } from '@affine/datacenter';
import { Avatar } from '@mui/material'; import { Avatar } from '@mui/material';
import { setMember } from '@/hooks/mock-data/mock';
interface LoginModalProps { interface LoginModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@@ -54,29 +55,34 @@ export const InviteMembers = ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [userData, setUserData] = useState<any>({}); const [userData, setUserData] = useState<any>({});
const inputChange = (value: string) => { const inputChange = (value: string) => {
setEmail(value);
setShowMember(true); setShowMember(true);
if (gmailReg.test(value)) { if (gmailReg.test(value)) {
setEmail(value);
setShowTip(false); setShowTip(false);
debounce( setUserData({
() => { name: 'wxl',
// getDataCenter() avatar: 'https://avatars.githubusercontent.com/u/20501502?v=4',
// .then(dc => email: value,
// dc.apis.getUserByEmail({ });
// email: value, // debounce(
// workspace_id: workspaceId, // () => {
// }) // getDataCenter()
// ) // .then(dc =>
// .then(data => { // dc.apis.getUserByEmail({
// if (data?.name) { // email: value,
// setUserData(data); // workspace_id: workspaceId,
// setShowTip(false); // })
// } // )
// }); // .then(data => {
}, // if (data?.name) {
300, // setUserData(data);
true // setShowTip(false);
)(); // }
// });
// },
// 300,
// true
// )();
} else { } else {
setShowTip(true); setShowTip(true);
} }
@@ -134,6 +140,8 @@ export const InviteMembers = ({
shape="circle" shape="circle"
type="primary" type="primary"
onClick={() => { onClick={() => {
setMember(workspaceId, userData);
onInviteSuccess();
// getDataCenter() // getDataCenter()
// .then(dc => dc.apis.inviteMember({ id: workspaceId, email })) // .then(dc => dc.apis.inviteMember({ id: workspaceId, email }))
// .then(() => { // .then(() => {
@@ -1,5 +1,5 @@
import { styled } from '@/styles'; import { styled } from '@/styles';
import Loading from './loading'; import Loading from './Loading';
// Used for the full page loading // Used for the full page loading
const StyledLoadingContainer = styled('div')(() => { const StyledLoadingContainer = styled('div')(() => {
@@ -1,3 +1,3 @@
import Loading from './loading'; import Loading from './Loading';
export * from './page-loading'; export * from './PageLoading';
export default Loading; export default Loading;
@@ -1,8 +1,8 @@
import { getDataCenter } from '@affine/datacenter'; import { getDataCenter } from '@affine/datacenter';
import { styled } from '@/styles'; import { styled } from '@/styles';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/GlobalModalProvider';
import { GoogleIcon, StayLogOutIcon } from './icons'; import { GoogleIcon, StayLogOutIcon } from './Icons';
export const GoogleLoginButton = () => { export const GoogleLoginButton = () => {
const { triggerLoginModal } = useModal(); const { triggerLoginModal } = useModal();
@@ -1,4 +1,4 @@
import { useConfirm } from '@/providers/confirm-provider'; import { useConfirm } from '@/providers/ConfirmProvider';
import { PageMeta } from '@/providers/app-state-provider'; import { PageMeta } from '@/providers/app-state-provider';
import { Menu, MenuItem } from '@/ui/menu'; import { Menu, MenuItem } from '@/ui/menu';
import { Wrapper } from '@/ui/layout'; import { Wrapper } from '@/ui/layout';
@@ -12,18 +12,18 @@ import {
StyledTitleWrapper, StyledTitleWrapper,
} from './styles'; } from './styles';
import { Table, TableBody, TableCell, TableHead, TableRow } from '@/ui/table'; import { Table, TableBody, TableCell, TableHead, TableRow } from '@/ui/table';
import { OperationCell, TrashOperationCell } from './operation-cell'; import { OperationCell, TrashOperationCell } from './OperationCell';
import Empty from './empty'; import Empty from './Empty';
import { Content } from '@/ui/layout'; import { Content } from '@/ui/layout';
import React from 'react'; import React from 'react';
import DateCell from '@/components/page-list/date-cell'; import DateCell from '@/components/page-list/DateCell';
import { IconButton } from '@/ui/button'; import { IconButton } from '@/ui/button';
import { Tooltip } from '@/ui/tooltip'; import { Tooltip } from '@/ui/tooltip';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useAppState } from '@/providers/app-state-provider/context'; import { useAppState } from '@/providers/app-state-provider/context';
import { toast } from '@/ui/toast'; import { toast } from '@/ui/toast';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useTheme } from '@/providers/themeProvider'; import { useTheme } from '@/providers/ThemeProvider';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const FavoriteTag = ({ const FavoriteTag = ({
pageMeta: { favorite, id }, pageMeta: { favorite, id },
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { AddIcon } from '@blocksuite/icons'; import { AddIcon } from '@blocksuite/icons';
import { StyledModalFooterContent } from './style'; import { StyledModalFooterContent } from './style';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/GlobalModalProvider';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -1,12 +1,12 @@
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { StyledListItem, StyledNotFound } from './style'; import { StyledListItem, StyledNotFound } from './style';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/GlobalModalProvider';
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons'; import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useSwitchToConfig } from './config'; import { useSwitchToConfig } from './config';
import { NoResultSVG } from './noResultSVG'; import { NoResultSVG } from './NoResultSVG';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import usePageHelper from '@/hooks/use-page-helper'; import usePageHelper from '@/hooks/use-page-helper';
import usePageMetaList from '@/hooks/use-page-meta-list'; import usePageMetaList from '@/hooks/use-page-meta-list';
@@ -6,12 +6,12 @@ import {
StyledModalDivider, StyledModalDivider,
StyledShortcut, StyledShortcut,
} from './style'; } from './style';
import { Input } from './input'; import { Input } from './Input';
import { Results } from './results'; import { Results } from './Results';
import { Footer } from './footer'; import { Footer } from './Footer';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/GlobalModalProvider';
import { getUaHelper } from '@/utils'; import { getUaHelper } from '@/utils';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
type TransitionsModalProps = { type TransitionsModalProps = {
@@ -13,7 +13,8 @@ export const useMacKeyboardShortcuts = (): ShortcutTip => {
[t('Strikethrough')]: '⌘+⇧+S', [t('Strikethrough')]: '⌘+⇧+S',
[t('Inline code')]: ' ⌘+E', [t('Inline code')]: ' ⌘+E',
[t('Code block')]: '⌘+⌥+C', [t('Code block')]: '⌘+⌥+C',
[t('Link')]: '⌘+K', [t('Hyperlink(with selected text)')]: '⌘+K',
[t('Quick search')]: '⌘+K',
[t('Body text')]: '⌘+⌥+0', [t('Body text')]: '⌘+⌥+0',
[t('Heading', { number: '1' })]: '⌘+⌥+1', [t('Heading', { number: '1' })]: '⌘+⌥+1',
[t('Heading', { number: '2' })]: '⌘+⌥+2', [t('Heading', { number: '2' })]: '⌘+⌥+2',
@@ -56,7 +57,8 @@ export const useWindowsKeyboardShortcuts = (): ShortcutTip => {
[t('Strikethrough')]: 'Ctrl+Shift+S', [t('Strikethrough')]: 'Ctrl+Shift+S',
[t('Inline code')]: ' Ctrl+E', [t('Inline code')]: ' Ctrl+E',
[t('Code block')]: 'Ctrl+Alt+C', [t('Code block')]: 'Ctrl+Alt+C',
[t('Link')]: 'Ctrl+K', [t('Hyperlink(with selected text)')]: 'Ctrl+K',
[t('Quick search')]: 'Ctrl+K',
[t('Body text')]: 'Ctrl+Shift+0', [t('Body text')]: 'Ctrl+Shift+0',
[t('Heading', { number: '1' })]: 'Ctrl+Shift+1', [t('Heading', { number: '1' })]: 'Ctrl+Shift+1',
[t('Heading', { number: '2' })]: 'Ctrl+Shift+2', [t('Heading', { number: '2' })]: 'Ctrl+Shift+2',
@@ -1,5 +1,5 @@
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { KeyboardIcon } from './icons'; import { KeyboardIcon } from './Icons';
import { import {
StyledListItem, StyledListItem,
StyledModalHeader, StyledModalHeader,
@@ -0,0 +1,30 @@
import { stringToColour } from '@/utils';
interface IWorkspaceAvatar {
size: number;
name: string;
}
export const WorkspaceAvatar = (props: IWorkspaceAvatar) => {
const size = props.size || 20;
const sizeStr = size + 'px';
return (
<>
<div
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',
}}
>
{(props.name || 'AFFiNE').substring(0, 1)}
</div>
</>
);
};
@@ -0,0 +1,228 @@
import { styled } from '@/styles';
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import { useState } from 'react';
import { SignOut } from '@/hooks/mock-data/mock';
import { CreateWorkspaceModal } from '../create-workspace';
import {
CloudUnsyncedIcon,
CloudInsyncIcon,
UsersIcon,
AddIcon,
} from '@blocksuite/icons';
import { useConfirm } from '@/providers/confirm-provider';
import { toast } from '@/ui/toast';
import { WorkspaceAvatar } from '@/components/workspace-avatar';
import { useTemporaryHelper } from '@/providers/temporary-helper-provider';
interface LoginModalProps {
open: boolean;
onClose: () => void;
}
export const WorkspaceModal = ({ open, onClose }: LoginModalProps) => {
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
const { confirm } = useConfirm();
const {
user,
login,
workspaceMetaList,
setActiveWorkspace,
updateWorkspaceMeta,
} = useTemporaryHelper();
return (
<div>
<Modal open={open} onClose={onClose}>
<ModalWrapper
width={820}
style={{ padding: '10px', display: 'flex', flexDirection: 'column' }}
>
<Header>
<ContentTitle>My Workspaces</ContentTitle>
{/* <LanguageMenu /> */}
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
/>
</Header>
<Content>
<WorkspaceList>
{workspaceMetaList.map((item, index) => {
return (
<WorkspaceItem
onClick={() => {
setActiveWorkspace(item);
onClose();
}}
key={index}
>
<span style={{ width: '100px' }}>
<div
style={{
float: 'left',
marginTop: '6px',
marginLeft: '10px',
marginRight: '10px',
}}
>
<WorkspaceAvatar size={50} name={item.name} />
</div>
<span
style={{
width: '235px',
fontSize: '16px',
display: 'inline-block',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
position: 'relative',
top: '20px',
}}
>
{item.name || 'AFFiNE'}
</span>
</span>
<span
style={{
position: 'relative',
top: '20px',
}}
>
{(item.type === 'local' || !item.type) && (
<CloudUnsyncedIcon fontSize={24} />
)}
{item.type === 'cloud' && (
<CloudInsyncIcon fontSize={24} />
)}
{item.isPublish && <UsersIcon fontSize={24} />}
</span>
{/* {item.isLocal ? 'isLocal' : ''}/ */}
</WorkspaceItem>
);
})}
<li>
<Button
style={{
marginTop: '20px',
}}
type="primary"
onClick={() => {
setCreateWorkspaceOpen(true);
}}
>
<AddIcon
style={{
fontSize: '20px',
top: '5px',
position: 'relative',
marginRight: '10px',
}}
/>
Create Or Import
</Button>
</li>
</WorkspaceList>
<p style={{ fontSize: '14px', color: '#ccc', margin: '12px 0' }}>
Tips:Workspace is your virtual space to capture, create and plan
as just one person or together as a team.
</p>
</Content>
<Footer>
{!user ? (
<Button
onClick={() => {
login();
toast('login success');
}}
>
Sign in AFFiNE Cloud
</Button>
) : (
<Button
onClick={() => {
SignOut();
}}
>
Sign out of AFFiNE Cloud
</Button>
)}
</Footer>
<CreateWorkspaceModal
open={createWorkspaceOpen}
onClose={({ workspaceId }) => {
setCreateWorkspaceOpen(false);
onClose();
confirm({
title: 'Enable AFFiNE Cloud?',
content: `If enabled, the data in this workspace will be backed up and synchronized via AFFiNE Cloud.`,
confirmText: user ? 'Enable' : 'Sign in and Enable',
cancelText: 'Skip',
}).then(confirm => {
if (confirm) {
if (user) {
workspaceId &&
updateWorkspaceMeta(workspaceId, { isPublish: true });
} else {
login();
workspaceId &&
updateWorkspaceMeta(workspaceId, { isPublish: true });
}
}
});
}}
></CreateWorkspaceModal>
</ModalWrapper>
</Modal>
</div>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')({
padding: '0 20px',
flexDirection: 'column',
alignItems: 'center',
gap: '16px',
flex: 1,
});
const ContentTitle = styled('span')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'left',
paddingBottom: '16px',
});
const Footer = styled('div')({
height: '70px',
paddingLeft: '24px',
marginTop: '32px',
textAlign: 'center',
});
const WorkspaceList = styled('div')({
display: 'grid',
gridRowGap: '10px',
gridColumnGap: '10px',
fontSize: '16px',
gridTemplateColumns: 'repeat(2, 1fr)',
});
const WorkspaceItem = styled('div')({
cursor: 'pointer',
padding: '8px',
border: '1px solid #eee',
':hover': {
background: '#eee',
},
});
@@ -0,0 +1,95 @@
import { LOCALES } from '@/libs/i18n/resources/index';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
import type { TooltipProps } from '@mui/material';
import { styled } from '@/styles';
import { Button, Tooltip } from '@mui/material';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
export const LanguageMenu = () => {
const { i18n } = useTranslation();
const changeLanguage = (event: string) => {
i18n.changeLanguage(event);
};
const [show, setShow] = useState(false);
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
const [languageName, setLanguageName] = useState(
currentLanguage?.originalName
);
return (
<StyledTooltip
title={
<>
{LOCALES.map(option => {
return (
<ListItem
key={option.name}
title={option.name}
onClick={() => {
changeLanguage(option.tag);
setShow(false);
setLanguageName(option.originalName);
}}
>
{option.originalName}
</ListItem>
);
})}
</>
}
open={show}
>
<StyledTitleButton
variant="text"
onClick={() => {
setShow(!show);
}}
>
<StyledContainer>
<StyledText>{languageName}</StyledText>
<UnfoldMoreIcon />
</StyledContainer>
</StyledTitleButton>
</StyledTooltip>
);
};
const StyledContainer = styled('div')(() => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
}));
const StyledText = styled('span')(({ theme }) => ({
marginRight: '4px',
marginLeft: '16px',
fontSize: theme.font.sm,
fontWeight: '500',
textTransform: 'capitalize',
}));
const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
zIndex: theme.zIndex.modal,
'& .MuiTooltip-tooltip': {
backgroundColor: theme.colors.popoverBackground,
boxShadow: theme.shadow.modal,
color: theme.colors.popoverColor,
},
}));
const ListItem = styled(Button)(({ theme }) => ({
display: 'block',
width: '100%',
color: theme.colors.popoverColor,
fontSize: theme.font.sm,
textTransform: 'capitalize',
}));
const StyledTitleButton = styled(Button)(({ theme }) => ({
position: 'absolute',
right: '50px',
color: theme.colors.popoverColor,
fontSize: theme.font.sm,
}));
@@ -0,0 +1,38 @@
import { displayFlex, styled } from '@/styles';
export const StyledTitle = styled.div(() => {
return {
...displayFlex('center', 'center'),
fontSize: '20px',
fontWeight: 500,
marginTop: '60px',
lineHeight: 1,
};
});
export const StyledContent = styled.div(() => {
return {
padding: '0 40px',
marginTop: '32px',
fontSize: '18px',
lineHeight: '25px',
'p:not(last-of-type)': {
marginBottom: '10px',
},
};
});
export const StyledButton = styled.div(({ theme }) => {
return {
width: '146px',
height: '42px',
background: theme.colors.primaryColor,
color: '#FFFFFF',
fontSize: '18px',
fontWeight: 500,
borderRadius: '21px',
margin: '52px auto 0',
cursor: 'pointer',
...displayFlex('center', 'center'),
};
});
@@ -0,0 +1,19 @@
import { Workspace } from '@/hooks/mock-data/mock';
import { styled } from '@/styles';
export const ExportPageTitleContainer = styled('div')(() => {
return {
display: 'flex',
marginTop: '60px',
fontWeight: '500',
flex: 1,
};
});
export const ExportPage = ({ workspace }: { workspace: Workspace }) => {
return (
<ExportPageTitleContainer>
Export Workspace{' '}
<code style={{ margin: '0 10px' }}>{workspace.name}</code> Is Comming
</ExportPageTitleContainer>
);
};
@@ -0,0 +1,206 @@
import {
StyledMemberAvatar,
StyledMemberButtonContainer,
StyledMemberEmail,
StyledMemberInfo,
StyledMemberListContainer,
StyledMemberListItem,
StyledMemberName,
StyledMemberNameContainer,
StyledMemberRoleContainer,
StyledMemberTitleContainer,
StyledMoreVerticalButton,
} from './style';
import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons';
import { useEffect, useState } from 'react';
import { Button, IconButton } from '@/ui/button';
import { InviteMembers } from '../invite-members/index';
import { Menu, MenuItem } from '@/ui/menu';
import { Empty } from '@/ui/empty';
import {
deleteMember,
getMembers,
User,
Workspace,
} from '@/hooks/mock-data/mock';
import { useTemporaryHelper } from '@/providers/temporary-helper-provider';
import { StyledMemberWarp } from './general/style';
import { useConfirm } from '@/providers/ConfirmProvider';
// import { useAppState } from '@/providers/app-state-provider';
export const MembersPage = ({ workspace }: { workspace: Workspace }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const [members, setMembers] = useState<User[]>([]);
const { user, login, updateWorkspaceMeta } = useTemporaryHelper();
const { confirm } = useConfirm();
// const refreshMembers = useCallback(() => {
// getDataCenter()
// .then(dc =>
// dc.apis.getWorkspaceMembers({
// id: workspace.id,
// })
// )
// .then(data => {
// setMembers(data);
// })
// .catch(err => {
// console.log(err);
// });
// }, [workspace.id]);
const setMembersList = () => {
const members = getMembers(workspace.id);
members && setMembers(members);
};
useEffect(() => {
setMembersList();
// refreshMembers();
});
return (
<div>
{workspace.type === 'cloud' ? (
<>
<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, index) => {
return (
<StyledMemberListItem key={index}>
<StyledMemberNameContainer>
<StyledMemberAvatar alt="member avatar">
<EmailIcon></EmailIcon>
</StyledMemberAvatar>
<StyledMemberInfo>
<StyledMemberName>{member.name}</StyledMemberName>
<StyledMemberEmail>{member.email}</StyledMemberEmail>
</StyledMemberInfo>
</StyledMemberNameContainer>
<StyledMemberRoleContainer>
{/* {member.accepted
? member.type !== 99
? 'Member'
: 'Workspace Owner'
: 'Pending'} */}
Pending
</StyledMemberRoleContainer>
<StyledMoreVerticalButton>
<Menu
content={
<>
<MenuItem
onClick={() => {
deleteMember(workspace.id, 0);
setMembersList();
// 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={() => {
setMembersList();
setIsInviteModalShow(false);
// refreshMembers();
}}
workspaceId={workspace.id}
open={isInviteModalShow}
></InviteMembers>
</StyledMemberButtonContainer>
</>
) : (
<StyledMemberWarp>
<div style={{ flex: 1 }}>
Collaborating with other members requires AFFiNE Cloud service.
</div>
<div style={{ height: '40px' }}>
<Button
type="primary"
shape="circle"
onClick={() => {
confirm({
title: 'Enable AFFiNE Cloud?',
content: `If enabled, the data in this workspace will be backed up and synchronized via AFFiNE Cloud.`,
confirmText: user ? 'Enable' : 'Sign in and Enable',
cancelText: 'Skip',
}).then(confirm => {
if (confirm) {
if (user) {
updateWorkspaceMeta(workspace.id, { isPublish: true });
} else {
login();
updateWorkspaceMeta(workspace.id, { isPublish: true });
}
}
});
}}
>
Enable AFFiNE Cloud
</Button>
</div>
</StyledMemberWarp>
)}
</div>
);
};
@@ -0,0 +1,122 @@
import {
StyledCopyButtonContainer,
StyledPublishContent,
StyledPublishCopyContainer,
StyledPublishExplanation,
StyledSettingH2,
} from './style';
import { Button } from '@/ui/button';
import Input from '@/ui/input';
import { toast } from '@/ui/toast';
import { Workspace } from '@/hooks/mock-data/mock';
import { useTemporaryHelper } from '@/providers/temporary-helper-provider';
import { useConfirm } from '@/providers/ConfirmProvider';
export const PublishPage = ({ workspace }: { workspace: Workspace }) => {
console.log('workspace: ', workspace);
const shareUrl =
window.location.host + '/workspace/' + workspace.id + '?share=true';
const { login, updateWorkspaceMeta, user } = useTemporaryHelper();
const { confirm } = useConfirm();
const togglePublic = (flag: boolean) => {
updateWorkspaceMeta(workspace.id, { isPublish: flag });
};
const copyUrl = () => {
navigator.clipboard.writeText(shareUrl);
toast('Copied url to clipboard');
};
const enableAffineCloud = () => {
confirm({
title: 'Enable AFFiNE Cloud?',
content: `If enabled, the data in this workspace will be backed up and synchronized via AFFiNE Cloud.`,
confirmText: user ? 'Enable' : 'Sign in and Enable',
cancelText: 'Skip',
}).then(confirm => {
if (confirm) {
if (user) {
updateWorkspaceMeta(workspace.id, { type: 'cloud' });
} else {
login();
updateWorkspaceMeta(workspace.id, { type: 'cloud' });
}
}
});
};
return (
<>
{workspace.type === 'cloud' ? (
<div>
<StyledPublishContent>
{workspace?.isPublish ? (
<>
<StyledPublishExplanation>
Publishing to web requires AFFiNE Cloud service .
</StyledPublishExplanation>
<StyledSettingH2>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>
{workspace.isPublish ? (
<Button
onClick={() => {
togglePublic(false);
}}
type="primary"
shape="circle"
>
Stop publishing
</Button>
) : (
<Button
onClick={() => {
togglePublic(true);
}}
type="primary"
shape="circle"
>
Publish to web
</Button>
)}
</div>
) : (
<StyledPublishContent>
<>
<StyledPublishExplanation>
Publishing to web requires AFFiNE Cloud service.
</StyledPublishExplanation>
<StyledPublishCopyContainer>
<Button
onClick={() => {
enableAffineCloud();
}}
type="primary"
shape="circle"
>
Enable AFFiNE Cloud
</Button>
</StyledPublishCopyContainer>
</>
</StyledPublishContent>
)}
</>
);
};
@@ -0,0 +1,79 @@
import {
StyledPublishContent,
StyledPublishCopyContainer,
StyledPublishExplanation,
} from './style';
import { DownloadIcon } from '@blocksuite/icons';
import { Button } from '@/ui/button';
import { Menu, MenuItem } from '@/ui/menu';
import { deleteMember, Workspace } from '@/hooks/mock-data/mock';
import { useTemporaryHelper } from '@/providers/temporary-helper-provider';
export const SyncPage = ({ workspace }: { workspace: Workspace }) => {
const { currentWorkspace, updateWorkspaceMeta } = useTemporaryHelper();
return (
<div>
<StyledPublishContent>
{currentWorkspace?.type === 'local' ? (
<>
<StyledPublishExplanation>
{currentWorkspace.name} is Local Workspace. All data is stored on
the current device. You can enable AFFiNE Cloud for this workspace
to keep data in sync with the cloud.
</StyledPublishExplanation>
<StyledPublishCopyContainer>
<Button
onClick={() => {
updateWorkspaceMeta(currentWorkspace.id, {
type: 'cloud',
});
}}
type="primary"
shape="circle"
>
Enable AFFiNE Cloud
</Button>
</StyledPublishCopyContainer>
</>
) : (
<>
<StyledPublishExplanation>
<code>{currentWorkspace && currentWorkspace.name}</code> is Cloud
Workspace. All data will be synchronized and saved to the AFFiNE
</StyledPublishExplanation>
<StyledPublishCopyContainer>
<Menu
content={
<>
<MenuItem
onClick={() => {
deleteMember(workspace.id, 0);
}}
icon={<DownloadIcon />}
>
Download core data to device
</MenuItem>
<MenuItem
onClick={() => {
deleteMember(workspace.id, 0);
}}
icon={<DownloadIcon />}
>
Download all data to device
</MenuItem>
</>
}
placement="bottom-end"
disablePortal={true}
>
<Button>Download all data to device</Button>
</Menu>
</StyledPublishCopyContainer>
</>
)}
</StyledPublishContent>
</div>
);
};
@@ -0,0 +1,164 @@
import Modal, { ModalCloseButton } from '@/ui/modal';
import {
StyledSettingContainer,
StyledSettingContent,
StyledSettingSidebar,
StyledSettingSidebarHeader,
StyledSettingTabContainer,
StyledSettingTagIconContainer,
WorkspaceSettingTagItem,
} from './style';
import {
EditIcon,
UsersIcon,
PublishIcon,
CloudInsyncIcon,
} from '@blocksuite/icons';
import { useEffect, useState } from 'react';
import { GeneralPage } from './general';
import { MembersPage } from './MembersPage';
import { PublishPage } from './PublishPage';
import { ExportPage } from './ExportPage';
import { SyncPage } from './SyncPage';
import { useTemporaryHelper } from '@/providers/temporary-helper-provider';
enum ActiveTab {
'general' = 'general',
'members' = 'members',
'publish' = 'publish',
'sync' = 'sync',
'export' = 'export',
}
type SettingTabProps = {
activeTab: ActiveTab;
onTabChange?: (tab: ActiveTab) => void;
};
type WorkspaceSettingProps = {
isShow: boolean;
onClose?: () => void;
};
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.sync}
onClick={() => {
onTabChange && onTabChange(ActiveTab.sync);
}}
>
<StyledSettingTagIconContainer>
<CloudInsyncIcon />
</StyledSettingTagIconContainer>
Sync
</WorkspaceSettingTagItem>
<WorkspaceSettingTagItem
isActive={activeTab === ActiveTab.members}
onClick={() => {
onTabChange && onTabChange(ActiveTab.members);
}}
>
<StyledSettingTagIconContainer>
<UsersIcon />
</StyledSettingTagIconContainer>
Collaboration
</WorkspaceSettingTagItem>
<WorkspaceSettingTagItem
isActive={activeTab === ActiveTab.publish}
onClick={() => {
onTabChange && onTabChange(ActiveTab.publish);
}}
>
<StyledSettingTagIconContainer>
<PublishIcon />
</StyledSettingTagIconContainer>
Publish
</WorkspaceSettingTagItem>
<WorkspaceSettingTagItem
isActive={activeTab === ActiveTab.export}
onClick={() => {
onTabChange && onTabChange(ActiveTab.export);
}}
>
<StyledSettingTagIconContainer>
<PublishIcon />
</StyledSettingTagIconContainer>
Export
</WorkspaceSettingTagItem>
</StyledSettingTabContainer>
);
};
export const WorkspaceSetting = ({
isShow,
onClose,
}: WorkspaceSettingProps) => {
// const { workspaces } = useAppState();
const [activeTab, setActiveTab] = useState<ActiveTab>(ActiveTab.general);
const handleTabChange = (tab: ActiveTab) => {
setActiveTab(tab);
};
const { currentWorkspace } = useTemporaryHelper();
const handleClickClose = () => {
onClose && onClose();
};
const isOwner = true;
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 && currentWorkspace && (
<GeneralPage workspace={currentWorkspace} />
)}
{activeTab === ActiveTab.sync && currentWorkspace && (
<SyncPage workspace={currentWorkspace} />
)}
{activeTab === ActiveTab.members && currentWorkspace && (
<MembersPage workspace={currentWorkspace} />
)}
{activeTab === ActiveTab.publish && currentWorkspace && (
<PublishPage workspace={currentWorkspace} />
)}
{activeTab === ActiveTab.export && currentWorkspace && (
<ExportPage workspace={currentWorkspace} />
)}
</StyledSettingContent>
</StyledSettingContainer>
</Modal>
);
};
@@ -1,62 +1,37 @@
import { import {
StyledDeleteButtonContainer, StyledDeleteButtonContainer,
StyledSettingAvatar, // StyledSettingAvatar,
StyledSettingAvatarContent, StyledSettingAvatarContent,
StyledSettingInputContainer, StyledSettingInputContainer,
} from './style'; } from './style';
import { StyledSettingH2 } from '../style'; import { StyledSettingH2 } from '../style';
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '@/ui/button'; import { Button, TextButton } from '@/ui/button';
import Input from '@/ui/input'; import Input from '@/ui/input';
import { getDataCenter, Workspace, WorkspaceType } from '@affine/datacenter'; // import { useAppState } from '@/providers/app-state-provider';
import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceDetails } from '@/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
import { WorkspaceDelete } from './delete'; import { WorkspaceDelete } from './delete';
import { Workspace as StoreWorkspace } from '@blocksuite/store'; // import { debounce } from '@/utils';
import { debounce } from '@/utils';
import { WorkspaceLeave } from './leave'; import { WorkspaceLeave } from './leave';
import { Upload } from '@/components/file-upload'; import { Upload } from '@/components/file-upload';
import { Workspace } from '@/hooks/mock-data/mock';
export const GeneralPage = ({ import { WorkspaceAvatar } from '@/components/workspace-avatar';
workspace, import { useTemporaryHelper } from '@/providers/temporary-helper-provider';
owner, export const GeneralPage = ({ workspace }: { workspace: Workspace }) => {
}: { // const { refreshWorkspacesMeta } = useAppState();
workspace: Workspace; const { updateWorkspaceMeta } = useTemporaryHelper();
owner: WorkspaceDetails[string]['owner'];
workspaces: Record<string, StoreWorkspace | null>;
}) => {
const {
user,
currentWorkspace,
workspacesMeta,
workspaces,
refreshWorkspacesMeta,
} = useAppState();
const [showDelete, setShowDelete] = useState<boolean>(false); const [showDelete, setShowDelete] = useState<boolean>(false);
const [showLeave, setShowLeave] = useState<boolean>(false); const [showLeave, setShowLeave] = useState<boolean>(false);
const [uploading, setUploading] = useState<boolean>(false); const [uploading, setUploading] = useState<boolean>(false);
const [workspaceName, setWorkspaceName] = useState<string>( const [workspaceName, setWorkspaceName] = useState<string>('');
workspaces[workspace.id]?.meta.name || // const debouncedRefreshWorkspacesMeta = debounce(() => {
(workspace.type === WorkspaceType.Private && user ? user.name : '') // refreshWorkspacesMeta();
); // }, 100);
const debouncedRefreshWorkspacesMeta = debounce(() => { const isOwner = true;
refreshWorkspacesMeta();
}, 100);
const isOwner = user && owner.id === user.id;
const handleChangeWorkSpaceName = (newName: string) => { const handleChangeWorkSpaceName = (newName: string) => {
setWorkspaceName(newName); 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 = () => { const handleClickDelete = () => {
setShowDelete(true); setShowDelete(true);
}; };
@@ -70,9 +45,13 @@ export const GeneralPage = ({
const handleCloseLeave = () => { const handleCloseLeave = () => {
setShowLeave(false); setShowLeave(false);
}; };
const handleUpdateWorkspaceName = () => {
workspace && updateWorkspaceMeta(workspace.id, { name: workspaceName });
};
const fileChange = async (file: File) => { const fileChange = async (file: File) => {
// setUploading(true); console.log('file: ', file);
setUploading(true);
// const blob = new Blob([file], { type: file.type }); // const blob = new Blob([file], { type: file.type });
// const blobId = await getDataCenter() // const blobId = await getDataCenter()
// .then(dc => dc.apis.uploadBlob({ blob })) // .then(dc => dc.apis.uploadBlob({ blob }))
@@ -81,7 +60,7 @@ export const GeneralPage = ({
// }); // });
// if (blobId) { // if (blobId) {
// currentWorkspace?.meta.setAvatar(blobId); // currentWorkspace?.meta.setAvatar(blobId);
// workspaces[workspace.id]?.meta.setAvatar(blobId); // // workspaces[workspace.id]?.meta.setAvatar(blobId);
// setUploading(false); // setUploading(false);
// debouncedRefreshWorkspacesMeta(); // debouncedRefreshWorkspacesMeta();
// } // }
@@ -89,18 +68,17 @@ export const GeneralPage = ({
return workspace ? ( return workspace ? (
<div> <div>
<StyledSettingH2 marginTop={56}>Workspace Avatar</StyledSettingH2> <StyledSettingH2 marginTop={56}>Workspace Icon</StyledSettingH2>
<StyledSettingAvatarContent> <StyledSettingAvatarContent>
<StyledSettingAvatar <div
alt="workspace avatar" style={{
src={ float: 'left',
workspaces[workspace.id]?.meta.avatar
? '/api/blob/' + workspaces[workspace.id]?.meta.avatar marginRight: '5px',
: '' }}
}
> >
{workspaces[workspace.id]?.meta.name[0]} <WorkspaceAvatar size={60} name={workspace.name} />
</StyledSettingAvatar> </div>
<Upload <Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg" accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange} fileChange={fileChange}
@@ -108,31 +86,45 @@ export const GeneralPage = ({
<Button loading={uploading}>Upload</Button> <Button loading={uploading}>Upload</Button>
</Upload> </Upload>
{/* TODO: add upload logic */} {/* TODO: add upload logic */}
{/* {isOwner ? (
<StyledAvatarUploadBtn shape="round">upload</StyledAvatarUploadBtn>
) : null} */}
{/* <Button shape="round">remove</Button> */}
</StyledSettingAvatarContent> </StyledSettingAvatarContent>
<StyledSettingH2 marginTop={36}>Workspace Name</StyledSettingH2> <StyledSettingH2 marginTop={20}>Workspace Name</StyledSettingH2>
<StyledSettingInputContainer> <StyledSettingInputContainer>
<Input <Input
width={327} width={327}
value={workspaceName} value={workspaceName}
placeholder="Workspace Name" placeholder="Workspace Name"
disabled={!isOwner}
maxLength={14} maxLength={14}
minLength={1} minLength={1}
onChange={handleChangeWorkSpaceName} onChange={handleChangeWorkSpaceName}
></Input> ></Input>
<TextButton
onClick={() => {
handleUpdateWorkspaceName();
}}
style={{ marginLeft: '10px' }}
>
</TextButton>
</StyledSettingInputContainer> </StyledSettingInputContainer>
<StyledSettingH2 marginTop={36}>Workspace Owner</StyledSettingH2> {/* {userInfo ? (
<div>
<StyledSettingH2 marginTop={20}>Workspace Owner</StyledSettingH2>
<StyledSettingInputContainer>
<Input
width={327}
disabled
value={userInfo?.name}
placeholder="Workspace Owner"
></Input>
</StyledSettingInputContainer>
</div>
) : (
''
)} */}
<StyledSettingH2 marginTop={20}>Workspace Type</StyledSettingH2>
<StyledSettingInputContainer> <StyledSettingInputContainer>
<Input <code>{workspace.type} </code>
width={327}
disabled
value={owner.name}
placeholder="Workspace Owner"
></Input>
</StyledSettingInputContainer> </StyledSettingInputContainer>
<StyledDeleteButtonContainer> <StyledDeleteButtonContainer>
{isOwner ? ( {isOwner ? (
@@ -143,9 +135,7 @@ export const GeneralPage = ({
<WorkspaceDelete <WorkspaceDelete
open={showDelete} open={showDelete}
onClose={handleCloseDelete} onClose={handleCloseDelete}
workspaceName={workspaceName} workspace={workspace}
workspaceId={workspace.id}
nextWorkSpaceId={nextWorkSpaceId}
/> />
</> </>
) : ( ) : (
@@ -158,7 +148,6 @@ export const GeneralPage = ({
onClose={handleCloseLeave} onClose={handleCloseLeave}
workspaceName={workspaceName} workspaceName={workspaceName}
workspaceId={workspace.id} workspaceId={workspace.id}
nextWorkSpaceId={nextWorkSpaceId}
/> />
</> </>
)} )}
@@ -11,27 +11,25 @@ import {
import { useState } from 'react'; import { useState } from 'react';
import { ModalCloseButton } from '@/ui/modal'; import { ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { getDataCenter } from '@affine/datacenter';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useAppState } from '@/providers/app-state-provider'; import {
deleteWorkspace,
getWorkspaces,
Workspace,
} from '@/hooks/mock-data/mock';
interface WorkspaceDeleteProps { interface WorkspaceDeleteProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
workspaceName: string; workspace: Workspace;
workspaceId: string;
nextWorkSpaceId: string;
} }
export const WorkspaceDelete = ({ export const WorkspaceDelete = ({
open, open,
onClose, onClose,
workspaceId, workspace,
workspaceName,
nextWorkSpaceId,
}: WorkspaceDeleteProps) => { }: WorkspaceDeleteProps) => {
const [deleteStr, setDeleteStr] = useState<string>(''); const [deleteStr, setDeleteStr] = useState<string>('');
const { refreshWorkspacesMeta } = useAppState();
const router = useRouter(); const router = useRouter();
const handlerInputChange = (workspaceName: string) => { const handlerInputChange = (workspaceName: string) => {
@@ -42,8 +40,14 @@ export const WorkspaceDelete = ({
// const dc = await getDataCenter(); // const dc = await getDataCenter();
// await dc.apis.deleteWorkspace({ id: workspaceId }); // await dc.apis.deleteWorkspace({ id: workspaceId });
// router.push(`/workspace/${nextWorkSpaceId}`); // router.push(`/workspace/${nextWorkSpaceId}`);
// refreshWorkspacesMeta(); deleteWorkspace(workspace.id);
// onClose(); const workspaceList = getWorkspaces();
if (workspaceList.length) {
router.push(`/workspace/${workspaceList[0].id}`);
} else {
router.push(`/workspace`);
}
onClose();
}; };
return ( return (
@@ -51,11 +55,20 @@ export const WorkspaceDelete = ({
<StyledModalWrapper> <StyledModalWrapper>
<ModalCloseButton onClick={onClose} /> <ModalCloseButton onClick={onClose} />
<StyledModalHeader>Delete Workspace</StyledModalHeader> <StyledModalHeader>Delete Workspace</StyledModalHeader>
<StyledTextContent> {workspace.type === 'local' ? (
This action cannot be undone. This will permanently delete ( <StyledTextContent>
<StyledWorkspaceName>{workspaceName}</StyledWorkspaceName>) along with Deleting (
all its content. <StyledWorkspaceName>{workspace.name}</StyledWorkspaceName>) cannot
</StyledTextContent> be undone, please proceed with caution. along with all its content.
</StyledTextContent>
) : (
<StyledTextContent>
Deleting (
<StyledWorkspaceName>{workspace.name}</StyledWorkspaceName>) will
delete both local and cloud data, this operation cannot be undone,
please proceed with caution.
</StyledTextContent>
)}
<StyledInputContent> <StyledInputContent>
<Input <Input
onChange={handlerInputChange} onChange={handlerInputChange}
@@ -1 +1 @@
export * from './delete'; export * from './Delete';
@@ -16,7 +16,8 @@ export const StyledModalHeader = styled('div')(({ theme }) => {
width: '460px', width: '460px',
fontWeight: '600', fontWeight: '600',
fontSize: '20px;', fontSize: '20px;',
textAlign: 'center', textAlign: 'left',
paddingLeft: '20px',
color: theme.colors.popoverColor, color: theme.colors.popoverColor,
}; };
}); });
@@ -32,7 +33,7 @@ export const StyledTextContent = styled('div')(() => {
fontWeight: '400', fontWeight: '400',
fontSize: '18px', fontSize: '18px',
lineHeight: '26px', lineHeight: '26px',
textAlign: 'center', textAlign: 'left',
}; };
}); });
@@ -41,7 +42,7 @@ export const StyledInputContent = styled('div')(() => {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
margin: '40px 0 24px 0', margin: '20px 0 24px 0',
}; };
}); });
@@ -1 +1 @@
export * from './general'; export * from './General';
@@ -7,8 +7,7 @@ import {
} from './style'; } from './style';
import { ModalCloseButton } from '@/ui/modal'; import { ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { getDataCenter } from '@affine/datacenter'; // import { getDataCenter } from '@affine/datacenter';
import { useRouter } from 'next/router';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
interface WorkspaceDeleteProps { interface WorkspaceDeleteProps {
@@ -16,22 +15,20 @@ interface WorkspaceDeleteProps {
onClose: () => void; onClose: () => void;
workspaceName: string; workspaceName: string;
workspaceId: string; workspaceId: string;
nextWorkSpaceId: string;
} }
export const WorkspaceLeave = ({ export const WorkspaceLeave = ({
open, open,
onClose, onClose,
nextWorkSpaceId,
workspaceId, workspaceId,
}: WorkspaceDeleteProps) => { }: WorkspaceDeleteProps) => {
const router = useRouter(); // const router = useRouter();
const { refreshWorkspacesMeta } = useAppState(); const { refreshWorkspacesMeta } = useAppState();
const handleLeave = async () => { const handleLeave = async () => {
// const dc = await getDataCenter(); // const dc = await getDataCenter();
// await dc.apis.leaveWorkspace({ id: workspaceId }); // await dc.apis.leaveWorkspace({ id: workspaceId });
router.push(`/workspace/${nextWorkSpaceId}`); // // router.push(`/workspace/${nextWorkSpaceId}`);
refreshWorkspacesMeta(); // refreshWorkspacesMeta();
onClose(); onClose();
}; };
@@ -1 +1 @@
export * from './leave'; export * from './Leave';
@@ -9,7 +9,7 @@ export const StyledSettingInputContainer = styled('div')(() => {
export const StyledDeleteButtonContainer = styled('div')(() => { export const StyledDeleteButtonContainer = styled('div')(() => {
return { return {
marginTop: '154px', marginTop: '30px',
}; };
}); });
@@ -26,3 +26,12 @@ export const StyledSettingAvatarContent = styled('div')(() => {
export const StyledSettingAvatar = styled(MuiAvatar)(() => { export const StyledSettingAvatar = styled(MuiAvatar)(() => {
return { height: '72px', width: '72px', marginRight: '24px' }; return { height: '72px', width: '72px', marginRight: '24px' };
}); });
export const StyledMemberWarp = styled('div')(() => {
return {
display: 'flex',
height: '500px',
flexDirection: 'column',
padding: '60px 0',
};
});
@@ -1 +1 @@
export * from './workspace-setting'; export * from './WorkspaceSetting';
@@ -117,6 +117,7 @@ export const StyledMemberTitleContainer = styled('div')(() => {
display: 'flex', display: 'flex',
marginTop: '60px', marginTop: '60px',
fontWeight: '500', fontWeight: '500',
flex: 1,
}; };
}); });
@@ -199,11 +200,12 @@ export const StyledMoreVerticalButton = styled('button')(() => {
export const StyledPublishExplanation = styled('div')(() => { export const StyledPublishExplanation = styled('div')(() => {
return { return {
marginTop: '56px',
paddingRight: '48px', paddingRight: '48px',
fontWeight: '500', fontWeight: '500',
fontSize: '18px', fontSize: '18px',
lineHeight: '26px', lineHeight: '26px',
flex: 1,
marginTop: '60px',
}; };
}); });
@@ -213,7 +215,8 @@ export const StyledPublishCopyContainer = styled('div')(() => {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
height: '38px', marginBottom: '20px',
paddingTop: '20px',
}; };
}); });
@@ -226,5 +229,7 @@ export const StyledCopyButtonContainer = styled('div')(() => {
export const StyledPublishContent = styled('div')(() => { export const StyledPublishContent = styled('div')(() => {
return { return {
height: '494px', height: '494px',
display: 'flex',
flexDirection: 'column',
}; };
}); });
@@ -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>
);
};
@@ -13,7 +13,7 @@ import {
import { WorkspaceSetting } from '@/components/workspace-setting'; import { WorkspaceSetting } from '@/components/workspace-setting';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { getDataCenter, WorkspaceType } from '@affine/datacenter'; import { getDataCenter, WorkspaceType } from '@affine/datacenter';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/GlobalModalProvider';
export type WorkspaceDetails = Record< export type WorkspaceDetails = Record<
string, string,
@@ -27,8 +27,7 @@ type SelectorPopperContentProps = {
export const SelectorPopperContent = ({ export const SelectorPopperContent = ({
isShow, isShow,
}: SelectorPopperContentProps) => { }: SelectorPopperContentProps) => {
const { user, workspacesMeta, workspaces, refreshWorkspacesMeta } = const { user, workspacesMeta, refreshWorkspacesMeta } = useAppState();
useAppState();
const [settingWorkspaceId, setSettingWorkspaceId] = useState<string | null>( const [settingWorkspaceId, setSettingWorkspaceId] = useState<string | null>(
null null
); );
@@ -115,13 +114,9 @@ export const SelectorPopperContent = ({
type={workspace.type} type={workspace.type}
key={workspace.id} key={workspace.id}
id={workspace.id} id={workspace.id}
icon={ icon={`loading...`}
(workspaces[workspace.id]?.meta.avatar &&
`/api/blob/${workspaces[workspace.id]?.meta.avatar}`) ||
`loading...`
}
onClickSetting={handleClickSettingWorkspace} onClickSetting={handleClickSettingWorkspace}
name={workspaces[workspace.id]?.meta.name || `loading...`} name={`loading...`}
memberCount={workSpaceDetails[workspace.id]?.memberCount || 1} memberCount={workSpaceDetails[workspace.id]?.memberCount || 1}
/> />
) : null; ) : null;
@@ -130,16 +125,16 @@ export const SelectorPopperContent = ({
<CreateWorkspaceItem /> <CreateWorkspaceItem />
{settingWorkspace ? ( {settingWorkspace ? (
<WorkspaceSetting <WorkspaceSetting
isShow={Boolean(settingWorkspaceId)} isShow={false}
onClose={handleCloseWorkSpace} onClose={handleCloseWorkSpace}
workspace={settingWorkspace} // workspace={settingWorkspace}
owner={ // owner={
(settingWorkspaceId && // (settingWorkspaceId &&
workSpaceDetails[settingWorkspaceId]?.owner) || { // workSpaceDetails[settingWorkspaceId]?.owner) || {
id: user.id, // id: user.id,
name: user.name, // name: user.name,
} // }
} // }
/> />
) : null} ) : null}
<StyledDivider /> <StyledDivider />
@@ -1 +1 @@
export * from './workspace-create'; export * from './WorkspaceCreate';
@@ -1,6 +1,6 @@
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/GlobalModalProvider';
import { styled } from '@/styles'; import { styled } from '@/styles';
import { AffineIcon } from '../../icons/icons'; import { AffineIcon } from '../../icons/Icons';
import { import {
WorkspaceItemAvatar, WorkspaceItemAvatar,
LoginItemWrapper, LoginItemWrapper,
@@ -1,53 +1,52 @@
import { Popper } from '@/ui/popper';
import { Avatar, WorkspaceName, SelectorWrapper } from './styles'; import { Avatar, WorkspaceName, SelectorWrapper } from './styles';
import { SelectorPopperContent } from './SelectorPopperContent'; import { useEffect, useState } from 'react';
import { useState } from 'react'; import { WorkspaceModal } from '@/components/workspace-modal';
import { useAppState } from '@/providers/app-state-provider'; import { WorkspaceAvatar } from '@/components/workspace-avatar';
import { WorkspaceType } from '@affine/datacenter'; import { useTemporaryHelper } from '@/providers/temporary-helper-provider';
import { AffineIcon } from '../icons/icons';
export const WorkspaceSelector = () => { export const WorkspaceSelector = () => {
const [isShow, setIsShow] = useState(false); const [workspaceListShow, setWorkspaceListShow] = useState(false);
const { currentWorkspace, workspacesMeta, currentWorkspaceId, user } = const { currentWorkspace, workspaceMetaList } = useTemporaryHelper();
useAppState();
const workspaceMeta = workspacesMeta.find( useEffect(() => {
meta => String(meta.id) === String(currentWorkspaceId) if (workspaceMetaList.length === 0) {
); setWorkspaceListShow(true);
}
}, [workspaceMetaList]);
return ( return (
<Popper <>
content={<SelectorPopperContent isShow={isShow} />} <SelectorWrapper
zIndex={1000} onClick={() => {
placement="bottom-start" setWorkspaceListShow(true);
trigger="click" }}
onVisibleChange={setIsShow} data-testid="current-workspace"
> >
<SelectorWrapper data-testid="current-workspace">
<Avatar <Avatar
alt="Affine" alt="Affine"
data-testid="workspace-avatar" data-testid="workspace-avatar"
src={ src={currentWorkspace?.avatar}
workspaceMeta?.type === WorkspaceType.Private
? user
? user.avatar_url
: ''
: currentWorkspace?.meta.avatar &&
`/api/blob/${currentWorkspace?.meta.avatar}`
}
> >
{workspaceMeta?.type === WorkspaceType.Private && user ? ( <div
user?.name[0] style={{
) : ( float: 'left',
<AffineIcon /> }}
)} >
<WorkspaceAvatar
size={28}
name={currentWorkspace?.name ?? 'AFFiNE'}
/>
</div>
</Avatar> </Avatar>
<WorkspaceName data-testid="workspace-name"> <WorkspaceName data-testid="workspace-name">
{workspaceMeta?.type === WorkspaceType.Private {currentWorkspace?.name ?? 'AFFiNE'}
? user
? user.name
: 'AFFiNE'
: currentWorkspace?.meta.name || 'AFFiNE'}
</WorkspaceName> </WorkspaceName>
</SelectorWrapper> </SelectorWrapper>
</Popper> <WorkspaceModal
open={workspaceListShow}
onClose={() => {
setWorkspaceListShow(false);
}}
></WorkspaceModal>
</>
); );
}; };
@@ -21,15 +21,17 @@ import {
ImportIcon, ImportIcon,
TrashIcon, TrashIcon,
AddIcon, AddIcon,
SettingsIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import Link from 'next/link'; import Link from 'next/link';
import { Tooltip } from '@/ui/tooltip'; import { Tooltip } from '@/ui/tooltip';
import { useModal } from '@/providers/global-modal-provider'; import { useModal } from '@/providers/GlobalModalProvider';
import { useAppState } from '@/providers/app-state-provider/context'; import { useAppState } from '@/providers/app-state-provider/context';
import { IconButton } from '@/ui/button'; import { IconButton } from '@/ui/button';
import useLocalStorage from '@/hooks/use-local-storage'; import useLocalStorage from '@/hooks/use-local-storage';
import usePageMetaList from '@/hooks/use-page-meta-list'; import usePageMetaList from '@/hooks/use-page-meta-list';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { WorkspaceSetting } from '@/components/workspace-setting';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const FavoriteList = ({ showList }: { showList: boolean }) => { const FavoriteList = ({ showList }: { showList: boolean }) => {
@@ -74,6 +76,8 @@ export const WorkSpaceSliderBar = () => {
const [showTip, setShowTip] = useState(false); const [showTip, setShowTip] = useState(false);
const [show, setShow] = useLocalStorage('AFFiNE_SLIDE_BAR', false, true); const [show, setShow] = useLocalStorage('AFFiNE_SLIDE_BAR', false, true);
const [showWorkspaceSetting, setShowWorkspaceSetting] = useState(false);
const paths = { const paths = {
all: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/all` : '', all: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/all` : '',
favorite: currentWorkspaceId favorite: currentWorkspaceId
@@ -146,6 +150,20 @@ export const WorkSpaceSliderBar = () => {
</IconButton> </IconButton>
</StyledListItem> </StyledListItem>
<FavoriteList showList={showSubFavorite} /> <FavoriteList showList={showSubFavorite} />
<StyledListItem
onClick={() => {
setShowWorkspaceSetting(true);
}}
>
<SettingsIcon /> Setting
</StyledListItem>
<WorkspaceSetting
isShow={showWorkspaceSetting}
onClose={() => {
setShowWorkspaceSetting(false);
}}
/>
<StyledListItem <StyledListItem
onClick={() => { onClick={() => {
@@ -62,6 +62,7 @@ export const StyledListItem = styled.div<{
color: active ? theme.colors.primaryColor : theme.colors.popoverColor, color: active ? theme.colors.primaryColor : theme.colors.popoverColor,
paddingLeft: '12px', paddingLeft: '12px',
borderRadius: '5px', borderRadius: '5px',
cursor: 'pointer',
...displayFlex('flex-start', 'center'), ...displayFlex('flex-start', 'center'),
...(disabled ...(disabled
? { ? {
+160
View File
@@ -0,0 +1,160 @@
export interface Workspace {
name: string; // 名称
id: string; //唯一标识
isPublish?: boolean; // 是否公开
isLocal?: boolean; // 是否全部数据都在本地
avatar?: string; // 封面
type: 'local' | 'cloud' | 'join'; // cloud: 云端(本次暂不支持),local: 本地,join : 加入别人的
workspaceOwner?: User; // 本地工作空间的拥有者
}
export interface User {
name: string;
id: string;
email: string;
avatar: string;
}
export function updateWorkspaceMeta(
workspaceId: string,
workspaceData: {
name?: string;
avatar?: string;
type?: 'local' | 'cloud' | 'join';
}
) {
const workspacesMeta = getWorkspaces();
const newWorkspacesMeta = workspacesMeta.map((workspace: Workspace) => {
if (workspace.id === workspaceId) {
workspaceData.name && (workspace.name = workspaceData.name);
workspaceData.avatar && (workspace.avatar = workspaceData.avatar);
workspaceData.type && (workspace.type = workspaceData.type);
return workspaceData;
}
return workspace;
});
localStorage.setItem('affine-workspace', JSON.stringify(newWorkspacesMeta));
const activeWorkspace = getActiveWorkspace();
workspaceData.name && (activeWorkspace.name = workspaceData.name);
workspaceData.avatar && (activeWorkspace.avatar = workspaceData.avatar);
workspaceData.type && (activeWorkspace.type = workspaceData.type);
console.log(workspaceData);
setActiveWorkspace(activeWorkspace);
}
export function createWorkspace(workspaceName: string) {
const workspaceId = 'workspace-' + Date.now();
const workspaceData = {
name: workspaceName,
id: workspaceId,
isPublish: false,
isLocal: true,
avatar: '',
type: 'local',
} as Workspace;
const workspacesMeta = getWorkspaces();
workspacesMeta.push(workspaceData);
localStorage.setItem('affine-workspace', JSON.stringify(workspacesMeta));
setActiveWorkspace(workspaceData);
return { workspaceId };
}
export function getWorkspaces(): Workspace[] {
const workspacesMeta = JSON.parse(
localStorage.getItem('affine-workspace') ?? '[]'
);
return workspacesMeta;
}
export function deleteWorkspace(workspaceId: string) {
const workspacesMeta = getWorkspaces();
const newWorkspacesMeta = workspacesMeta.filter(() => {
return workspaceId !== workspaceId;
});
localStorage.setItem('affine-workspace', JSON.stringify(newWorkspacesMeta));
}
export function getMembers(id: string): User[] {
const memberMap = JSON.parse(localStorage.getItem('affine-member') ?? '{}');
return memberMap[id] || [];
}
export function setMember(workspaceId: string, member: User) {
const memberMap = JSON.parse(localStorage.getItem('affine-member') ?? '{}');
memberMap[workspaceId] = memberMap[workspaceId] || [];
memberMap[workspaceId].push(member);
localStorage.setItem('affine-member', JSON.stringify(memberMap));
}
export function deleteMember(workspaceId: string, index: number) {
const memberMap = JSON.parse(localStorage.getItem('affine-member') ?? '{}');
const memberList = memberMap[workspaceId];
memberList.splice(index, 1);
memberMap[workspaceId] = memberList;
localStorage.setItem('affine-member', JSON.stringify(memberMap));
}
export function leaveWorkspace() {
return true;
}
export function setWorkspacePublish(id: string, isPublish: boolean): boolean {
const workspacesMeta = getWorkspaces();
const newWorkspacesMeta = workspacesMeta.map((workspace: Workspace) => {
if (workspace.id === id) {
workspace.isPublish = isPublish;
}
return workspace;
});
localStorage.setItem('affine-workspace', JSON.stringify(newWorkspacesMeta));
return isPublish;
}
export function getWorkspaceById(id: string) {
const workspacesMeta = getWorkspaces();
return workspacesMeta.find((workspace: Workspace) => {
return workspace.id === id;
});
}
export function getPagesByWorkspaceId(workspaceId: string) {
if (!workspaceId) return [];
const workspacesMeta = [];
for (let i = 0; i < 10; i++) {
workspacesMeta.push({
id: 'page-' + i,
name: 'page ' + i,
});
}
}
export function getActiveWorkspace(): Workspace {
return JSON.parse(localStorage.getItem('affine-active-workspace') ?? '{}');
}
export function setActiveWorkspace(workspaceData: Workspace) {
console.log('workspaceData: ', workspaceData);
localStorage.setItem(
'affine-active-workspace',
JSON.stringify(workspaceData)
);
}
export function getUserInfo(): User {
return JSON.parse(localStorage.getItem('affine-user') ?? 'null');
}
export function Login(): void {
localStorage.setItem(
'affine-user',
JSON.stringify({
name: 'Diamond',
id: 'ttt',
email: 'diamond.shx@gmail.com',
avatar: 'string',
})
);
}
export function SignOut(): void {
localStorage.removeItem('affine-user');
}
+1 -22
View File
@@ -1,22 +1 @@
{ {}
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
"Add A Below Block": "নীচে একটি ব্লক যোগ করুন",
"WarningTips": {
"IsNotfsApiSupported": "অ্যাফাইন ডেমোতে স্বাগতম। পরিবর্তনগুলি সংরক্ষণ করা শুরু করতে আপনি Chrome/Edge এর মতো ক্রোমিয়াম ভিত্তিক ব্রাউজারের সর্বশেষ সংস্করণের মাধ্যমে ডিস্কে ডেটা সিঙ্ক করতে পারেন",
"DoNotStore": "অ্যাফাইন সক্রিয় ডেভেলপমেন্ট এর অধীনে এবং বর্তমান সংস্করণটি অস্থিতিশীল। দয়া করে কোন তথ্য বা ডেটা সঞ্চয় করবেন না"
},
"Language": "ভাষা",
"Settings": "সেটিংস",
"Share": "শেয়ার করুন",
"Comment": "মন্তব্য",
"Delete": "মুছে ফেলুন",
"Copy Page Link": "পেজ লিংক কপি করুন",
"Duplicate Page": "সদৃশ পৃষ্ঠা তৈরি করুন",
"Logout": "লগআউট",
"Divide Here As A New Group": "একটি নতুন গ্রুপ হিসেবে বিভক্ত করুন",
"ComingSoon": "লেআউট সেটিংস শীঘ্রই আসছে...",
"Clear Workspace": "ওয়ার্কস্পেস পরিষ্কার করুন",
"Layout": "লেআউট",
"Turn into": "রূপান্তর করুন",
"Sync to Disk": "ডিস্ক এ সিঙ্ক করুন"
}
+17 -3
View File
@@ -34,7 +34,7 @@
"Delete page?": "Delete page?", "Delete page?": "Delete page?",
"Delete permanently?": "Delete permanently?", "Delete permanently?": "Delete permanently?",
"will be moved to Trash": "{{title}} will be moved to Trash", "will be moved to Trash": "{{title}} will be moved to Trash",
"Once deleted, you can't undo this action.": "Once deleted,you can't undo this action.", "Once deleted, you can't undo this action.": "Once deleted, you can't undo this action.",
"Moved to Trash": "Moved to Trash", "Moved to Trash": "Moved to Trash",
"Permanently deleted": "Permanently deleted", "Permanently deleted": "Permanently deleted",
"restored": "{{title}} restored", "restored": "{{title}} restored",
@@ -54,12 +54,26 @@
"Strikethrough": "Strikethrough", "Strikethrough": "Strikethrough",
"Inline code": "Inline code", "Inline code": "Inline code",
"Code block": "Code block", "Code block": "Code block",
"Link": "Link", "Hyperlink(with selected text)": "Hyperlink(with selected text)",
"Body text": "Body text", "Body text": "Body text",
"Heading": "Heading {{number}}", "Heading": "Heading {{number}}",
"Increase indent": "Increase indent", "Increase indent": "Increase indent",
"Reduce indent": "Reduce indent", "Reduce indent": "Reduce indent",
"Markdown Syntax": "Markdown Syntax", "Markdown Syntax": "Markdown Syntax",
"Divider": "Divider", "Divider": "Divider",
"404 - Page Not Found": "404 - Page Not Found" "404 - Page Not Found": "404 - Page Not Found",
"New Workspace": "New Workspace",
"Workspace description": "Workspace is your virtual space to capture, create and plan as just one person or together as a team.",
"Create": "Create",
"Select": "Select",
"Text": "Text (coming soon)",
"Shape": "Shape",
"Sticky": "Sticky (coming soon)",
"Pen": "Pen (coming soon)",
"Connector": "Connector (coming soon)",
"Upload": "Upload",
"Restore it": "Restore it",
"TrashButtonGroupTitle": "Permanently delete",
"TrashButtonGroupDescription": "Once deleted, you can't undo this action. Do you confirm?",
"Delete permanently": "Delete permanently"
} }
+1 -29
View File
@@ -1,29 +1 @@
{ {}
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
"ComingSoon": "Bientôt disponible",
"Duplicate Page": "Dupliquer la page",
"Copy Page Link": "Copier le lien de la page",
"Delete": "Supprimer",
"Comment": "Commentaire",
"Export As HTML": "Exporter en HTML",
"Export As Markdown": "Exporter en Markdown",
"Export As PDF (Unsupported)": "exporter en PDF (non supporté)",
"Logout": "Déconnexion",
"Export Workspace": "Exporter l'espace de travail",
"Import Workspace": "Importer l'espace de travail",
"Language": "Langue",
"Last edited by": "Dernière édition par {{name}}",
"Layout": "Mise en forme",
"Settings": "Réglages",
"Share": "Partager",
"Sync to Disk": "Synchroniser sur le disque",
"Turn into": "Transformer en",
"WarningTips": {
"DoNotStore": "Affine est en développement actif ; la version actuelle est INSTABLE. Veuillez NE PAS stocker d'informations ou de données",
"IsNotLocalWorkspace": "Bienvenue sur la démo d'AFFiNE. Pour commencer à sauvegarder vos modifications, vous pouvez SYNCHRONISER SUR LE DISQUE",
"IsNotfsApiSupported": "Bienvenue sur la démo d'AFFiNE. Pour commencer à sauvegarder vos modifications, vous pouvez SYNCHRONISER SUR LE DISQUE\navec la dernière version d'un navigateur basé sur Chromium tel que Chrome ou Edge."
},
"Add A Below Block": "Ajouter un bloc en-dessous",
"Divide Here As A New Group": "Séparer ici en un nouveau groupe",
"Clear Workspace": "Vider l'espace de travail"
}
+1 -27
View File
@@ -1,27 +1 @@
{ {}
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
"Clear Workspace": "Očisti radni prostor",
"ComingSoon": "Podešavanja za izgled dolaze",
"Comment": "Komentar",
"Copy Page Link": "Kopiraj link stranice",
"Delete": "Obriši",
"Duplicate Page": "Dupliraj stranicu",
"Export As HTML": "Izvezi kao HTML",
"Export As Markdown": "Izvezi kao Markdown",
"Export As PDF (Unsupported)": "Izvezi kao PDF (nepodržano)",
"Export Workspace": "Izvezi radnu površinu",
"Import Workspace": "Poboljšaj radnu površinu",
"Language": "Jezik",
"Last edited by": "Zadnju promenu uradio {{ime}}",
"Layout": "Izgled",
"Logout": "Odjava",
"Settings": "Podešavanja",
"Share": "Podeli",
"Sync to Disk": "Sinhroniziraj sa diskom",
"Turn into": "Promeni u",
"WarningTips": {
"DoNotStore": "AFFiNE je u stanju aktivnog razvoja i trenutna verzija je NESTABILNA. Molimo vas, NEMOJTE čuvati informacije ili podatke.",
"IsNotLocalWorkspace": "Dobrodošli u AFFiNE demo. Da bi započeli proces čuvanja promena možete kliknuti SINHRONIZUJ SA DISKOM.",
"IsNotfsApiSupported": "Dobrodošli u AFFiNE demo. Da bi započeli proces čuvanja promena možete SINHRONIZOVATI NA DISK sa poslednjom verzijom pretraživača tipa Chromium, kao što su Chrome/Edge."
}
}
@@ -1,29 +1,65 @@
{ {
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "", "Quick search": "快速搜索",
"Sync to Disk": "同步到磁盘", "All pages": "全部页面",
"Share": "分享", "Favourites": "收藏夹",
"WarningTips": { "No item": "没有项目",
"IsNotfsApiSupported": "欢迎来到AFFiNE 的演示界面。您可以使用最新版本的基于Chrome的浏览器(如Chrome/Edge)将数据同步到磁盘来进行保存", "Import": "导入",
"IsNotLocalWorkspace": "欢迎来到AFFiNE 的演示界面,您可以同步到磁盘来进行保存操作。", "Trash": "回收站",
"DoNotStore": "AFFiNE 正在积极开发中,当前版本不稳定。请不要存储信息或数据。" "New Page": "新建文章",
}, "New Keyword Page": "新建 '{{query}}' 为标题的文章",
"ComingSoon": "布局设置即将到来", "Find 0 result": "找到 0 个结果",
"Layout": "布局", "Find results": "找到 {{number}} 个结果",
"Comment": "评论", "Collapse sidebar": "关闭侧边栏",
"Settings": "设置", "Expand sidebar": "展开侧边栏",
"Duplicate Page": "复制页面", "Removed from Favourites": "已从收藏中移除",
"Copy Page Link": "复制页面链接", "Remove from favourites": "从收藏中移除",
"Language": "当前语言", "Added to Favourites": "已添加到收藏",
"Clear Workspace": "清空工作区域", "Add to favourites": "添加到收藏",
"Export As Markdown": "导出 markdown", "Paper": "文章",
"Export As HTML": "导出 HTML", "Edgeless": "无边模式",
"Export As PDF (Unsupported)": "导出 PDF (暂不支持)", "Switch to": "跳转到",
"Import Workspace": "导入 Workspace", "Convert to ": "转换成 ",
"Export Workspace": "导出 Workspace", "Page": "文章",
"Last edited by": "最后编辑者为 {{name}}", "Export": "导出",
"Logout": "退出登录", "Export to HTML": "导出到 HTML",
"Export to Markdown": "导出到 Markdown",
"Delete": "删除", "Delete": "删除",
"Turn into": "转换为", "Title": "标题",
"Add A Below Block": "在下方添加一个新块", "Untitled": "无标题",
"Divide Here As A New Group": "从这里划分一个新组" "Created": "创建时间",
"Updated": "更新时间",
"Open in new tab": "在新页面打开",
"Favourite": "收藏",
"Favourited": "已收藏",
"Delete page?": "删除文章?",
"Delete permanently?": "永久删除?",
"will be moved to Trash": "{{title}} 将被移动到回收站",
"Once deleted, you can't undo this action.": "一次性删除,无法恢复。",
"Moved to Trash": "已移动到回收站",
"Permanently deleted": "已永久删除",
"restored": "{{title}} 已恢复",
"Cancel": "取消",
"Keyboard Shortcuts": "快捷键",
"Contact Us": "联系我们",
"Official Website": "官网",
"Get in touch!": "Get in touch!",
"AFFiNE Community": "AFFiNE Community",
"How is AFFiNE Alpha different?": "How is AFFiNE Alpha different?",
"Shortcuts": "Shortcuts",
"Undo": "Undo",
"Redo": "Redo",
"Bold": "Bold",
"Italic": "Italic",
"Underline": "Underline",
"Strikethrough": "Strikethrough",
"Inline code": "Inline code",
"Code block": "Code block",
"Link": "Link",
"Body text": "Body text",
"Heading": "Heading {{number}}",
"Increase indent": "Increase indent",
"Reduce indent": "Reduce indent",
"Markdown Syntax": "Markdown Syntax",
"Divider": "Divider",
"404 - Page Not Found": "404 - Page Not Found"
} }
@@ -1,29 +1 @@
{ {}
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
"Add A Below Block": "在下方新添塊",
"Clear Workspace": "清空工作區",
"ComingSoon": "自定義佈局功能即將與您見面",
"Comment": "評論",
"Copy Page Link": "拷貝頁面鏈接",
"Delete": "刪除",
"Divide Here As A New Group": "從此地劃分成新組",
"Duplicate Page": "複製界面",
"Export As HTML": "導出 HTML",
"Export As Markdown": "以 Markdown 導出",
"Export As PDF (Unsupported)": "導出為 PDF(即將可用)",
"Export Workspace": "導出 Workspace",
"Import Workspace": "導入 Workspace",
"Language": "語言",
"Last edited by": "最後編輯者為 {{name}}",
"Layout": "佈局",
"Logout": "退出登錄",
"Settings": "設置",
"Share": "分享",
"Sync to Disk": "同步到磁盤",
"Turn into": "轉換為",
"WarningTips": {
"DoNotStore": "我們正在積極開發 AFFiNE,目前版本尚不穩定,請避免存儲信息或數據。",
"IsNotLocalWorkspace": "歡迎來到 AFFiNE 演示界面。您可以通過「同步到磁盤」來保存更改。",
"IsNotfsApiSupported": "歡迎進入AFFiNE演示!使用最新版本的基於 Chromium 內核的瀏覽器如Chrome/Edge,您可以通過「同步到磁盤」來保存更改"
}
}
+6 -4
View File
@@ -10,17 +10,18 @@ import '../utils/print-build-info';
import ProviderComposer from '@/components/provider-composer'; import ProviderComposer from '@/components/provider-composer';
import type { PropsWithChildren, ReactElement, ReactNode } from 'react'; import type { PropsWithChildren, ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import { AppStateProvider } from '@/providers/app-state-provider/provider'; import { AppStateProvider } from '@/providers/app-state-provider/Provider';
import ConfirmProvider from '@/providers/confirm-provider'; import ConfirmProvider from '@/providers/ConfirmProvider';
import { ModalProvider } from '@/providers/global-modal-provider'; import { ModalProvider } from '@/providers/GlobalModalProvider';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { PageLoading } from '@/components/loading'; import { PageLoading } from '@/components/loading';
import Head from 'next/head'; import Head from 'next/head';
import '@/libs/i18n'; import '@/libs/i18n';
import TemporaryHelperProvider from '@/providers/temporary-helper-provider';
const ThemeProvider = dynamic(() => import('@/providers/themeProvider'), { const ThemeProvider = dynamic(() => import('@/providers/ThemeProvider'), {
ssr: false, ssr: false,
}); });
@@ -52,6 +53,7 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => {
<Logger /> <Logger />
<ProviderComposer <ProviderComposer
contexts={[ contexts={[
<TemporaryHelperProvider key="TemporaryHelperProvider" />,
<ThemeProvider key="ThemeProvider" />, <ThemeProvider key="ThemeProvider" />,
<AppStateProvider key="appStateProvider" />, <AppStateProvider key="appStateProvider" />,
<ModalProvider key="ModalProvider" />, <ModalProvider key="ModalProvider" />,
+1 -1
View File
@@ -1,7 +1,7 @@
import type { NextPage } from 'next'; import type { NextPage } from 'next';
const Home: NextPage = () => { const Home: NextPage = () => {
return <div>Home Page</div>; return <div title="Home Page"></div>;
}; };
export default Home; export default Home;
@@ -0,0 +1,47 @@
import { WorkspaceModal } from '@/components/workspace-modal';
import { getWorkspaces } from '@/hooks/mock-data/mock';
import { useEffect, useState } from 'react';
import { styled } from '@/styles';
import Button from '@/ui/button/Button';
const Page = () => {
const [open, setOpen] = useState<boolean>(false);
useEffect(() => {
const data = getWorkspaces();
if (!data.length) {
setOpen(true);
}
}, []);
return (
<Workspace>
<h1>workspace</h1>
<div>
<Button
onClick={() => {
setOpen(true);
}}
>
View Workspace List
</Button>
</div>
<WorkspaceModal
open={open}
onClose={() => {
setOpen(false);
}}
></WorkspaceModal>
</Workspace>
);
};
export default Page;
const Workspace = styled.div(({ theme }) => {
return {
height: '100vh',
background: theme.colors.pageBackground,
color: '#FFFFFF',
fontSize: '18px',
fontWeight: 500,
};
});
@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import type { Page } from '@blocksuite/store';
import '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor';
import type { CreateEditorHandler } from './context';
interface Props {
setCreateEditorHandler: (handler: CreateEditorHandler) => void;
}
const DynamicBlocksuite = ({ setCreateEditorHandler }: Props) => {
useEffect(() => {
const createEditorHandler: CreateEditorHandler = (page: Page) => {
const editor = new EditorContainer();
editor.page = page;
return editor;
};
setCreateEditorHandler(createEditorHandler);
}, [setCreateEditorHandler]);
return <></>;
};
export default DynamicBlocksuite;
@@ -1,4 +1,4 @@
import { useMemo, useState, useEffect, useCallback, useRef } from 'react'; import { useMemo, useState, useCallback, useRef } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { getDataCenter } from '@affine/datacenter'; import { getDataCenter } from '@affine/datacenter';
@@ -8,12 +8,20 @@ import type {
CreateEditorHandler, CreateEditorHandler,
LoadWorkspaceHandler, LoadWorkspaceHandler,
} from './context'; } from './context';
import { Page, Workspace as StoreWorkspace } from '@blocksuite/store'; import { Page } from '@blocksuite/store';
import { EditorContainer } from '@blocksuite/editor'; import { EditorContainer } from '@blocksuite/editor';
const DynamicBlocksuite = dynamic(() => import('./dynamic-blocksuite'), { const DynamicBlocksuite = dynamic(() => import('./DynamicBlocksuite'), {
ssr: false, ssr: false,
}); });
const loadWorkspaceHandler: LoadWorkspaceHandler = async workspaceId => {
if (workspaceId) {
const dc = await getDataCenter();
return dc.load(workspaceId, { providerId: 'affine' });
} else {
return null;
}
};
export const AppStateProvider = ({ children }: { children?: ReactNode }) => { export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
const refreshWorkspacesMeta = async () => { const refreshWorkspacesMeta = async () => {
const dc = await getDataCenter(); const dc = await getDataCenter();
@@ -30,43 +38,10 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
currentWorkspace: null, currentWorkspace: null,
currentPage: null, currentPage: null,
editor: null, editor: null,
// Synced is used to ensure that the provider has synced with the server,
// So after Synced set to true, the other state is sure to be set.
synced: false,
refreshWorkspacesMeta, refreshWorkspacesMeta,
workspaces: {}, synced: true,
}); });
useEffect(() => {
(async () => {
const workspacesList = await Promise.all(
state.workspacesMeta.map(async ({ id }) => {
const workspace =
(await loadWorkspaceHandler?.(id, state.user)) || null;
return { id, workspace };
})
);
const workspaces: Record<string, StoreWorkspace | null> = {};
workspacesList.forEach(({ id, workspace }) => {
workspaces[id] = workspace;
});
setState(state => ({
...state,
workspaces,
}));
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.workspacesMeta]);
const [loadWorkspaceHandler, _setLoadWorkspaceHandler] =
useState<LoadWorkspaceHandler>();
const setLoadWorkspaceHandler = useCallback(
(handler: LoadWorkspaceHandler) => {
_setLoadWorkspaceHandler(() => handler);
},
[_setLoadWorkspaceHandler]
);
const [createEditorHandler, _setCreateEditorHandler] = const [createEditorHandler, _setCreateEditorHandler] =
useState<CreateEditorHandler>(); useState<CreateEditorHandler>();
@@ -85,7 +60,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
return state.currentWorkspace; return state.currentWorkspace;
} }
const workspace = const workspace =
(await loadWorkspaceHandler?.(workspaceId, state.user)) || null; (await loadWorkspaceHandler(workspaceId, state.user)) || null;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
@@ -103,6 +78,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
})); }));
return workspace; return workspace;
}; };
const loadPage = useRef<AppStateContext['loadPage']>(() => const loadPage = useRef<AppStateContext['loadPage']>(() =>
Promise.resolve(null) Promise.resolve(null)
); );
@@ -147,17 +123,6 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
setState(state => ({ ...state, editor })); setState(state => ({ ...state, editor }));
}; };
useEffect(() => {
if (!loadWorkspaceHandler) {
return;
}
setState(state => ({
...state,
workspacesMeta: [],
synced: true,
}));
}, [loadWorkspaceHandler]);
const context = useMemo( const context = useMemo(
() => ({ () => ({
...state, ...state,
@@ -172,10 +137,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
return ( return (
<AppState.Provider value={context}> <AppState.Provider value={context}>
<DynamicBlocksuite <DynamicBlocksuite setCreateEditorHandler={setCreateEditorHandler} />
setLoadWorkspaceHandler={setLoadWorkspaceHandler}
setCreateEditorHandler={setCreateEditorHandler}
/>
{children} {children}
</AppState.Provider> </AppState.Provider>
); );
@@ -5,6 +5,7 @@ import type {
Workspace as StoreWorkspace, Workspace as StoreWorkspace,
} from '@blocksuite/store'; } from '@blocksuite/store';
import type { EditorContainer } from '@blocksuite/editor'; import type { EditorContainer } from '@blocksuite/editor';
export type LoadWorkspaceHandler = ( export type LoadWorkspaceHandler = (
workspaceId: string, workspaceId: string,
user?: AccessTokenMessage | null user?: AccessTokenMessage | null
@@ -20,7 +21,7 @@ export interface AppStateValue {
currentPage: StorePage | null; currentPage: StorePage | null;
workspaces: Record<string, StoreWorkspace | null>; // workspaces: Record<string, StoreWorkspace | null>;
editor: EditorContainer | null; editor: EditorContainer | null;
synced: boolean; synced: boolean;
@@ -40,14 +41,10 @@ export interface AppStateContext extends AppStateValue {
export const AppState = createContext<AppStateContext>({ export const AppState = createContext<AppStateContext>({
user: null, user: null,
workspacesMeta: [], workspacesMeta: [],
currentWorkspaceId: '', currentWorkspaceId: '',
currentWorkspace: null, currentWorkspace: null,
currentPage: null, currentPage: null,
editor: null, editor: null,
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
setState: () => {}, setState: () => {},
createEditor: undefined, createEditor: undefined,
@@ -57,7 +54,6 @@ export const AppState = createContext<AppStateContext>({
synced: false, synced: false,
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
refreshWorkspacesMeta: () => {}, refreshWorkspacesMeta: () => {},
workspaces: {},
}); });
export const useAppState = () => { export const useAppState = () => {
@@ -1,43 +0,0 @@
import { useEffect } from 'react';
import type { Page } from '@blocksuite/store';
import '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor';
import type { LoadWorkspaceHandler, CreateEditorHandler } from './context';
import { getDataCenter } from '@affine/datacenter';
interface Props {
setLoadWorkspaceHandler: (handler: LoadWorkspaceHandler) => void;
setCreateEditorHandler: (handler: CreateEditorHandler) => void;
}
const DynamicBlocksuite = ({
setLoadWorkspaceHandler,
setCreateEditorHandler,
}: Props) => {
useEffect(() => {
const openWorkspace: LoadWorkspaceHandler = async (workspaceId: string) => {
if (workspaceId) {
const dc = await getDataCenter();
return dc.load(workspaceId, { providerId: 'selfhosted' });
} else {
return null;
}
};
setLoadWorkspaceHandler(openWorkspace);
}, [setLoadWorkspaceHandler]);
useEffect(() => {
const createEditorHandler: CreateEditorHandler = (page: Page) => {
const editor = new EditorContainer();
editor.page = page;
return editor;
};
setCreateEditorHandler(createEditorHandler);
}, [setCreateEditorHandler]);
return <></>;
};
export default DynamicBlocksuite;
@@ -1,51 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAppState } from './context';
import { usePageHelper } from '@/hooks/use-page-helper';
export const useLoadWorkspace = () => {
const router = useRouter();
const { loadWorkspace, currentWorkspace, currentWorkspaceId } = useAppState();
const workspaceId = router.query.workspaceId as string;
useEffect(() => {
loadWorkspace?.(workspaceId);
}, [workspaceId, loadWorkspace]);
return currentWorkspaceId === workspaceId ? currentWorkspace : null;
};
export const useLoadPage = () => {
const router = useRouter();
const { loadPage, currentPage, currentWorkspaceId } = useAppState();
const { createPage } = usePageHelper();
const workspace = useLoadWorkspace();
const pageId = router.query.pageId as string;
useEffect(() => {
if (!workspace) {
return;
}
const page = pageId ? workspace?.getPage(pageId) : null;
if (page) {
loadPage?.(pageId);
return;
}
const savedPageId = workspace.meta.pageMetas[0]?.id;
if (savedPageId) {
router.push(`/workspace/${currentWorkspaceId}/${savedPageId}`);
return;
}
createPage().then(async pageId => {
if (!pageId) {
return;
}
router.push(`/workspace/${currentWorkspaceId}/${pageId}`);
});
}, [workspace, pageId, loadPage, createPage, router, currentWorkspaceId]);
return currentPage?.id === pageId ? currentPage : null;
};
@@ -0,0 +1,183 @@
import { createContext, useContext, useEffect, useState } from 'react';
import type { PropsWithChildren } from 'react';
import { getWorkspaces, User, Workspace } from '@/hooks/mock-data/mock';
type TemporaryContextValue = {
workspaceMetaList: Workspace[];
currentWorkspace: Workspace | null;
user: User | null;
updateWorkspaceMeta: (
workspaceId: string,
workspaceData: {
name?: string;
avatar?: string;
isPublish?: boolean;
type?: 'local' | 'cloud' | 'join';
}
) => void;
createWorkspace: (workspaceName: string) => Workspace;
deleteWorkspace: (workspaceId: string) => void;
setWorkspacePublish: (id: string, isPublish: boolean) => void;
setActiveWorkspace: (workspaceData: Workspace) => void;
login: () => void;
signOut: () => void;
};
type TemporaryContextProps = PropsWithChildren<Record<string, unknown>>;
export const TemporaryContext = createContext<TemporaryContextValue>({
workspaceMetaList: [],
currentWorkspace: null,
user: null,
// eslint-disable-next-line @typescript-eslint/no-empty-function
updateWorkspaceMeta: () => {},
createWorkspace: () => {
return {} as Workspace;
},
// eslint-disable-next-line @typescript-eslint/no-empty-function
deleteWorkspace: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
setWorkspacePublish: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
setActiveWorkspace: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
login: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
signOut: () => {},
});
export const useTemporaryHelper = () => useContext(TemporaryContext);
export const TemporaryHelperProvider = ({
children,
}: PropsWithChildren<TemporaryContextProps>) => {
const [workspaceMetaList, setWorkspaceMetaList] = useState<Workspace[]>([]);
const [currentWorkspace, setCurrentWorkspace] = useState<Workspace | null>(
null
);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
setWorkspaceMetaList(getWorkspaces());
setCurrentWorkspace(
JSON.parse(localStorage.getItem('affine-active-workspace') ?? '{}')
);
}, []);
return (
<TemporaryContext.Provider
value={{
workspaceMetaList,
currentWorkspace,
user,
updateWorkspaceMeta: (workspaceId, workspaceData) => {
const workspacesMeta = getWorkspaces();
const newWorkspacesMeta = workspacesMeta.map(
(workspace: Workspace) => {
if (workspace.id === workspaceId) {
const workspaceObj = Object.assign(workspace, workspaceData);
return workspaceObj;
}
return workspace;
}
);
localStorage.setItem(
'affine-workspace',
JSON.stringify(newWorkspacesMeta)
);
setWorkspaceMetaList(newWorkspacesMeta);
if (workspaceId === currentWorkspace?.id) {
setCurrentWorkspace(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
newWorkspacesMeta.find(v => v.id === currentWorkspace.id)
);
}
},
createWorkspace: workspaceName => {
const workspaceData = {
name: workspaceName,
id: 'workspace-' + Date.now(),
isPublish: false,
isLocal: true,
avatar: '',
type: 'local',
} as Workspace;
const workspacesMeta = getWorkspaces();
workspacesMeta.push(workspaceData);
localStorage.setItem(
'affine-workspace',
JSON.stringify(workspacesMeta)
);
setWorkspaceMetaList([...workspacesMeta]);
return workspaceData;
},
deleteWorkspace: workspaceId => {
const workspacesMeta = getWorkspaces();
const newWorkspacesMeta = workspacesMeta.filter(() => {
return workspaceId !== workspaceId;
});
localStorage.setItem(
'affine-workspace',
JSON.stringify(newWorkspacesMeta)
);
setWorkspaceMetaList(workspacesMeta);
},
setWorkspacePublish: (id, isPublish) => {
const workspacesMeta = getWorkspaces();
const newWorkspacesMeta = workspacesMeta.map(
(workspace: Workspace) => {
if (workspace.id === id) {
workspace.isPublish = isPublish;
}
return workspace;
}
);
localStorage.setItem(
'affine-workspace',
JSON.stringify(newWorkspacesMeta)
);
setWorkspaceMetaList(workspacesMeta);
},
setActiveWorkspace(workspaceData) {
localStorage.setItem(
'affine-active-workspace',
JSON.stringify(workspaceData)
);
setCurrentWorkspace(workspaceData);
},
login: () => {
const userInfo = {
name: 'Diamond',
id: 'ttt',
email: 'diamond.shx@gmail.com',
avatar: 'string',
};
localStorage.setItem('affine-user', JSON.stringify(userInfo));
setUser(userInfo);
},
signOut(): void {
localStorage.removeItem('affine-user');
setUser(null);
},
}}
>
{children}
</TemporaryContext.Provider>
);
};
export default TemporaryHelperProvider;
@@ -13,7 +13,7 @@ Let us know what you think of this latest version.
5. You can self-host locally with Docker. 5. You can self-host locally with Docker.
```basic ```basic
docker run -d -v [YOUR_PATH]:/app/data -p 3000:3000 ghcr.io/toeverything/affine-self-hosted:alpha-abbey-wood docker run -it --name affine -d -v [YOUR_PATH]:/app/data -p 3000:3000 ghcr.io/toeverything/affine-self-hosted:alpha-abbey-wood
``` ```
**Looking for Markdown syntax or keyboard shortcuts?** **Looking for Markdown syntax or keyboard shortcuts?**
@@ -24,6 +24,9 @@ docker run -d -v [YOUR_PATH]:/app/data -p 3000:3000 ghcr.io/toeverything/affine-
- Manage your pages from the collapsible **sidebar**, which allows you to add **favourites** and restore deleted files from the **trash** - Manage your pages from the collapsible **sidebar**, which allows you to add **favourites** and restore deleted files from the **trash**
- Search through all your content with the quick search - activate with `Ctrl/⌘ + K` - Search through all your content with the quick search - activate with `Ctrl/⌘ + K`
- A friendly Reminder:
- In the case of unselected text, `Ctrl/⌘ + K` activates quick search;
- In the case of selected text, `Ctrl/⌘ + K` will firstly ask to add a hyperlink, and then using `Ctrl/⌘ + K` again activates the quick search
- Quickly format text with the **pop-up toolbar** (highlight any text to give it a try) - Quickly format text with the **pop-up toolbar** (highlight any text to give it a try)
- Copy and paste **images** into your pages, resize them and add captions - Copy and paste **images** into your pages, resize them and add captions
- Add horizontal line dividers to your text with `---` and `***` - Add horizontal line dividers to your text with `---` and `***`
+3 -1
View File
@@ -12,6 +12,7 @@ export type ConfirmProps = {
title?: string; title?: string;
content?: string; content?: string;
confirmText?: string; confirmText?: string;
cancelText?: string;
// TODO: Confirm button's color should depend on confirm type // TODO: Confirm button's color should depend on confirm type
confirmType?: 'primary' | 'warning' | 'danger'; confirmType?: 'primary' | 'warning' | 'danger';
onConfirm?: () => void; onConfirm?: () => void;
@@ -25,6 +26,7 @@ export const Confirm = ({
confirmType, confirmType,
onConfirm, onConfirm,
onCancel, onCancel,
cancelText = 'Cancel',
}: ConfirmProps) => { }: ConfirmProps) => {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -50,7 +52,7 @@ export const Confirm = ({
}} }}
style={{ marginRight: '24px' }} style={{ marginRight: '24px' }}
> >
{t('Cancel')} {cancelText === 'Cancel' ? t('Cancel') : cancelText}
</Button> </Button>
<Button <Button
type={confirmType} type={confirmType}
+15 -3
View File
@@ -1,4 +1,10 @@
import { InputHTMLAttributes, useEffect, useState } from 'react'; import {
InputHTMLAttributes,
useEffect,
useState,
FocusEventHandler,
KeyboardEventHandler,
} from 'react';
import { StyledInput } from './style'; import { StyledInput } from './style';
type inputProps = { type inputProps = {
@@ -9,8 +15,8 @@ type inputProps = {
maxLength?: number; maxLength?: number;
minLength?: number; minLength?: number;
onChange?: (value: string) => void; onChange?: (value: string) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any onBlur?: FocusEventHandler<HTMLInputElement>;
onBlur?: (e: any) => void; onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
}; };
export const Input = (props: inputProps) => { export const Input = (props: inputProps) => {
@@ -23,6 +29,7 @@ export const Input = (props: inputProps) => {
width = 260, width = 260,
onChange, onChange,
onBlur, onBlur,
onKeyDown,
} = props; } = props;
const [value, setValue] = useState<string>(valueProp || ''); const [value, setValue] = useState<string>(valueProp || '');
const handleChange: InputHTMLAttributes<HTMLInputElement>['onChange'] = e => { const handleChange: InputHTMLAttributes<HTMLInputElement>['onChange'] = e => {
@@ -37,6 +44,10 @@ export const Input = (props: inputProps) => {
const handleBlur: InputHTMLAttributes<HTMLInputElement>['onBlur'] = e => { const handleBlur: InputHTMLAttributes<HTMLInputElement>['onBlur'] = e => {
onBlur && onBlur(e); onBlur && onBlur(e);
}; };
const handleKeyDown: InputHTMLAttributes<HTMLInputElement>['onKeyDown'] =
e => {
onKeyDown && onKeyDown(e);
};
useEffect(() => { useEffect(() => {
setValue(valueProp || ''); setValue(valueProp || '');
}, [valueProp]); }, [valueProp]);
@@ -50,6 +61,7 @@ export const Input = (props: inputProps) => {
minLength={minLength} minLength={minLength}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
onKeyDown={handleKeyDown}
></StyledInput> ></StyledInput>
); );
}; };
+20
View File
@@ -0,0 +1,20 @@
export function stringToColour(str: string) {
str = str || 'affine';
let colour = '#';
let hash = 0;
// str to hash
for (
let i = 0;
i < str.length;
hash = str.charCodeAt(i++) + ((hash << 5) - hash)
);
// int/hash to hex
for (
let i = 0;
i < 3;
colour += ('00' + ((hash >> (i++ * 8)) & 0xff).toString(16)).slice(-2)
);
return colour;
}
+2
View File
@@ -3,3 +3,5 @@ export { isDev } from './env';
export * from './useragent'; export * from './useragent';
export * from './tools'; export * from './tools';
export * from './colors';

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