refactor!: next generation AFFiNE code structure (#1176)

This commit is contained in:
Himself65
2023-03-01 01:40:01 -06:00
committed by GitHub
parent 2dcccc772c
commit e0481d29ad
270 changed files with 8308 additions and 6829 deletions
+19
View File
@@ -0,0 +1,19 @@
# Affine Official Workspace Component
This component need specific configuration to work properly.
## Configuration
### SWR
Each component use SWR to fetch data from the API. You need to provide a configuration to SWR to make it work.
```tsx
const Wrapper = () => {
return (
<AffineSWRConfigProvider>
<Component />
</AffineSWRConfigProvider>
);
};
```
@@ -0,0 +1,129 @@
import { RequestError } from '@affine/datacenter';
import { NextRouter } from 'next/router';
import React, { Component, ErrorInfo } from 'react';
import { BlockSuiteWorkspace } from '../../shared';
export type AffineErrorBoundaryProps = React.PropsWithChildren<{
router: NextRouter;
}>;
export class PageNotFoundError extends TypeError {
readonly workspace: BlockSuiteWorkspace;
readonly pageId: string;
constructor(workspace: BlockSuiteWorkspace, pageId: string) {
super();
this.workspace = workspace;
this.pageId = pageId;
}
}
export class WorkspaceNotFoundError extends TypeError {
readonly workspaceId: string;
constructor(workspaceId: string) {
super();
this.workspaceId = workspaceId;
}
}
export class QueryParamError extends TypeError {
readonly targetKey: string;
readonly query: unknown;
constructor(targetKey: string, query: unknown) {
super();
this.targetKey = targetKey;
this.query = query;
}
}
export class Unreachable extends Error {
constructor(message?: string) {
super(message);
}
}
type AffineError =
| QueryParamError
| Unreachable
| WorkspaceNotFoundError
| PageNotFoundError
| RequestError
| Error;
interface AffineErrorBoundaryState {
error: AffineError | null;
}
export class AffineErrorBoundary extends Component<
AffineErrorBoundaryProps,
AffineErrorBoundaryState
> {
public state: AffineErrorBoundaryState = {
error: null,
};
public static getDerivedStateFromError(
error: AffineError
): AffineErrorBoundaryState {
return { error };
}
public componentDidCatch(error: AffineError, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
public render() {
if (this.state.error) {
const error = this.state.error;
if (error instanceof PageNotFoundError) {
return (
<>
<h1>Sorry.. there was an error</h1>
<>
<span> Page error </span>
<span>
Cannot find page {error.pageId} in workspace{' '}
{error.workspace.meta.name}
</span>
<button
onClick={() => {
this.props.router
.replace({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
workspaceId: error.workspace.room,
pageId: error.workspace.meta.pageMetas[0].id,
},
})
.then(() => {
this.setState({ error: null });
});
}}
>
{' '}
refresh{' '}
</button>
</>
</>
);
} else if (error instanceof RequestError) {
return (
<>
<h1>Sorry.. there was an error</h1>
{error.message}
</>
);
}
return (
<>
<h1>Sorry.. there was an error</h1>
</>
);
}
return this.props.children;
}
}
@@ -0,0 +1,64 @@
import { IconButton, Modal, ModalWrapper } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { CloseIcon } from '@blocksuite/icons';
import React from 'react';
import { useCurrentUser } from '../../../hooks/current/use-current-user';
import { AffineRemoteWorkspace } from '../../../shared';
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
interface EnableAffineCloudModalProps {
workspace: AffineRemoteWorkspace;
open: boolean;
onConfirm: () => void;
onClose: () => void;
}
export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
onConfirm,
open,
onClose,
}) => {
const { t } = useTranslation();
const user = useCurrentUser();
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={560} height={292}>
<Header>
<IconButton
onClick={() => {
onClose();
}}
>
<CloseIcon />
</IconButton>
</Header>
<Content>
<ContentTitle>{t('Enable AFFiNE Cloud')}?</ContentTitle>
<StyleTips>{t('Enable AFFiNE Cloud Description')}</StyleTips>
{/* <StyleTips>{t('Retain cached cloud data')}</StyleTips> */}
<div>
<StyleButton
shape="round"
type="primary"
onClick={() => {
onConfirm();
}}
>
{user ? t('Enable') : t('Sign in and Enable')}
</StyleButton>
<StyleButton
shape="round"
onClick={() => {
onClose();
}}
>
{t('Not now')}
</StyleButton>
</div>
</Content>
</ModalWrapper>
</Modal>
);
};
@@ -0,0 +1,40 @@
import { Button, styled } from '@affine/component';
export const Header = styled('div')({
height: '44px',
display: 'flex',
flexDirection: 'row-reverse',
paddingRight: '10px',
paddingTop: '10px',
flexShrink: 0,
});
export const Content = styled('div')({
textAlign: 'center',
});
export const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
});
export const StyleTips = styled('div')(() => {
return {
userSelect: 'none',
width: '400px',
margin: 'auto',
marginBottom: '32px',
marginTop: '12px',
};
});
export const StyleButton = styled(Button)(() => {
return {
width: '284px',
display: 'block',
margin: 'auto',
marginTop: '16px',
};
});
@@ -0,0 +1,74 @@
import { IconButton, Modal, ModalWrapper } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { CloseIcon } from '@blocksuite/icons';
import React from 'react';
import { useCurrentUser } from '../../../hooks/current/use-current-user';
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
export type TransformWorkspaceToAffineModalProps = {
open: boolean;
onClose: () => void;
onConform: () => void;
};
export const TransformWorkspaceToAffineModal: React.FC<
TransformWorkspaceToAffineModalProps
> = ({ open, onClose, onConform }) => {
const { t } = useTranslation();
const user = useCurrentUser();
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={560} height={292}>
<Header>
<IconButton
onClick={() => {
onClose();
}}
>
<CloseIcon />
</IconButton>
</Header>
<Content>
<ContentTitle>{t('Enable AFFiNE Cloud')}?</ContentTitle>
<StyleTips>{t('Enable AFFiNE Cloud Description')}</StyleTips>
{/* <StyleTips>{t('Retain cached cloud data')}</StyleTips> */}
<div>
<StyleButton
shape="round"
type="primary"
onClick={async () => {
onConform();
// setLoading(true);
// if (user || (await login())) {
// if (currentWorkspace) {
// const workspace = await dataCenter.enableWorkspaceCloud(
// currentWorkspace
// );
// toast(t('Enabled success'));
//
// if (workspace) {
// router.push(`/workspace/${workspace.id}/setting`);
// }
// }
// }
// setLoading(false);
}}
>
{user ? t('Enable') : t('Sign in and Enable')}
</StyleButton>
<StyleButton
shape="round"
onClick={() => {
onClose();
}}
>
{t('Not now')}
</StyleButton>
</div>
</Content>
</ModalWrapper>
</Modal>
);
};
@@ -0,0 +1,40 @@
import { Button, styled } from '@affine/component';
export const Header = styled('div')({
height: '44px',
display: 'flex',
flexDirection: 'row-reverse',
paddingRight: '10px',
paddingTop: '10px',
flexShrink: 0,
});
export const Content = styled('div')({
textAlign: 'center',
});
export const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
});
export const StyleTips = styled('div')(() => {
return {
userSelect: 'none',
width: '400px',
margin: 'auto',
marginBottom: '32px',
marginTop: '12px',
};
});
export const StyleButton = styled(Button)(() => {
return {
width: '284px',
display: 'block',
margin: 'auto',
marginTop: '16px',
};
});
@@ -0,0 +1,159 @@
import { useTranslation } from '@affine/i18n';
import React, {
MouseEvent,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { preload } from 'swr';
import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner';
import { fetcher, QueryKey } from '../../../plugins/affine/fetcher';
import {
AffineOfficialWorkspace,
SettingPanel,
settingPanel,
} from '../../../shared';
import { CollaborationPanel } from './panel/collaboration';
import { ExportPanel } from './panel/export';
import { GeneralPanel } from './panel/general';
import { PublishPanel } from './panel/publish';
import {
StyledIndicator,
StyledSettingContainer,
StyledSettingContent,
StyledTabButtonWrapper,
WorkspaceSettingTagItem,
} from './style';
export type WorkspaceSettingDetailProps = {
workspace: AffineOfficialWorkspace;
currentTab: SettingPanel;
onChangeTab: (tab: SettingPanel) => void;
onDeleteWorkspace: () => void;
onTransferWorkspace: (targetWorkspaceId: string) => void;
};
export type PanelProps = WorkspaceSettingDetailProps;
const panelMap = {
[settingPanel.General]: {
name: 'General',
ui: GeneralPanel,
},
[settingPanel.Collaboration]: {
name: 'Collaboration',
ui: CollaborationPanel,
},
[settingPanel.Publish]: {
name: 'Publish',
ui: PublishPanel,
},
[settingPanel.Export]: {
name: 'Export',
ui: ExportPanel,
},
} satisfies {
[Key in SettingPanel]: {
name: string;
ui: React.FC<PanelProps>;
};
};
function assertInstanceOf<T, U extends T>(
obj: T,
type: new (...args: any[]) => U
): asserts obj is U {
if (!(obj instanceof type)) {
throw new Error('Object is not instance of type');
}
}
export const WorkspaceSettingDetail: React.FC<
WorkspaceSettingDetailProps
> = props => {
const {
workspace,
currentTab,
onChangeTab,
// onDeleteWorkspace,
// onTransferWorkspace,
} = props;
const isAffine = workspace.flavour === 'affine';
const isOwner = useIsWorkspaceOwner(workspace);
if (!(workspace.flavour === 'affine' || workspace.flavour === 'local')) {
throw new Error('Unsupported workspace flavour');
}
if (!(currentTab in panelMap)) {
throw new Error('Invalid activeTab: ' + currentTab);
}
const { t } = useTranslation();
const workspaceId = workspace.id;
useEffect(() => {
if (isAffine && isOwner) {
preload([QueryKey.getMembers, workspaceId], fetcher);
}
}, [isAffine, isOwner, workspaceId]);
const containerRef = useRef<HTMLDivElement | null>(null);
const indicatorRef = useRef<HTMLDivElement | null>(null);
const startTransaction = useCallback(() => {
if (indicatorRef.current && containerRef.current) {
const indicator = indicatorRef.current;
const activeTabElement = containerRef.current.querySelector(
`[data-tab-key="${currentTab}"]`
);
assertInstanceOf(activeTabElement, HTMLElement);
requestAnimationFrame(() => {
indicator.style.left = `${activeTabElement.offsetLeft}px`;
indicator.style.width = `${activeTabElement.offsetWidth}px`;
});
}
}, [currentTab]);
const handleTabClick = useCallback(
(event: MouseEvent<HTMLElement>) => {
assertInstanceOf(event.target, HTMLElement);
const key = event.target.getAttribute('data-tab-key');
if (!key || !(key in panelMap)) {
throw new Error('data-tab-key is invalid: ' + key);
}
onChangeTab(key as SettingPanel);
startTransaction();
},
[onChangeTab, startTransaction]
);
const Component = useMemo(() => panelMap[currentTab].ui, [currentTab]);
return (
<StyledSettingContainer
aria-label="workspace-setting-detail"
ref={containerRef}
>
<StyledTabButtonWrapper>
{Object.entries(panelMap).map(([key, value]) => {
if (!isAffine && key === 'Sync') {
return null;
}
return (
<WorkspaceSettingTagItem
key={key}
isActive={currentTab === key}
data-tab-key={key}
onClick={handleTabClick}
>
{t(value.name)}
</WorkspaceSettingTagItem>
);
})}
<StyledIndicator
ref={ref => {
indicatorRef.current = ref;
startTransaction();
}}
/>
</StyledTabButtonWrapper>
<StyledSettingContent>
<Component {...props} key={currentTab} data-tab-ui={currentTab} />
</StyledSettingContent>
</StyledSettingContainer>
);
};
@@ -0,0 +1,227 @@
import {
Button,
IconButton,
Menu,
MenuItem,
toast,
Wrapper,
} from '@affine/component';
import { PermissionType } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n';
import {
DeleteTemporarilyIcon,
EmailIcon,
MoreVerticalIcon,
} from '@blocksuite/icons';
import React, { useCallback, useState } from 'react';
import { lockMutex } from '../../../../../atoms';
import { useMembers } from '../../../../../hooks/affine/use-members';
import { transformWorkspace } from '../../../../../plugins';
import {
AffineRemoteWorkspace,
LocalWorkspace,
RemWorkspaceFlavour,
} from '../../../../../shared';
import { Unreachable } from '../../../affine-error-eoundary';
import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal';
import { PanelProps } from '../../index';
import { InviteMemberModal } from './invite-member-modal';
import {
StyledMemberAvatar,
StyledMemberButtonContainer,
StyledMemberContainer,
StyledMemberEmail,
StyledMemberInfo,
StyledMemberListContainer,
StyledMemberListItem,
StyledMemberName,
StyledMemberNameContainer,
StyledMemberRoleContainer,
StyledMemberTitleContainer,
StyledMoreVerticalButton,
StyledMoreVerticalDiv,
} from './style';
const AffineRemoteCollaborationPanel: React.FC<
Omit<PanelProps, 'workspace'> & {
workspace: AffineRemoteWorkspace;
}
> = ({ workspace }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const { t } = useTranslation();
const { members, removeMember } = useMembers(workspace.id);
return (
<>
<StyledMemberContainer>
<ul>
<StyledMemberTitleContainer>
<StyledMemberNameContainer>
{t('Users')} ({members.length})
</StyledMemberNameContainer>
<StyledMemberRoleContainer>
{t('Access level')}
</StyledMemberRoleContainer>
<div style={{ width: '24px', paddingRight: '48px' }}></div>
</StyledMemberTitleContainer>
</ul>
<StyledMemberListContainer>
{members.length > 0 && (
<>
{members
.sort((b, a) => a.type - b.type)
.map((member, index) => {
const user = {
avatar_url: '',
id: '',
name: '',
...member.user,
};
return (
<StyledMemberListItem key={index}>
<StyledMemberNameContainer>
<StyledMemberAvatar
alt="member avatar"
src={user.avatar_url}
>
<EmailIcon />
</StyledMemberAvatar>
<StyledMemberInfo>
<StyledMemberName>{user.name}</StyledMemberName>
<StyledMemberEmail>
{member.user.email}
</StyledMemberEmail>
</StyledMemberInfo>
</StyledMemberNameContainer>
<StyledMemberRoleContainer>
{member.accepted
? member.type !== PermissionType.Owner
? t('Member')
: t('Owner')
: t('Pending')}
</StyledMemberRoleContainer>
{member.type === PermissionType.Owner ? (
<StyledMoreVerticalDiv />
) : (
<StyledMoreVerticalButton>
<Menu
content={
<>
<MenuItem
onClick={async () => {
// FIXME: remove ignore
// @ts-ignore
await removeMember(member.id);
toast(
t('Member has been removed', {
name: user.name,
})
);
}}
icon={<DeleteTemporarilyIcon />}
>
{t('Remove from workspace')}
</MenuItem>
</>
}
placement="bottom-end"
disablePortal={true}
trigger="click"
>
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
</StyledMoreVerticalButton>
)}
</StyledMemberListItem>
);
})}
</>
)}
</StyledMemberListContainer>
<StyledMemberButtonContainer>
<Button
onClick={() => {
setIsInviteModalShow(true);
}}
type="primary"
shape="circle"
>
{t('Invite Members')}
</Button>
</StyledMemberButtonContainer>
</StyledMemberContainer>
<InviteMemberModal
onClose={useCallback(() => {
setIsInviteModalShow(false);
}, [])}
onInviteSuccess={useCallback(() => {
setIsInviteModalShow(false);
}, [])}
workspaceId={workspace.id}
open={isInviteModalShow}
/>
</>
);
};
const LocalCollaborationPanel: React.FC<
Omit<PanelProps, 'workspace'> & {
workspace: LocalWorkspace;
}
> = ({ workspace, onTransferWorkspace }) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
return (
<>
<Wrapper marginBottom="32px">{t('Collaboration Description')}</Wrapper>
<Button
type="light"
shape="circle"
onClick={() => {
setOpen(true);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
<TransformWorkspaceToAffineModal
open={open}
onClose={() => {
setOpen(false);
}}
onConform={() => {
// todo(himself65): move this function out of affine component
lockMutex(async () => {
const id = await transformWorkspace(
RemWorkspaceFlavour.LOCAL,
RemWorkspaceFlavour.AFFINE,
workspace
);
onTransferWorkspace(id);
setOpen(false);
});
}}
/>
</>
);
};
export const CollaborationPanel: React.FC<PanelProps> = props => {
switch (props.workspace.flavour) {
case RemWorkspaceFlavour.AFFINE: {
const workspace = props.workspace as AffineRemoteWorkspace;
return (
<AffineRemoteCollaborationPanel {...props} workspace={workspace} />
);
}
case RemWorkspaceFlavour.LOCAL: {
const workspace = props.workspace as LocalWorkspace;
return <LocalCollaborationPanel {...props} workspace={workspace} />;
}
}
throw new Unreachable();
};
@@ -0,0 +1,211 @@
import { styled } from '@affine/component';
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
import { Button } from '@affine/component';
import { Input } from '@affine/component';
import { MuiAvatar } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { EmailIcon } from '@blocksuite/icons';
import React, { Suspense, useCallback, useState } from 'react';
import { useMembers } from '../../../../../../hooks/affine/use-members';
import { useUsersByEmail } from '../../../../../../hooks/affine/use-users-by-email';
interface LoginModalProps {
open: boolean;
onClose: () => void;
workspaceId: string;
onInviteSuccess: () => void;
}
const gmailReg = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@gmail\.com$/;
const Result: React.FC<{
workspaceId: string;
queryEmail: string;
}> = ({ workspaceId, queryEmail }) => {
const users = useUsersByEmail(workspaceId, queryEmail);
const firstUser = users?.at(0) ?? null;
if (!firstUser || !firstUser.email) {
return null;
}
return (
<Members>
<Member>
{firstUser.avatar_url ? (
<MuiAvatar src={firstUser.avatar_url}></MuiAvatar>
) : (
<MemberIcon>
<EmailIcon></EmailIcon>
</MemberIcon>
)}
<Email>{firstUser.email}</Email>
{/* <div>invited</div> */}
</Member>
</Members>
);
};
export const InviteMemberModal = ({
open,
onClose,
onInviteSuccess,
workspaceId,
}: LoginModalProps) => {
const { inviteMember } = useMembers(workspaceId);
const [email, setEmail] = useState<string>('');
const [showMemberPreview, setShowMemberPreview] = useState(false);
const { t } = useTranslation();
const inputChange = useCallback((value: string) => {
setEmail(value);
}, []);
return (
<div>
<Modal open={open} onClose={onClose}>
<ModalWrapper width={460} height={236}>
<Header>
<ModalCloseButton
onClick={() => {
onClose();
setEmail('');
}}
/>
</Header>
<Content>
<ContentTitle>{t('Invite Members')}</ContentTitle>
<InviteBox>
<Input
width={360}
value={email}
onChange={inputChange}
onFocus={useCallback(() => {
setShowMemberPreview(true);
}, [])}
onBlur={useCallback(() => {
setShowMemberPreview(false);
}, [])}
placeholder={t('Invite placeholder')}
></Input>
{showMemberPreview && gmailReg.test(email) && (
<Suspense fallback="loading...">
<Result workspaceId={workspaceId} queryEmail={email} />
</Suspense>
)}
</InviteBox>
</Content>
<Footer>
<Button
disabled={!gmailReg.test(email)}
shape="circle"
type="primary"
style={{ width: '364px', height: '38px', borderRadius: '40px' }}
onClick={async () => {
await inviteMember(email);
setEmail('');
onInviteSuccess();
}}
>
{t('Invite')}
</Button>
</Footer>
</ModalWrapper>
</Modal>
</div>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
});
const Footer = styled('div')({
height: '102px',
margin: '32px 0',
textAlign: 'center',
});
const InviteBox = styled('div')({
position: 'relative',
});
const Members = styled('div')(({ theme }) => {
return {
position: 'absolute',
width: '100%',
background: theme.colors.pageBackground,
textAlign: 'left',
zIndex: 1,
borderRadius: '0px 10px 10px 10px',
height: '56px',
padding: '8px 12px',
input: {
'&::placeholder': {
color: theme.colors.placeHolderColor,
},
},
};
});
// const NoFind = styled('div')(({ theme }) => {
// return {
// color: theme.colors.iconColor,
// fontSize: theme.font.sm,
// lineHeight: '40px',
// userSelect: 'none',
// width: '100%',
// };
// });
const Member = styled('div')(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
lineHeight: '40px',
userSelect: 'none',
display: 'flex',
};
});
const MemberIcon = styled('div')(({ theme }) => {
return {
width: '40px',
height: '40px',
borderRadius: '50%',
color: theme.colors.primaryColor,
background: '#F5F5F5',
textAlign: 'center',
lineHeight: '45px',
// icon size
fontSize: '20px',
overflow: 'hidden',
img: {
width: '100%',
height: '100%',
},
};
});
const Email = styled('div')(({ theme }) => {
return {
flex: '1',
color: theme.colors.popoverColor,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
marginLeft: '8px',
};
});
@@ -0,0 +1,101 @@
import { styled } from '@affine/component';
import { MuiAvatar } from '@affine/component';
export const StyledMemberTitleContainer = styled('li')(() => {
return {
display: 'flex',
fontWeight: '500',
marginBottom: '42px',
flex: 1,
};
});
export const StyledMemberContainer = styled('div')(() => {
return {
display: 'flex',
height: '100%',
flexDirection: 'column',
overflow: 'hidden',
};
});
export const StyledMemberAvatar = styled(MuiAvatar)(() => {
return { height: '40px', width: '40px' };
});
export const StyledMemberNameContainer = styled('div')(() => {
return {
display: 'flex',
alignItems: 'center',
flex: '2 0 402px',
};
});
export const StyledMemberRoleContainer = styled('div')(() => {
return {
display: 'flex',
alignItems: 'center',
flex: '1 0 222px',
};
});
export const StyledMemberListContainer = styled('ul')(() => {
return {
overflowY: 'scroll',
flexGrow: 1,
paddingBottom: '58px',
};
});
export const StyledMemberListItem = styled('li')(() => {
return {
display: 'flex',
alignItems: 'center',
height: '72px',
width: '100%',
};
});
export const StyledMemberInfo = styled('div')(() => {
return {
paddingLeft: '12px',
};
});
export const StyledMemberName = styled('div')(({ theme }) => {
return {
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
color: theme.colors.textColor,
};
});
export const StyledMemberEmail = styled('div')(({ theme }) => {
return {
fontWeight: '400',
fontSize: '16px',
lineHeight: '22px',
color: theme.colors.iconColor,
};
});
export const StyledMemberButtonContainer = styled('div')(() => {
return {
position: 'fixed',
bottom: '20px',
};
});
export const StyledMoreVerticalDiv = styled('div')(() => {
return {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '24px',
height: '24px',
cursor: 'pointer',
paddingRight: '48px',
};
});
export const StyledMoreVerticalButton = styled(StyledMoreVerticalDiv)();
@@ -0,0 +1,14 @@
import { Button, Wrapper } from '@affine/component';
import { useTranslation } from '@affine/i18n';
export const ExportPanel = () => {
const { t } = useTranslation();
return (
<>
<Wrapper marginBottom="42px"> {t('Export Description')}</Wrapper>
<Button type="light" shape="circle" disabled>
{t('Export AFFiNE backup file')}
</Button>
</>
);
};
@@ -0,0 +1,93 @@
import { Button, Input, Modal, ModalCloseButton } from '@affine/component';
import { Trans, useTranslation } from '@affine/i18n';
import { useCallback, useState } from 'react';
import {
AffineOfficialWorkspace,
RemWorkspaceFlavour,
} from '../../../../../../shared';
import {
StyledButtonContent,
StyledInputContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
StyledWorkspaceName,
} from './style';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
workspace: AffineOfficialWorkspace;
onDeleteWorkspace: () => void;
}
export const WorkspaceDeleteModal = ({
open,
onClose,
workspace,
onDeleteWorkspace,
}: WorkspaceDeleteProps) => {
const [deleteStr, setDeleteStr] = useState<string>('');
const allowDelete = deleteStr.toLowerCase() === 'delete';
const { t } = useTranslation();
const handleDelete = useCallback(() => {
onDeleteWorkspace();
}, [onDeleteWorkspace]);
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t('Delete Workspace')}?</StyledModalHeader>
{workspace.flavour === RemWorkspaceFlavour.LOCAL ? (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspace.blockSuiteWorkspace.meta.name } as any}
</StyledWorkspaceName>
) cannot be undone, please proceed with caution. All contents will
be lost.
</Trans>
</StyledTextContent>
) : (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description2">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspace.blockSuiteWorkspace.meta.name } as any}
</StyledWorkspaceName>
) will delete both local and cloud data, this operation cannot be
undone, please proceed with caution.
</Trans>
</StyledTextContent>
)}
<StyledInputContent>
<Input
onChange={setDeleteStr}
placeholder={t('Delete Workspace placeholder')}
value={deleteStr}
width={284}
height={42}
></Input>
</StyledInputContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t('Cancel')}
</Button>
<Button
disabled={!allowDelete}
onClick={handleDelete}
type="danger"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Delete')}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};
@@ -0,0 +1,77 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(({ theme }) => {
return {
position: 'relative',
padding: '0px',
width: '560px',
background: theme.colors.popoverBackground,
borderRadius: '12px',
// height: '312px',
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
margin: '44px 0px 12px 0px',
width: '560px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
color: theme.colors.popoverColor,
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'left',
};
});
export const StyledInputContent = styled('div')(({ theme }) => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '24px 0',
fontSize: theme.font.base,
};
});
export const StyledButtonContent = styled('div')(() => {
return {
marginBottom: '42px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
};
});
export const StyledWorkspaceName = styled('span')(() => {
return {
color: '#E8178A',
};
});
// export const StyledCancelButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });
// export const StyledDeleteButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });
@@ -0,0 +1,18 @@
export const CameraIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.6236 4.25001C10.635 4.25001 10.6467 4.25002 10.6584 4.25002H13.3416C13.3533 4.25002 13.365 4.25001 13.3764 4.25001C13.5609 4.24995 13.7105 4.2499 13.8543 4.26611C14.5981 4.34997 15.2693 4.75627 15.6826 5.38026C15.7624 5.50084 15.83 5.63398 15.9121 5.79586C15.9173 5.80613 15.9226 5.81652 15.9279 5.82703C15.9538 5.87792 15.9679 5.90562 15.9789 5.9261C15.9832 5.9341 15.9857 5.93861 15.9869 5.94065C16.0076 5.97069 16.0435 5.99406 16.0878 5.99905L16.0849 5.99877C16.0849 5.99877 16.0907 5.99918 16.1047 5.99947C16.1286 5.99998 16.1604 6.00002 16.2181 6.00002L17.185 6.00001C17.6577 6 18.0566 5.99999 18.3833 6.02627C18.7252 6.05377 19.0531 6.11364 19.3656 6.27035C19.8402 6.50842 20.2283 6.88944 20.4723 7.36077C20.6336 7.67233 20.6951 7.99944 20.7232 8.33858C20.75 8.66166 20.75 9.05554 20.75 9.51992V16.2301C20.75 16.6945 20.75 17.0884 20.7232 17.4114C20.6951 17.7506 20.6336 18.0777 20.4723 18.3893C20.2283 18.8606 19.8402 19.2416 19.3656 19.4797C19.0531 19.6364 18.7252 19.6963 18.3833 19.7238C18.0566 19.75 17.6578 19.75 17.185 19.75H6.81497C6.34225 19.75 5.9434 19.75 5.61668 19.7238C5.27477 19.6963 4.94688 19.6364 4.63444 19.4797C4.15978 19.2416 3.77167 18.8606 3.52771 18.3893C3.36644 18.0777 3.30494 17.7506 3.27679 17.4114C3.24998 17.0884 3.24999 16.6945 3.25 16.2302V9.51987C3.24999 9.05551 3.24998 8.66164 3.27679 8.33858C3.30494 7.99944 3.36644 7.67233 3.52771 7.36077C3.77167 6.88944 4.15978 6.50842 4.63444 6.27035C4.94688 6.11364 5.27477 6.05377 5.61668 6.02627C5.9434 5.99999 6.34225 6 6.81498 6.00001L7.78191 6.00002C7.83959 6.00002 7.87142 5.99998 7.8953 5.99947C7.90607 5.99924 7.91176 5.99897 7.91398 5.99884C7.95747 5.99343 7.99267 5.9703 8.01312 5.94066C8.01429 5.93863 8.01684 5.93412 8.02113 5.9261C8.0321 5.90561 8.04622 5.87791 8.07206 5.82703C8.07739 5.81653 8.08266 5.80615 8.08787 5.79588C8.17004 5.63397 8.23759 5.50086 8.31745 5.38026C8.73067 4.75627 9.40192 4.34997 10.1457 4.26611C10.2895 4.2499 10.4391 4.24995 10.6236 4.25001ZM10.6584 5.75002C10.422 5.75002 10.3627 5.75114 10.3138 5.75666C10.0055 5.79142 9.73316 5.95919 9.56809 6.20845C9.54218 6.24758 9.51544 6.29761 9.40943 6.50633C9.40611 6.51287 9.40274 6.5195 9.39934 6.52622C9.36115 6.60161 9.31758 6.68761 9.26505 6.76694C8.9964 7.17261 8.56105 7.4354 8.08026 7.48961C7.98625 7.50021 7.89021 7.50011 7.80434 7.50003C7.79678 7.50002 7.7893 7.50002 7.78191 7.50002H6.84445C6.33444 7.50002 5.99634 7.50058 5.73693 7.52144C5.48594 7.54163 5.37478 7.57713 5.30693 7.61115C5.11257 7.70864 4.95675 7.86306 4.85983 8.05029C4.82733 8.11308 4.79194 8.21816 4.77165 8.46266C4.7506 8.71626 4.75 9.0474 4.75 9.55001V16.2C4.75 16.7026 4.7506 17.0338 4.77165 17.2874C4.79194 17.5319 4.82733 17.6369 4.85983 17.6997C4.95675 17.887 5.11257 18.0414 5.30693 18.1389C5.37478 18.1729 5.48594 18.2084 5.73693 18.2286C5.99634 18.2494 6.33444 18.25 6.84445 18.25H17.1556C17.6656 18.25 18.0037 18.2494 18.2631 18.2286C18.5141 18.2084 18.6252 18.1729 18.6931 18.1389C18.8874 18.0414 19.0433 17.887 19.1402 17.6997C19.1727 17.6369 19.2081 17.5319 19.2283 17.2874C19.2494 17.0338 19.25 16.7026 19.25 16.2V9.55001C19.25 9.0474 19.2494 8.71626 19.2283 8.46266C19.2081 8.21816 19.1727 8.11308 19.1402 8.05029C19.0433 7.86306 18.8874 7.70864 18.6931 7.61115C18.6252 7.57713 18.5141 7.54163 18.2631 7.52144C18.0037 7.50058 17.6656 7.50002 17.1556 7.50002H16.2181C16.2107 7.50002 16.2032 7.50002 16.1957 7.50003C16.1098 7.50011 16.0138 7.50021 15.9197 7.48961C15.4389 7.4354 15.0036 7.17261 14.735 6.76694C14.6824 6.68761 14.6389 6.60163 14.6007 6.52622C14.5973 6.5195 14.5939 6.51287 14.5906 6.50633C14.4846 6.29763 14.4578 6.24758 14.4319 6.20846C14.2668 5.95919 13.9945 5.79142 13.6862 5.75666C13.6373 5.75114 13.578 5.75002 13.3416 5.75002H10.6584ZM12 11C10.9303 11 10.0833 11.8506 10.0833 12.875C10.0833 13.8995 10.9303 14.75 12 14.75C13.0697 14.75 13.9167 13.8995 13.9167 12.875C13.9167 11.8506 13.0697 11 12 11ZM8.58333 12.875C8.58333 11 10.1242 9.50002 12 9.50002C13.8758 9.50002 15.4167 11 15.4167 12.875C15.4167 14.7501 13.8758 16.25 12 16.25C10.1242 16.25 8.58333 14.7501 8.58333 12.875Z"
fill="white"
/>
</svg>
);
};
@@ -0,0 +1,236 @@
import { Button, FlexWrapper, MuiFade } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { assertExists } from '@blocksuite/store';
import React, { useState } from 'react';
import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner';
import { refreshDataCenter } from '../../../../../hooks/use-workspaces';
import { RemWorkspaceFlavour } from '../../../../../shared';
import { Upload } from '../../../../pure/file-upload';
import {
CloudWorkspaceIcon,
JoinedWorkspaceIcon,
LocalWorkspaceIcon,
} from '../../../../pure/icons';
import { WorkspaceAvatar } from '../../../../pure/workspace-avatar';
import { PanelProps } from '../../index';
import { StyledRow, StyledSettingKey } from '../../style';
import { WorkspaceDeleteModal } from './delete';
import { CameraIcon } from './icons';
import { WorkspaceLeave } from './leave';
import {
StyledAvatar,
StyledEditButton,
StyledInput,
StyledWorkspaceInfo,
} from './style';
export const GeneralPanel: React.FC<PanelProps> = ({
workspace,
onDeleteWorkspace,
}) => {
const [showDelete, setShowDelete] = useState<boolean>(false);
const [showLeave, setShowLeave] = useState<boolean>(false);
const [workspaceName, setWorkspaceName] = useState<string>(
workspace.blockSuiteWorkspace.meta.name
);
const isOwner = useIsWorkspaceOwner(workspace);
const [showEditInput, setShowEditInput] = useState(false);
const { t } = useTranslation();
const handleUpdateWorkspaceName = () => {
workspace.blockSuiteWorkspace.meta.setName(workspaceName);
// fixme(himself65): don't refresh
refreshDataCenter();
};
const fileChange = async (file: File) => {
const blob = new Blob([file], { type: file.type });
const blobs = await workspace.blockSuiteWorkspace.blobs;
assertExists(blobs);
const blobId = await blobs.set(blob);
workspace.blockSuiteWorkspace.meta.setAvatar(blobId);
// fixme(himself65): don't refresh
refreshDataCenter();
};
return (
<>
<StyledRow>
<StyledSettingKey>{t('Workspace Avatar')}</StyledSettingKey>
<StyledAvatar disabled={!isOwner}>
{isOwner ? (
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange}
>
<>
<div className="camera-icon">
<CameraIcon></CameraIcon>
</div>
<WorkspaceAvatar size={72} workspace={workspace} />
</>
</Upload>
) : (
<WorkspaceAvatar size={72} workspace={workspace} />
)}
</StyledAvatar>
</StyledRow>
<StyledRow>
<StyledSettingKey>{t('Workspace Name')}</StyledSettingKey>
<div style={{ position: 'relative' }}>
<MuiFade in={!showEditInput}>
<FlexWrapper>
{workspace.blockSuiteWorkspace.meta.name}
{isOwner && (
<StyledEditButton
onClick={() => {
setShowEditInput(true);
}}
>
{t('Edit')}
</StyledEditButton>
)}
</FlexWrapper>
</MuiFade>
{isOwner && (
<MuiFade in={showEditInput}>
<FlexWrapper style={{ position: 'absolute', top: 0, left: 0 }}>
<StyledInput
width={284}
height={38}
value={workspaceName}
placeholder={t('Workspace Name')}
maxLength={15}
minLength={0}
onChange={newName => {
setWorkspaceName(newName);
}}
></StyledInput>
<Button
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
disabled={
workspaceName === workspace.blockSuiteWorkspace.meta.name
}
onClick={() => {
handleUpdateWorkspaceName();
setShowEditInput(false);
}}
>
{t('Confirm')}
</Button>
<Button
type="default"
shape="circle"
style={{ marginLeft: '24px' }}
onClick={() => {
setWorkspaceName(workspace.blockSuiteWorkspace.meta.name);
setShowEditInput(false);
}}
>
{t('Cancel')}
</Button>
</FlexWrapper>
</MuiFade>
)}
</div>
</StyledRow>
{/* fixme(himself65): how to know a workspace owner by api? */}
{/*{!isOwner && (*/}
{/* <StyledRow>*/}
{/* <StyledSettingKey>{t('Workspace Owner')}</StyledSettingKey>*/}
{/* <FlexWrapper alignItems="center">*/}
{/* <MuiAvatar*/}
{/* sx={{ width: 72, height: 72, marginRight: '12px' }}*/}
{/* alt="owner avatar"*/}
{/* // src={currentWorkspace?.owner?.avatar}*/}
{/* >*/}
{/* <EmailIcon />*/}
{/* </MuiAvatar>*/}
{/* /!*<span>{currentWorkspace?.owner?.name}</span>*!/*/}
{/* </FlexWrapper>*/}
{/* </StyledRow>*/}
{/*)}*/}
{/*{!isOwner && (*/}
{/* <StyledRow>*/}
{/* <StyledSettingKey>{t('Members')}</StyledSettingKey>*/}
{/* <FlexWrapper alignItems="center">*/}
{/* /!*<span>{currentWorkspace?.memberCount}</span>*!/*/}
{/* </FlexWrapper>*/}
{/* </StyledRow>*/}
{/*)}*/}
<StyledRow>
<StyledSettingKey>{t('Workspace Type')}</StyledSettingKey>
{isOwner ? (
workspace.flavour === RemWorkspaceFlavour.LOCAL ? (
<StyledWorkspaceInfo>
<LocalWorkspaceIcon />
<span>{t('Local Workspace')}</span>
</StyledWorkspaceInfo>
) : (
<StyledWorkspaceInfo>
<CloudWorkspaceIcon />
<span>{t('Cloud Workspace')}</span>
</StyledWorkspaceInfo>
)
) : (
<StyledWorkspaceInfo>
<JoinedWorkspaceIcon />
<span>{t('Joined Workspace')}</span>
</StyledWorkspaceInfo>
)}
</StyledRow>
<StyledRow>
<StyledSettingKey> {t('Delete Workspace')}</StyledSettingKey>
{isOwner ? (
<>
<Button
type="danger"
shape="circle"
style={{ borderRadius: '40px' }}
onClick={() => {
setShowDelete(true);
}}
>
{t('Delete Workspace')}
</Button>
<WorkspaceDeleteModal
onDeleteWorkspace={onDeleteWorkspace}
open={showDelete}
onClose={() => {
setShowDelete(false);
}}
workspace={workspace}
/>
</>
) : (
<>
<Button
type="danger"
shape="circle"
onClick={() => {
setShowLeave(true);
}}
>
{t('Leave Workspace')}
</Button>
<WorkspaceLeave
open={showLeave}
onClose={() => {
setShowLeave(false);
}}
/>
</>
)}
</StyledRow>
</>
);
};
@@ -0,0 +1,50 @@
import { Modal } from '@affine/component';
import { ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import {
StyledButtonContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
} from './style';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
}
export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
// const { leaveWorkSpace } = useWorkspaceHelper();
const { t } = useTranslation();
const handleLeave = async () => {
// await leaveWorkSpace();
onClose();
};
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t('Leave Workspace')}</StyledModalHeader>
<StyledTextContent>
{t('Leave Workspace Description')}
</StyledTextContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t('Cancel')}
</Button>
<Button
onClick={handleLeave}
type="danger"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Leave')}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};
@@ -0,0 +1,46 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(({ theme }) => {
return {
position: 'relative',
padding: '0px',
width: '460px',
background: theme.colors.popoverBackground,
borderRadius: '12px',
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
margin: '44px 0px 12px 0px',
width: '460px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
color: theme.colors.popoverColor,
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'center',
};
});
export const StyledButtonContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '0px 0 32px 0',
};
});
@@ -0,0 +1,56 @@
import { displayFlex, styled } from '@affine/component';
import { Input } from '@affine/component';
export const StyledInput = styled(Input)(({ theme }) => {
return {
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: '10px',
fontSize: theme.font.sm,
};
});
export const StyledWorkspaceInfo = styled.div(({ theme }) => {
return {
...displayFlex('flex-start', 'center'),
fontSize: '20px',
span: {
fontSize: theme.font.base,
marginLeft: '15px',
},
};
});
export const StyledAvatar = styled('div')(
({ disabled }: { disabled: boolean }) => {
return {
position: 'relative',
marginRight: '20px',
cursor: disabled ? 'default' : 'pointer',
':hover': {
'.camera-icon': {
display: 'flex',
},
},
'.camera-icon': {
position: 'absolute',
top: 0,
left: 0,
display: 'none',
width: '100%',
height: '100%',
borderRadius: '50%',
backgroundColor: 'rgba(60, 61, 63, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
};
}
);
export const StyledEditButton = styled('div')(({ theme }) => {
return {
color: theme.colors.primaryColor,
cursor: 'pointer',
marginLeft: '36px',
};
});
@@ -0,0 +1,147 @@
import {
Button,
Content,
FlexWrapper,
Input,
toast,
Wrapper,
} from '@affine/component';
import { useTranslation } from '@affine/i18n';
import React, { useCallback, useEffect, useState } from 'react';
import { lockMutex } from '../../../../../atoms';
import { useToggleWorkspacePublish } from '../../../../../hooks/affine/use-toggle-workspace-publish';
import {
AffineOfficialWorkspace,
AffineRemoteWorkspace,
LocalWorkspace,
RemWorkspaceFlavour,
} from '../../../../../shared';
import { Unreachable } from '../../../affine-error-eoundary';
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
export type PublishPanelProps = {
workspace: AffineOfficialWorkspace;
};
export type PublishPanelAffineProps = {
workspace: AffineRemoteWorkspace;
};
const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
workspace,
}) => {
const [origin, setOrigin] = useState('');
useEffect(() => {
setOrigin(
typeof window !== 'undefined' && window.location.origin
? window.location.origin
: ''
);
}, []);
const shareUrl = origin + '/public-workspace/' + workspace.id;
const { t } = useTranslation();
const publishWorkspace = useToggleWorkspacePublish(workspace);
const copyUrl = useCallback(() => {
navigator.clipboard.writeText(shareUrl);
toast(t('Copied link to clipboard'));
}, [shareUrl, t]);
const [open, setOpen] = useState(false);
if (workspace.public) {
return (
<>
<Wrapper marginBottom="42px">{t('Published Description')}</Wrapper>
<Wrapper marginBottom="12px">
<Content weight="500">{t('Share with link')}</Content>
</Wrapper>
<FlexWrapper>
<Input width={582} value={shareUrl} disabled={true}></Input>
<Button
onClick={copyUrl}
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Copy Link')}
</Button>
</FlexWrapper>
<Button
onClick={async () => {
lockMutex(async () => {
return publishWorkspace(false);
});
}}
loading={false}
type="danger"
shape="circle"
style={{ marginTop: '38px' }}
>
{t('Stop publishing')}
</Button>
</>
);
}
return (
<>
<Wrapper marginBottom="42px">{t('Publishing Description')}</Wrapper>
<Button
onClick={() => {
setOpen(true);
}}
type="light"
shape="circle"
>
{t('Publish to web')}
</Button>
<EnableAffineCloudModal
workspace={workspace}
open={open}
onClose={() => {
setOpen(false);
}}
onConfirm={() => {
lockMutex(async () => {
return publishWorkspace(true);
}).then(() => {
setOpen(false);
});
}}
/>
</>
);
};
export type PublishPanelLocalProps = {
workspace: LocalWorkspace;
};
const PublishPanelLocal: React.FC<PublishPanelLocalProps> = ({ workspace }) => {
const { t } = useTranslation();
return (
<>
<Wrapper marginBottom="42px">{t('Publishing')}</Wrapper>
<Button
type="light"
shape="circle"
onClick={async () => {
// fixme: regression
toast('You need to enable AFFiNE Cloud to use this feature.');
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
</>
);
};
export const PublishPanel: React.FC<PublishPanelProps> = ({ workspace }) => {
if (workspace.flavour === RemWorkspaceFlavour.AFFINE) {
return <PublishPanelAffine workspace={workspace} />;
} else if (workspace.flavour === RemWorkspaceFlavour.LOCAL) {
return <PublishPanelLocal workspace={workspace} />;
}
throw new Unreachable();
};
@@ -0,0 +1,113 @@
import { styled } from '@affine/component';
import { FlexWrapper } from '@affine/component';
export const StyledSettingContainer = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'column',
padding: '48px 0 0 48px',
height: 'calc(100vh - 60px)',
};
});
export const StyledSettingSidebar = styled('div')(() => {
{
return {
marginTop: '48px',
};
}
});
export const StyledSettingContent = styled('div')(() => {
return {
overflow: 'auto',
flex: 1,
paddingTop: '48px',
};
});
export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
({ theme, isActive }) => {
{
return {
display: 'flex',
margin: '0 48px 0 0',
height: '34px',
color: isActive ? theme.colors.primaryColor : theme.colors.textColor,
fontWeight: '500',
fontSize: theme.font.h6,
lineHeight: theme.font.lineHeight,
cursor: 'pointer',
transition: 'all 0.15s ease',
};
}
}
);
export const StyledSettingKey = styled.div(({ theme }) => {
return {
width: '140px',
fontSize: theme.font.base,
fontWeight: 500,
marginRight: '56px',
flexShrink: 0,
};
});
export const StyledRow = styled(FlexWrapper)(() => {
return {
marginBottom: '42px',
};
});
export const StyledWorkspaceName = styled('span')(({ theme }) => {
return {
fontWeight: '400',
fontSize: theme.font.h6,
};
});
export const StyledIndicator = styled.div(({ theme }) => {
return {
height: '2px',
background: theme.colors.primaryColor,
position: 'absolute',
left: '0',
bottom: '0',
transition: 'left .3s, width .3s',
};
});
export const StyledTabButtonWrapper = styled.div(() => {
return {
display: 'flex',
position: 'relative',
};
});
// export const StyledDownloadCard = styled.div<{ active?: boolean }>(
// ({ theme, active }) => {
// return {
// width: '240px',
// height: '86px',
// border: '1px solid',
// borderColor: active
// ? theme.colors.primaryColor
// : theme.colors.borderColor,
// borderRadius: '10px',
// padding: '8px 12px',
// position: 'relative',
// ':not(:last-of-type)': {
// marginRight: '24px',
// },
// svg: {
// display: active ? 'block' : 'none',
// ...positionAbsolute({ top: '-12px', right: '-12px' }),
// },
// };
// }
// );
// export const StyledDownloadCardDes = styled.div(({ theme }) => {
// return {
// fontSize: theme.font.sm,
// color: theme.colors.iconColor,
// };
// });