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

View File

@@ -1,5 +1,4 @@
**/webpack.config.js
**/jest.config.js
**/scripts/*.js
**/node_modules/**
.github/**

View File

@@ -36,7 +36,7 @@ jobs:
scope: '@toeverything'
cache: 'pnpm'
- run: node scripts/module-resolve/ci.js
- run: node scripts/module-resolve/ci.cjs
- name: Restore cache
uses: actions/cache@v3

View File

@@ -244,7 +244,7 @@ jobs:
scope: '@toeverything'
cache: 'pnpm'
- run: node scripts/module-resolve/ci.js
- run: node scripts/module-resolve/ci.cjs
- name: Restore cache
uses: actions/cache@v3

View File

@@ -23,7 +23,7 @@ jobs:
scope: '@toeverything'
cache: 'pnpm'
- run: node scripts/module-resolve/ci.js
- run: node scripts/module-resolve/ci.cjs
- name: Install dependencies
run: pnpm install --no-frozen-lockfile

1
.gitignore vendored
View File

@@ -48,6 +48,7 @@ Thumbs.db
out/
module-resolve.js
module-resolve.cjs
/test-results/
/playwright-report/
/playwright/.cache/

View File

@@ -1,11 +1,11 @@
const fs = require('fs');
function getCustomize() {
const customed = fs.existsSync('./module-resolve.js');
const customed = fs.existsSync('./module-resolve.cjs');
if (!customed) {
return null;
}
const script = require('./module-resolve.js');
const script = require('./module-resolve.cjs');
return script && script.resolve;
}

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"
]
}

View File

@@ -4,6 +4,7 @@
"private": true,
"author": "toeverything",
"license": "MPL-2.0",
"type": "module",
"scripts": {
"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",
@@ -47,8 +48,7 @@
"lint-staged": "^13.1.0",
"prettier": "^2.7.1",
"ts-jest": "^29.0.3",
"typescript": "^4.9.3",
"vitest": "^0.26.3"
"typescript": "^4.9.3"
},
"eslintConfig": {
"root": true,

View File

@@ -1,5 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

View File

@@ -10,10 +10,10 @@
},
"dependencies": {
"@affine/datacenter": "workspace:*",
"@blocksuite/blocks": "=0.3.1-20230106060050-1aad55d",
"@blocksuite/editor": "=0.3.1-20230106060050-1aad55d",
"@blocksuite/blocks": "0.3.1-20230109032243-37ad3ba",
"@blocksuite/editor": "0.3.1-20230109032243-37ad3ba",
"@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/react": "^11.10.4",
"@emotion/server": "^11.10.0",
@@ -49,8 +49,8 @@
"eslint-config-next": "12.3.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"raw-loader": "^4.0.2",
"next-pwa": "^5.6.0",
"raw-loader": "^4.0.2",
"typescript": "4.8.3"
},
"eslintConfig": {

View File

@@ -8,7 +8,7 @@ import {
TelegramIcon,
RedditIcon,
LinkIcon,
} from './icons';
} from './Icons';
import logo from './affine-text-logo.png';
import {
StyledBigLink,

View File

@@ -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',
// });

View File

@@ -13,79 +13,83 @@ import {
ConnectorIcon,
UndoIcon,
RedoIcon,
} from './icons';
} from './Icons';
import { Tooltip } from '@/ui/tooltip';
import Slide from '@mui/material/Slide';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { useAppState } from '@/providers/app-state-provider';
import useHistoryUpdated from '@/hooks/use-history-update';
import { useTranslation } from 'react-i18next';
const toolbarList1 = [
{
flavor: 'select',
icon: <SelectIcon />,
toolTip: 'Select',
disable: false,
callback: () => {
window.dispatchEvent(
new CustomEvent('affine.switch-mouse-mode', {
detail: {
type: 'default',
},
})
);
const useToolbarList1 = () => {
const { t } = useTranslation();
return [
{
flavor: 'select',
icon: <SelectIcon />,
toolTip: t('Select'),
disable: false,
callback: () => {
window.dispatchEvent(
new CustomEvent('affine.switch-mouse-mode', {
detail: {
type: 'default',
},
})
);
},
},
},
{
flavor: 'text',
icon: <TextIcon />,
toolTip: 'Text (coming soon)',
disable: true,
},
{
flavor: 'shape',
icon: <ShapeIcon />,
toolTip: 'Shape',
disable: false,
callback: () => {
window.dispatchEvent(
new CustomEvent('affine.switch-mouse-mode', {
detail: {
type: 'shape',
color: 'black',
shape: 'rectangle',
},
})
);
{
flavor: 'text',
icon: <TextIcon />,
toolTip: t('Text'),
disable: true,
},
{
flavor: 'shape',
icon: <ShapeIcon />,
toolTip: t('Shape'),
disable: false,
callback: () => {
window.dispatchEvent(
new CustomEvent('affine.switch-mouse-mode', {
detail: {
type: 'shape',
color: 'black',
shape: 'rectangle',
},
})
);
},
},
{
flavor: 'sticky',
icon: <StickerIcon />,
toolTip: t('Sticky'),
disable: true,
},
{
flavor: 'pen',
icon: <PenIcon />,
toolTip: t('Pen'),
disable: true,
},
},
{
flavor: 'sticky',
icon: <StickerIcon />,
toolTip: 'Sticky (coming soon)',
disable: true,
},
{
flavor: 'pen',
icon: <PenIcon />,
toolTip: 'Pen (coming soon)',
disable: true,
},
{
flavor: 'connector',
icon: <ConnectorIcon />,
toolTip: 'Connector (coming soon)',
disable: true,
},
];
{
flavor: 'connector',
icon: <ConnectorIcon />,
toolTip: t('Connector'),
disable: true,
},
];
};
const UndoRedo = () => {
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const { currentPage } = useAppState();
const onHistoryUpdated = useHistoryUpdated();
const { t } = useTranslation();
useEffect(() => {
onHistoryUpdated(page => {
setCanUndo(page.canUndo);
@@ -95,7 +99,7 @@ const UndoRedo = () => {
return (
<StyledToolbarWrapper>
<Tooltip content="Undo" placement="right-start">
<Tooltip content={t('Undo')} placement="right-start">
<StyledToolbarItem
disable={!canUndo}
onClick={() => {
@@ -105,7 +109,7 @@ const UndoRedo = () => {
<UndoIcon />
</StyledToolbarItem>
</Tooltip>
<Tooltip content="Redo" placement="right-start">
<Tooltip content={t('Redo')} placement="right-start">
<StyledToolbarItem
disable={!canRedo}
onClick={() => {
@@ -131,7 +135,7 @@ export const EdgelessToolbar = () => {
>
<StyledEdgelessToolbar aria-label="edgeless-toolbar">
<StyledToolbarWrapper>
{toolbarList1.map(
{useToolbarList1().map(
({ icon, toolTip, flavor, disable, callback }, index) => {
return (
<Tooltip key={index} content={toolTip} placement="right-start">

View File

@@ -11,8 +11,8 @@ import type {
AnimateRadioProps,
AnimateRadioItemProps,
} from './type';
import { useTheme } from '@/providers/themeProvider';
import { EdgelessIcon, PaperIcon } from './icons';
import { useTheme } from '@/providers/ThemeProvider';
import { EdgelessIcon, PaperIcon } from './Icons';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from 'react-i18next';

View File

@@ -1,6 +1,7 @@
import { Button } from '@/ui/button';
import { FC, useRef, ChangeEvent, ReactElement } from 'react';
import { styled } from '@/styles';
import { useTranslation } from 'react-i18next';
interface Props {
uploadType?: string;
children?: ReactElement;
@@ -9,6 +10,7 @@ interface Props {
}
export const Upload: FC<Props> = props => {
const { fileChange, accept } = props;
const { t } = useTranslation();
const input_ref = useRef<HTMLInputElement>(null);
const _chooseFile = () => {
if (input_ref.current) {
@@ -28,7 +30,7 @@ export const Upload: FC<Props> = props => {
};
return (
<UploadStyle onClick={_chooseFile}>
{props.children ?? <Button>Upload</Button>}
{props.children ?? <Button>{t('Upload')}</Button>}
<input
ref={input_ref}
type="file"

View File

@@ -8,8 +8,8 @@ import {
import { Content } from '@/ui/layout';
import { useAppState } from '@/providers/app-state-provider/context';
import EditorModeSwitch from '@/components/editor-mode-switch';
import QuickSearchButton from './quick-search-button';
import Header from './header';
import QuickSearchButton from './QuickSearchButton';
import Header from './Header';
import usePropsUpdated from '@/hooks/use-props-updated';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';

View File

@@ -8,10 +8,10 @@ import {
} from './styles';
import CloseIcon from '@mui/icons-material/Close';
import { getWarningMessage, shouldShowWarning } from './utils';
import EditorOptionMenu from './header-right-items/editor-option-menu';
import TrashButtonGroup from './header-right-items/trash-button-group';
import EditorOptionMenu from './header-right-items/EditorOptionMenu';
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
import SyncUser from './header-right-items/sync-user';
import SyncUser from './header-right-items/SyncUser';
const BrowserWarning = ({
show,

View File

@@ -1,7 +1,7 @@
import { PropsWithChildren, ReactNode } from 'react';
import Header from './header';
import Header from './Header';
import { StyledPageListTittleWrapper } from './styles';
import QuickSearchButton from './quick-search-button';
import QuickSearchButton from './QuickSearchButton';
export type PageListHeaderProps = PropsWithChildren<{
icon?: ReactNode;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { IconButton, IconButtonProps } from '@/ui/button';
import { Tooltip } from '@/ui/tooltip';
import { ArrowDownIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/global-modal-provider';
import { useModal } from '@/providers/GlobalModalProvider';
import { useTranslation } from 'react-i18next';
export const QuickSearchButton = ({
onClick,

View File

@@ -13,7 +13,7 @@ import {
} from '@blocksuite/icons';
import { useAppState } from '@/providers/app-state-provider';
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 { toast } from '@/ui/toast';
import { useTranslation } from 'react-i18next';

View File

@@ -1,5 +1,5 @@
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 { IconButton } from '@/ui/button';

View File

@@ -1,9 +1,10 @@
import { Button } from '@/ui/button';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useAppState } from '@/providers/app-state-provider';
import { useConfirm } from '@/providers/confirm-provider';
import { useConfirm } from '@/providers/ConfirmProvider';
import { useRouter } from 'next/router';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { useTranslation } from 'react-i18next';
export const TrashButtonGroup = () => {
const { permanentlyDeletePage } = usePageHelper();
@@ -12,7 +13,7 @@ export const TrashButtonGroup = () => {
const { confirm } = useConfirm();
const router = useRouter();
const { id = '' } = useCurrentPageMeta() || {};
const { t } = useTranslation();
return (
<>
<Button
@@ -23,7 +24,7 @@ export const TrashButtonGroup = () => {
toggleDeletePage(id);
}}
>
Restore it
{t('Restore it')}
</Button>
<Button
bold={true}
@@ -31,10 +32,9 @@ export const TrashButtonGroup = () => {
type="danger"
onClick={() => {
confirm({
title: 'Permanently delete',
content:
"Once deleted, you can't undo this action. Do you confirm?",
confirmText: 'Delete',
title: t('TrashButtonGroupTitle'),
content: t('TrashButtonGroupDescription'),
confirmText: t('Delete'),
confirmType: 'danger',
}).then(confirm => {
if (confirm) {
@@ -44,7 +44,7 @@ export const TrashButtonGroup = () => {
});
}}
>
Delete permanently
{t('Delete permanently')}
</Button>
</>
);

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useTheme } from '@/providers/themeProvider';
import { MoonIcon, SunIcon } from './icons';
import { useTheme } from '@/providers/ThemeProvider';
import { MoonIcon, SunIcon } from './Icons';
import { StyledThemeModeSwitch, StyledSwitchItem } from './style';
export const ThemeModeSwitch = () => {

View File

@@ -1,3 +1,3 @@
export * from './header';
export * from './editor-header';
export * from './page-list-header';
export * from './Header';
export * from './EditorHeader';
export * from './PageListHeader';

View File

@@ -5,12 +5,12 @@ import {
StyledIslandWrapper,
StyledTransformIcon,
} from './style';
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './icons';
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
import Grow from '@mui/material/Grow';
import { Tooltip } from '@/ui/tooltip';
import { useTranslation } from 'react-i18next';
import { useModal } from '@/providers/global-modal-provider';
import { useTheme } from '@/providers/themeProvider';
import { useModal } from '@/providers/GlobalModalProvider';
import { useTheme } from '@/providers/ThemeProvider';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
export type IslandItemNames = 'contact' | 'shortcuts';
export const HelpIsland = ({

View File

@@ -4,8 +4,9 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import Input from '@/ui/input';
import { useState } from 'react';
import { getDataCenter } from '@affine/datacenter';
// import { getDataCenter } from '@affine/datacenter';
import { Avatar } from '@mui/material';
import { setMember } from '@/hooks/mock-data/mock';
interface LoginModalProps {
open: boolean;
onClose: () => void;
@@ -54,29 +55,34 @@ export const InviteMembers = ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [userData, setUserData] = useState<any>({});
const inputChange = (value: string) => {
setEmail(value);
setShowMember(true);
if (gmailReg.test(value)) {
setEmail(value);
setShowTip(false);
debounce(
() => {
// getDataCenter()
// .then(dc =>
// dc.apis.getUserByEmail({
// email: value,
// workspace_id: workspaceId,
// })
// )
// .then(data => {
// if (data?.name) {
// setUserData(data);
// setShowTip(false);
// }
// });
},
300,
true
)();
setUserData({
name: 'wxl',
avatar: 'https://avatars.githubusercontent.com/u/20501502?v=4',
email: value,
});
// debounce(
// () => {
// getDataCenter()
// .then(dc =>
// dc.apis.getUserByEmail({
// email: value,
// workspace_id: workspaceId,
// })
// )
// .then(data => {
// if (data?.name) {
// setUserData(data);
// setShowTip(false);
// }
// });
// },
// 300,
// true
// )();
} else {
setShowTip(true);
}
@@ -134,6 +140,8 @@ export const InviteMembers = ({
shape="circle"
type="primary"
onClick={() => {
setMember(workspaceId, userData);
onInviteSuccess();
// getDataCenter()
// .then(dc => dc.apis.inviteMember({ id: workspaceId, email }))
// .then(() => {

View File

@@ -1,5 +1,5 @@
import { styled } from '@/styles';
import Loading from './loading';
import Loading from './Loading';
// Used for the full page loading
const StyledLoadingContainer = styled('div')(() => {

View File

@@ -1,3 +1,3 @@
import Loading from './loading';
export * from './page-loading';
import Loading from './Loading';
export * from './PageLoading';
export default Loading;

View File

@@ -1,8 +1,8 @@
import { getDataCenter } from '@affine/datacenter';
import { styled } from '@/styles';
import { Button } from '@/ui/button';
import { useModal } from '@/providers/global-modal-provider';
import { GoogleIcon, StayLogOutIcon } from './icons';
import { useModal } from '@/providers/GlobalModalProvider';
import { GoogleIcon, StayLogOutIcon } from './Icons';
export const GoogleLoginButton = () => {
const { triggerLoginModal } = useModal();

View File

@@ -1,4 +1,4 @@
import { useConfirm } from '@/providers/confirm-provider';
import { useConfirm } from '@/providers/ConfirmProvider';
import { PageMeta } from '@/providers/app-state-provider';
import { Menu, MenuItem } from '@/ui/menu';
import { Wrapper } from '@/ui/layout';

View File

@@ -12,18 +12,18 @@ import {
StyledTitleWrapper,
} from './styles';
import { Table, TableBody, TableCell, TableHead, TableRow } from '@/ui/table';
import { OperationCell, TrashOperationCell } from './operation-cell';
import Empty from './empty';
import { OperationCell, TrashOperationCell } from './OperationCell';
import Empty from './Empty';
import { Content } from '@/ui/layout';
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 { Tooltip } from '@/ui/tooltip';
import { useRouter } from 'next/router';
import { useAppState } from '@/providers/app-state-provider/context';
import { toast } from '@/ui/toast';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useTheme } from '@/providers/themeProvider';
import { useTheme } from '@/providers/ThemeProvider';
import { useTranslation } from 'react-i18next';
const FavoriteTag = ({
pageMeta: { favorite, id },

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { AddIcon } from '@blocksuite/icons';
import { StyledModalFooterContent } from './style';
import { useModal } from '@/providers/global-modal-provider';
import { useModal } from '@/providers/GlobalModalProvider';
import { Command } from 'cmdk';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from 'react-i18next';

View File

@@ -1,12 +1,12 @@
import { Command } from 'cmdk';
import { StyledListItem, StyledNotFound } from './style';
import { useModal } from '@/providers/global-modal-provider';
import { useModal } from '@/providers/GlobalModalProvider';
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
import { useSwitchToConfig } from './config';
import { NoResultSVG } from './noResultSVG';
import { NoResultSVG } from './NoResultSVG';
import { useTranslation } from 'react-i18next';
import usePageHelper from '@/hooks/use-page-helper';
import usePageMetaList from '@/hooks/use-page-meta-list';

View File

@@ -6,12 +6,12 @@ import {
StyledModalDivider,
StyledShortcut,
} from './style';
import { Input } from './input';
import { Results } from './results';
import { Footer } from './footer';
import { Input } from './Input';
import { Results } from './Results';
import { Footer } from './Footer';
import { Command } from 'cmdk';
import { useEffect, useState } from 'react';
import { useModal } from '@/providers/global-modal-provider';
import { useModal } from '@/providers/GlobalModalProvider';
import { getUaHelper } from '@/utils';
import { useAppState } from '@/providers/app-state-provider';
type TransitionsModalProps = {

View File

@@ -13,7 +13,8 @@ export const useMacKeyboardShortcuts = (): ShortcutTip => {
[t('Strikethrough')]: '⌘+⇧+S',
[t('Inline code')]: ' ⌘+E',
[t('Code block')]: '⌘+⌥+C',
[t('Link')]: '⌘+K',
[t('Hyperlink(with selected text)')]: '⌘+K',
[t('Quick search')]: '⌘+K',
[t('Body text')]: '⌘+⌥+0',
[t('Heading', { number: '1' })]: '⌘+⌥+1',
[t('Heading', { number: '2' })]: '⌘+⌥+2',
@@ -56,7 +57,8 @@ export const useWindowsKeyboardShortcuts = (): ShortcutTip => {
[t('Strikethrough')]: 'Ctrl+Shift+S',
[t('Inline code')]: ' Ctrl+E',
[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('Heading', { number: '1' })]: 'Ctrl+Shift+1',
[t('Heading', { number: '2' })]: 'Ctrl+Shift+2',

View File

@@ -1,5 +1,5 @@
import { createPortal } from 'react-dom';
import { KeyboardIcon } from './icons';
import { KeyboardIcon } from './Icons';
import {
StyledListItem,
StyledModalHeader,

View File

@@ -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>
</>
);
};

View File

@@ -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',
},
});

View File

@@ -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,
}));

View File

@@ -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'),
};
});

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
)}
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -1,62 +1,37 @@
import {
StyledDeleteButtonContainer,
StyledSettingAvatar,
// StyledSettingAvatar,
StyledSettingAvatarContent,
StyledSettingInputContainer,
} from './style';
import { StyledSettingH2 } from '../style';
import { useState } from 'react';
import { Button } from '@/ui/button';
import { Button, TextButton } from '@/ui/button';
import Input from '@/ui/input';
import { getDataCenter, Workspace, WorkspaceType } from '@affine/datacenter';
import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceDetails } from '@/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
// import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceDelete } from './delete';
import { Workspace as StoreWorkspace } from '@blocksuite/store';
import { debounce } from '@/utils';
// import { debounce } from '@/utils';
import { WorkspaceLeave } from './leave';
import { Upload } from '@/components/file-upload';
export const GeneralPage = ({
workspace,
owner,
}: {
workspace: Workspace;
owner: WorkspaceDetails[string]['owner'];
workspaces: Record<string, StoreWorkspace | null>;
}) => {
const {
user,
currentWorkspace,
workspacesMeta,
workspaces,
refreshWorkspacesMeta,
} = useAppState();
import { Workspace } from '@/hooks/mock-data/mock';
import { WorkspaceAvatar } from '@/components/workspace-avatar';
import { useTemporaryHelper } from '@/providers/temporary-helper-provider';
export const GeneralPage = ({ workspace }: { workspace: Workspace }) => {
// const { refreshWorkspacesMeta } = useAppState();
const { updateWorkspaceMeta } = useTemporaryHelper();
const [showDelete, setShowDelete] = useState<boolean>(false);
const [showLeave, setShowLeave] = useState<boolean>(false);
const [uploading, setUploading] = useState<boolean>(false);
const [workspaceName, setWorkspaceName] = useState<string>(
workspaces[workspace.id]?.meta.name ||
(workspace.type === WorkspaceType.Private && user ? user.name : '')
);
const debouncedRefreshWorkspacesMeta = debounce(() => {
refreshWorkspacesMeta();
}, 100);
const isOwner = user && owner.id === user.id;
const [workspaceName, setWorkspaceName] = useState<string>('');
// const debouncedRefreshWorkspacesMeta = debounce(() => {
// refreshWorkspacesMeta();
// }, 100);
const isOwner = true;
const handleChangeWorkSpaceName = (newName: string) => {
setWorkspaceName(newName);
currentWorkspace?.meta.setName(newName);
workspaces[workspace.id]?.meta.setName(newName);
debouncedRefreshWorkspacesMeta();
};
const currentWorkspaceIndex = workspacesMeta.findIndex(
meta => meta.id === workspace.id
);
const nextWorkSpaceId =
currentWorkspaceIndex === workspacesMeta.length - 1
? workspacesMeta[currentWorkspaceIndex - 1]?.id
: workspacesMeta[currentWorkspaceIndex + 1]?.id;
const handleClickDelete = () => {
setShowDelete(true);
};
@@ -70,9 +45,13 @@ export const GeneralPage = ({
const handleCloseLeave = () => {
setShowLeave(false);
};
const handleUpdateWorkspaceName = () => {
workspace && updateWorkspaceMeta(workspace.id, { name: workspaceName });
};
const fileChange = async (file: File) => {
// setUploading(true);
console.log('file: ', file);
setUploading(true);
// const blob = new Blob([file], { type: file.type });
// const blobId = await getDataCenter()
// .then(dc => dc.apis.uploadBlob({ blob }))
@@ -81,7 +60,7 @@ export const GeneralPage = ({
// });
// if (blobId) {
// currentWorkspace?.meta.setAvatar(blobId);
// workspaces[workspace.id]?.meta.setAvatar(blobId);
// // workspaces[workspace.id]?.meta.setAvatar(blobId);
// setUploading(false);
// debouncedRefreshWorkspacesMeta();
// }
@@ -89,18 +68,17 @@ export const GeneralPage = ({
return workspace ? (
<div>
<StyledSettingH2 marginTop={56}>Workspace Avatar</StyledSettingH2>
<StyledSettingH2 marginTop={56}>Workspace Icon</StyledSettingH2>
<StyledSettingAvatarContent>
<StyledSettingAvatar
alt="workspace avatar"
src={
workspaces[workspace.id]?.meta.avatar
? '/api/blob/' + workspaces[workspace.id]?.meta.avatar
: ''
}
<div
style={{
float: 'left',
marginRight: '5px',
}}
>
{workspaces[workspace.id]?.meta.name[0]}
</StyledSettingAvatar>
<WorkspaceAvatar size={60} name={workspace.name} />
</div>
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange}
@@ -108,31 +86,45 @@ export const GeneralPage = ({
<Button loading={uploading}>Upload</Button>
</Upload>
{/* TODO: add upload logic */}
{/* {isOwner ? (
<StyledAvatarUploadBtn shape="round">upload</StyledAvatarUploadBtn>
) : null} */}
{/* <Button shape="round">remove</Button> */}
</StyledSettingAvatarContent>
<StyledSettingH2 marginTop={36}>Workspace Name</StyledSettingH2>
<StyledSettingH2 marginTop={20}>Workspace Name</StyledSettingH2>
<StyledSettingInputContainer>
<Input
width={327}
value={workspaceName}
placeholder="Workspace Name"
disabled={!isOwner}
maxLength={14}
minLength={1}
onChange={handleChangeWorkSpaceName}
></Input>
<TextButton
onClick={() => {
handleUpdateWorkspaceName();
}}
style={{ marginLeft: '10px' }}
>
</TextButton>
</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>
<Input
width={327}
disabled
value={owner.name}
placeholder="Workspace Owner"
></Input>
<code>{workspace.type} </code>
</StyledSettingInputContainer>
<StyledDeleteButtonContainer>
{isOwner ? (
@@ -143,9 +135,7 @@ export const GeneralPage = ({
<WorkspaceDelete
open={showDelete}
onClose={handleCloseDelete}
workspaceName={workspaceName}
workspaceId={workspace.id}
nextWorkSpaceId={nextWorkSpaceId}
workspace={workspace}
/>
</>
) : (
@@ -158,7 +148,6 @@ export const GeneralPage = ({
onClose={handleCloseLeave}
workspaceName={workspaceName}
workspaceId={workspace.id}
nextWorkSpaceId={nextWorkSpaceId}
/>
</>
)}

View File

@@ -11,27 +11,25 @@ import {
import { useState } from 'react';
import { ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import { getDataCenter } from '@affine/datacenter';
import { useRouter } from 'next/router';
import { useAppState } from '@/providers/app-state-provider';
import {
deleteWorkspace,
getWorkspaces,
Workspace,
} from '@/hooks/mock-data/mock';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
workspaceName: string;
workspaceId: string;
nextWorkSpaceId: string;
workspace: Workspace;
}
export const WorkspaceDelete = ({
open,
onClose,
workspaceId,
workspaceName,
nextWorkSpaceId,
workspace,
}: WorkspaceDeleteProps) => {
const [deleteStr, setDeleteStr] = useState<string>('');
const { refreshWorkspacesMeta } = useAppState();
const router = useRouter();
const handlerInputChange = (workspaceName: string) => {
@@ -42,8 +40,14 @@ export const WorkspaceDelete = ({
// const dc = await getDataCenter();
// await dc.apis.deleteWorkspace({ id: workspaceId });
// router.push(`/workspace/${nextWorkSpaceId}`);
// refreshWorkspacesMeta();
// onClose();
deleteWorkspace(workspace.id);
const workspaceList = getWorkspaces();
if (workspaceList.length) {
router.push(`/workspace/${workspaceList[0].id}`);
} else {
router.push(`/workspace`);
}
onClose();
};
return (
@@ -51,11 +55,20 @@ export const WorkspaceDelete = ({
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>Delete Workspace</StyledModalHeader>
<StyledTextContent>
This action cannot be undone. This will permanently delete (
<StyledWorkspaceName>{workspaceName}</StyledWorkspaceName>) along with
all its content.
</StyledTextContent>
{workspace.type === 'local' ? (
<StyledTextContent>
Deleting (
<StyledWorkspaceName>{workspace.name}</StyledWorkspaceName>) cannot
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>
<Input
onChange={handlerInputChange}

View File

@@ -1 +1 @@
export * from './delete';
export * from './Delete';

View File

@@ -16,7 +16,8 @@ export const StyledModalHeader = styled('div')(({ theme }) => {
width: '460px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
textAlign: 'left',
paddingLeft: '20px',
color: theme.colors.popoverColor,
};
});
@@ -32,7 +33,7 @@ export const StyledTextContent = styled('div')(() => {
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'center',
textAlign: 'left',
};
});
@@ -41,7 +42,7 @@ export const StyledInputContent = styled('div')(() => {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '40px 0 24px 0',
margin: '20px 0 24px 0',
};
});

View File

@@ -1 +1 @@
export * from './general';
export * from './General';

View File

@@ -7,8 +7,7 @@ import {
} from './style';
import { ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import { getDataCenter } from '@affine/datacenter';
import { useRouter } from 'next/router';
// import { getDataCenter } from '@affine/datacenter';
import { useAppState } from '@/providers/app-state-provider';
interface WorkspaceDeleteProps {
@@ -16,22 +15,20 @@ interface WorkspaceDeleteProps {
onClose: () => void;
workspaceName: string;
workspaceId: string;
nextWorkSpaceId: string;
}
export const WorkspaceLeave = ({
open,
onClose,
nextWorkSpaceId,
workspaceId,
}: WorkspaceDeleteProps) => {
const router = useRouter();
// const router = useRouter();
const { refreshWorkspacesMeta } = useAppState();
const handleLeave = async () => {
// const dc = await getDataCenter();
// await dc.apis.leaveWorkspace({ id: workspaceId });
router.push(`/workspace/${nextWorkSpaceId}`);
refreshWorkspacesMeta();
// // router.push(`/workspace/${nextWorkSpaceId}`);
// refreshWorkspacesMeta();
onClose();
};

View File

@@ -1 +1 @@
export * from './leave';
export * from './Leave';

View File

@@ -9,7 +9,7 @@ export const StyledSettingInputContainer = styled('div')(() => {
export const StyledDeleteButtonContainer = styled('div')(() => {
return {
marginTop: '154px',
marginTop: '30px',
};
});
@@ -26,3 +26,12 @@ export const StyledSettingAvatarContent = styled('div')(() => {
export const StyledSettingAvatar = styled(MuiAvatar)(() => {
return { height: '72px', width: '72px', marginRight: '24px' };
});
export const StyledMemberWarp = styled('div')(() => {
return {
display: 'flex',
height: '500px',
flexDirection: 'column',
padding: '60px 0',
};
});

View File

@@ -1 +1 @@
export * from './workspace-setting';
export * from './WorkspaceSetting';

View File

@@ -117,6 +117,7 @@ export const StyledMemberTitleContainer = styled('div')(() => {
display: 'flex',
marginTop: '60px',
fontWeight: '500',
flex: 1,
};
});
@@ -199,11 +200,12 @@ export const StyledMoreVerticalButton = styled('button')(() => {
export const StyledPublishExplanation = styled('div')(() => {
return {
marginTop: '56px',
paddingRight: '48px',
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
flex: 1,
marginTop: '60px',
};
});
@@ -213,7 +215,8 @@ export const StyledPublishCopyContainer = styled('div')(() => {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: '38px',
marginBottom: '20px',
paddingTop: '20px',
};
});
@@ -226,5 +229,7 @@ export const StyledCopyButtonContainer = styled('div')(() => {
export const StyledPublishContent = styled('div')(() => {
return {
height: '494px',
display: 'flex',
flexDirection: 'column',
};
});

View File

@@ -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>
);
};

View File

@@ -13,7 +13,7 @@ import {
import { WorkspaceSetting } from '@/components/workspace-setting';
import { useCallback, useEffect, useState } from 'react';
import { getDataCenter, WorkspaceType } from '@affine/datacenter';
import { useModal } from '@/providers/global-modal-provider';
import { useModal } from '@/providers/GlobalModalProvider';
export type WorkspaceDetails = Record<
string,
@@ -27,8 +27,7 @@ type SelectorPopperContentProps = {
export const SelectorPopperContent = ({
isShow,
}: SelectorPopperContentProps) => {
const { user, workspacesMeta, workspaces, refreshWorkspacesMeta } =
useAppState();
const { user, workspacesMeta, refreshWorkspacesMeta } = useAppState();
const [settingWorkspaceId, setSettingWorkspaceId] = useState<string | null>(
null
);
@@ -115,13 +114,9 @@ export const SelectorPopperContent = ({
type={workspace.type}
key={workspace.id}
id={workspace.id}
icon={
(workspaces[workspace.id]?.meta.avatar &&
`/api/blob/${workspaces[workspace.id]?.meta.avatar}`) ||
`loading...`
}
icon={`loading...`}
onClickSetting={handleClickSettingWorkspace}
name={workspaces[workspace.id]?.meta.name || `loading...`}
name={`loading...`}
memberCount={workSpaceDetails[workspace.id]?.memberCount || 1}
/>
) : null;
@@ -130,16 +125,16 @@ export const SelectorPopperContent = ({
<CreateWorkspaceItem />
{settingWorkspace ? (
<WorkspaceSetting
isShow={Boolean(settingWorkspaceId)}
isShow={false}
onClose={handleCloseWorkSpace}
workspace={settingWorkspace}
owner={
(settingWorkspaceId &&
workSpaceDetails[settingWorkspaceId]?.owner) || {
id: user.id,
name: user.name,
}
}
// workspace={settingWorkspace}
// owner={
// (settingWorkspaceId &&
// workSpaceDetails[settingWorkspaceId]?.owner) || {
// id: user.id,
// name: user.name,
// }
// }
/>
) : null}
<StyledDivider />

View File

@@ -1 +1 @@
export * from './workspace-create';
export * from './WorkspaceCreate';

View File

@@ -1,6 +1,6 @@
import { useModal } from '@/providers/global-modal-provider';
import { useModal } from '@/providers/GlobalModalProvider';
import { styled } from '@/styles';
import { AffineIcon } from '../../icons/icons';
import { AffineIcon } from '../../icons/Icons';
import {
WorkspaceItemAvatar,
LoginItemWrapper,

View File

@@ -1,53 +1,52 @@
import { Popper } from '@/ui/popper';
import { Avatar, WorkspaceName, SelectorWrapper } from './styles';
import { SelectorPopperContent } from './SelectorPopperContent';
import { useState } from 'react';
import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceType } from '@affine/datacenter';
import { AffineIcon } from '../icons/icons';
import { useEffect, useState } from 'react';
import { WorkspaceModal } from '@/components/workspace-modal';
import { WorkspaceAvatar } from '@/components/workspace-avatar';
import { useTemporaryHelper } from '@/providers/temporary-helper-provider';
export const WorkspaceSelector = () => {
const [isShow, setIsShow] = useState(false);
const { currentWorkspace, workspacesMeta, currentWorkspaceId, user } =
useAppState();
const workspaceMeta = workspacesMeta.find(
meta => String(meta.id) === String(currentWorkspaceId)
);
const [workspaceListShow, setWorkspaceListShow] = useState(false);
const { currentWorkspace, workspaceMetaList } = useTemporaryHelper();
useEffect(() => {
if (workspaceMetaList.length === 0) {
setWorkspaceListShow(true);
}
}, [workspaceMetaList]);
return (
<Popper
content={<SelectorPopperContent isShow={isShow} />}
zIndex={1000}
placement="bottom-start"
trigger="click"
onVisibleChange={setIsShow}
>
<SelectorWrapper data-testid="current-workspace">
<>
<SelectorWrapper
onClick={() => {
setWorkspaceListShow(true);
}}
data-testid="current-workspace"
>
<Avatar
alt="Affine"
data-testid="workspace-avatar"
src={
workspaceMeta?.type === WorkspaceType.Private
? user
? user.avatar_url
: ''
: currentWorkspace?.meta.avatar &&
`/api/blob/${currentWorkspace?.meta.avatar}`
}
src={currentWorkspace?.avatar}
>
{workspaceMeta?.type === WorkspaceType.Private && user ? (
user?.name[0]
) : (
<AffineIcon />
)}
<div
style={{
float: 'left',
}}
>
<WorkspaceAvatar
size={28}
name={currentWorkspace?.name ?? 'AFFiNE'}
/>
</div>
</Avatar>
<WorkspaceName data-testid="workspace-name">
{workspaceMeta?.type === WorkspaceType.Private
? user
? user.name
: 'AFFiNE'
: currentWorkspace?.meta.name || 'AFFiNE'}
{currentWorkspace?.name ?? 'AFFiNE'}
</WorkspaceName>
</SelectorWrapper>
</Popper>
<WorkspaceModal
open={workspaceListShow}
onClose={() => {
setWorkspaceListShow(false);
}}
></WorkspaceModal>
</>
);
};

View File

@@ -21,15 +21,17 @@ import {
ImportIcon,
TrashIcon,
AddIcon,
SettingsIcon,
} from '@blocksuite/icons';
import Link from 'next/link';
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 { IconButton } from '@/ui/button';
import useLocalStorage from '@/hooks/use-local-storage';
import usePageMetaList from '@/hooks/use-page-meta-list';
import { usePageHelper } from '@/hooks/use-page-helper';
import { WorkspaceSetting } from '@/components/workspace-setting';
import { useTranslation } from 'react-i18next';
const FavoriteList = ({ showList }: { showList: boolean }) => {
@@ -74,6 +76,8 @@ export const WorkSpaceSliderBar = () => {
const [showTip, setShowTip] = useState(false);
const [show, setShow] = useLocalStorage('AFFiNE_SLIDE_BAR', false, true);
const [showWorkspaceSetting, setShowWorkspaceSetting] = useState(false);
const paths = {
all: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/all` : '',
favorite: currentWorkspaceId
@@ -146,6 +150,20 @@ export const WorkSpaceSliderBar = () => {
</IconButton>
</StyledListItem>
<FavoriteList showList={showSubFavorite} />
<StyledListItem
onClick={() => {
setShowWorkspaceSetting(true);
}}
>
<SettingsIcon /> Setting
</StyledListItem>
<WorkspaceSetting
isShow={showWorkspaceSetting}
onClose={() => {
setShowWorkspaceSetting(false);
}}
/>
<StyledListItem
onClick={() => {

View File

@@ -62,6 +62,7 @@ export const StyledListItem = styled.div<{
color: active ? theme.colors.primaryColor : theme.colors.popoverColor,
paddingLeft: '12px',
borderRadius: '5px',
cursor: 'pointer',
...displayFlex('flex-start', 'center'),
...(disabled
? {

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');
}

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": "ডিস্ক এ সিঙ্ক করুন"
}
{}

View File

@@ -34,7 +34,7 @@
"Delete page?": "Delete page?",
"Delete permanently?": "Delete permanently?",
"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",
"Permanently deleted": "Permanently deleted",
"restored": "{{title}} restored",
@@ -54,12 +54,26 @@
"Strikethrough": "Strikethrough",
"Inline code": "Inline code",
"Code block": "Code block",
"Link": "Link",
"Hyperlink(with selected text)": "Hyperlink(with selected text)",
"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"
"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"
}

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"
}
{}

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."
}
}
{}

View File

@@ -1,29 +1,65 @@
{
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.": "",
"Sync to Disk": "同步到磁盘",
"Share": "分享",
"WarningTips": {
"IsNotfsApiSupported": "欢迎来到AFFiNE 的演示界面。您可以使用最新版本的基于Chrome的浏览器如Chrome/Edge将数据同步到磁盘来进行保存",
"IsNotLocalWorkspace": "欢迎来到AFFiNE 的演示界面,您可以同步到磁盘来进行保存操作。",
"DoNotStore": "AFFiNE 正在积极开发中,当前版本不稳定。请不要存储信息或数据。"
},
"ComingSoon": "布局设置即将到来",
"Layout": "布局",
"Comment": "评论",
"Settings": "设置",
"Duplicate Page": "复制页面",
"Copy Page Link": "复制页面链接",
"Language": "当前语言",
"Clear Workspace": "清空工作区域",
"Export As Markdown": "导出 markdown",
"Export As HTML": "导出 HTML",
"Export As PDF (Unsupported)": "导出 PDF (暂不支持)",
"Import Workspace": "导入 Workspace",
"Export Workspace": "导出 Workspace",
"Last edited by": "最后编辑者为 {{name}}",
"Logout": "退出登录",
"Quick search": "快速搜索",
"All pages": "全部页面",
"Favourites": "收藏夹",
"No item": "没有项目",
"Import": "导入",
"Trash": "回收站",
"New Page": "新建文章",
"New Keyword Page": "新建 '{{query}}' 为标题的文章",
"Find 0 result": "找到 0 个结果",
"Find results": "找到 {{number}} 个结果",
"Collapse sidebar": "关闭侧边栏",
"Expand sidebar": "展开侧边栏",
"Removed from Favourites": "已从收藏中移除",
"Remove from favourites": "从收藏中移除",
"Added to Favourites": "已添加到收藏",
"Add to favourites": "添加到收藏",
"Paper": "文章",
"Edgeless": "无边模式",
"Switch to": "跳转到",
"Convert to ": "转换成 ",
"Page": "文章",
"Export": "导出",
"Export to HTML": "导出到 HTML",
"Export to Markdown": "导出到 Markdown",
"Delete": "删除",
"Turn into": "转换为",
"Add A Below Block": "在下方添加一个新块",
"Divide Here As A New Group": "从这里划分一个新组"
"Title": "标题",
"Untitled": "无标题",
"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"
}

View File

@@ -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您可以通過「同步到磁盤」來保存更改"
}
}
{}

View File

@@ -10,17 +10,18 @@ import '../utils/print-build-info';
import ProviderComposer from '@/components/provider-composer';
import type { PropsWithChildren, ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
import { AppStateProvider } from '@/providers/app-state-provider/provider';
import ConfirmProvider from '@/providers/confirm-provider';
import { ModalProvider } from '@/providers/global-modal-provider';
import { AppStateProvider } from '@/providers/app-state-provider/Provider';
import ConfirmProvider from '@/providers/ConfirmProvider';
import { ModalProvider } from '@/providers/GlobalModalProvider';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useAppState } from '@/providers/app-state-provider';
import { PageLoading } from '@/components/loading';
import Head from 'next/head';
import '@/libs/i18n';
import TemporaryHelperProvider from '@/providers/temporary-helper-provider';
const ThemeProvider = dynamic(() => import('@/providers/themeProvider'), {
const ThemeProvider = dynamic(() => import('@/providers/ThemeProvider'), {
ssr: false,
});
@@ -52,6 +53,7 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => {
<Logger />
<ProviderComposer
contexts={[
<TemporaryHelperProvider key="TemporaryHelperProvider" />,
<ThemeProvider key="ThemeProvider" />,
<AppStateProvider key="appStateProvider" />,
<ModalProvider key="ModalProvider" />,

View File

@@ -1,7 +1,7 @@
import type { NextPage } from 'next';
const Home: NextPage = () => {
return <div>Home Page</div>;
return <div title="Home Page"></div>;
};
export default Home;

View File

@@ -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,
};
});

View File

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

View File

@@ -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 dynamic from 'next/dynamic';
import { getDataCenter } from '@affine/datacenter';
@@ -8,12 +8,20 @@ import type {
CreateEditorHandler,
LoadWorkspaceHandler,
} from './context';
import { Page, Workspace as StoreWorkspace } from '@blocksuite/store';
import { Page } from '@blocksuite/store';
import { EditorContainer } from '@blocksuite/editor';
const DynamicBlocksuite = dynamic(() => import('./dynamic-blocksuite'), {
const DynamicBlocksuite = dynamic(() => import('./DynamicBlocksuite'), {
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 }) => {
const refreshWorkspacesMeta = async () => {
const dc = await getDataCenter();
@@ -30,43 +38,10 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
currentWorkspace: null,
currentPage: 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,
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] =
useState<CreateEditorHandler>();
@@ -85,7 +60,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
return state.currentWorkspace;
}
const workspace =
(await loadWorkspaceHandler?.(workspaceId, state.user)) || null;
(await loadWorkspaceHandler(workspaceId, state.user)) || null;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
@@ -103,6 +78,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
}));
return workspace;
};
const loadPage = useRef<AppStateContext['loadPage']>(() =>
Promise.resolve(null)
);
@@ -147,17 +123,6 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
setState(state => ({ ...state, editor }));
};
useEffect(() => {
if (!loadWorkspaceHandler) {
return;
}
setState(state => ({
...state,
workspacesMeta: [],
synced: true,
}));
}, [loadWorkspaceHandler]);
const context = useMemo(
() => ({
...state,
@@ -172,10 +137,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
return (
<AppState.Provider value={context}>
<DynamicBlocksuite
setLoadWorkspaceHandler={setLoadWorkspaceHandler}
setCreateEditorHandler={setCreateEditorHandler}
/>
<DynamicBlocksuite setCreateEditorHandler={setCreateEditorHandler} />
{children}
</AppState.Provider>
);

View File

@@ -5,6 +5,7 @@ import type {
Workspace as StoreWorkspace,
} from '@blocksuite/store';
import type { EditorContainer } from '@blocksuite/editor';
export type LoadWorkspaceHandler = (
workspaceId: string,
user?: AccessTokenMessage | null
@@ -20,7 +21,7 @@ export interface AppStateValue {
currentPage: StorePage | null;
workspaces: Record<string, StoreWorkspace | null>;
// workspaces: Record<string, StoreWorkspace | null>;
editor: EditorContainer | null;
synced: boolean;
@@ -40,14 +41,10 @@ export interface AppStateContext extends AppStateValue {
export const AppState = createContext<AppStateContext>({
user: null,
workspacesMeta: [],
currentWorkspaceId: '',
currentWorkspace: null,
currentPage: null,
editor: null,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setState: () => {},
createEditor: undefined,
@@ -57,7 +54,6 @@ export const AppState = createContext<AppStateContext>({
synced: false,
// eslint-disable-next-line @typescript-eslint/no-empty-function
refreshWorkspacesMeta: () => {},
workspaces: {},
});
export const useAppState = () => {

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ Let us know what you think of this latest version.
5. You can self-host locally with Docker.
```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?**
@@ -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**
- 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)
- Copy and paste **images** into your pages, resize them and add captions
- Add horizontal line dividers to your text with `---` and `***`

View File

@@ -12,6 +12,7 @@ export type ConfirmProps = {
title?: string;
content?: string;
confirmText?: string;
cancelText?: string;
// TODO: Confirm button's color should depend on confirm type
confirmType?: 'primary' | 'warning' | 'danger';
onConfirm?: () => void;
@@ -25,6 +26,7 @@ export const Confirm = ({
confirmType,
onConfirm,
onCancel,
cancelText = 'Cancel',
}: ConfirmProps) => {
const [open, setOpen] = useState(true);
const { t } = useTranslation();
@@ -50,7 +52,7 @@ export const Confirm = ({
}}
style={{ marginRight: '24px' }}
>
{t('Cancel')}
{cancelText === 'Cancel' ? t('Cancel') : cancelText}
</Button>
<Button
type={confirmType}

View File

@@ -1,4 +1,10 @@
import { InputHTMLAttributes, useEffect, useState } from 'react';
import {
InputHTMLAttributes,
useEffect,
useState,
FocusEventHandler,
KeyboardEventHandler,
} from 'react';
import { StyledInput } from './style';
type inputProps = {
@@ -9,8 +15,8 @@ type inputProps = {
maxLength?: number;
minLength?: number;
onChange?: (value: string) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onBlur?: (e: any) => void;
onBlur?: FocusEventHandler<HTMLInputElement>;
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
};
export const Input = (props: inputProps) => {
@@ -23,6 +29,7 @@ export const Input = (props: inputProps) => {
width = 260,
onChange,
onBlur,
onKeyDown,
} = props;
const [value, setValue] = useState<string>(valueProp || '');
const handleChange: InputHTMLAttributes<HTMLInputElement>['onChange'] = e => {
@@ -37,6 +44,10 @@ export const Input = (props: inputProps) => {
const handleBlur: InputHTMLAttributes<HTMLInputElement>['onBlur'] = e => {
onBlur && onBlur(e);
};
const handleKeyDown: InputHTMLAttributes<HTMLInputElement>['onKeyDown'] =
e => {
onKeyDown && onKeyDown(e);
};
useEffect(() => {
setValue(valueProp || '');
}, [valueProp]);
@@ -50,6 +61,7 @@ export const Input = (props: inputProps) => {
minLength={minLength}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
></StyledInput>
);
};

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;
}

View File

@@ -3,3 +3,5 @@ export { isDev } from './env';
export * from './useragent';
export * from './tools';
export * from './colors';

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