refactor: remove legacy cloud (#2987)

This commit is contained in:
Alex Yang
2023-07-03 22:29:37 +08:00
parent 1887a36ca5
commit dedb4ba833
87 changed files with 148 additions and 6383 deletions
@@ -4,7 +4,6 @@ import type {
WorkspaceNotFoundError,
} from '@affine/env/constant';
import { PageNotFoundError } from '@affine/env/constant';
import { RequestError } from '@affine/workspace/affine/api';
import type { NextRouter } from 'next/router';
import type { ErrorInfo, ReactNode } from 'react';
import type React from 'react';
@@ -19,7 +18,6 @@ type AffineError =
| Unreachable
| WorkspaceNotFoundError
| PageNotFoundError
| RequestError
| Error;
interface AffineErrorBoundaryState {
@@ -78,13 +76,6 @@ export class AffineErrorBoundary extends Component<
</>
</>
);
} else if (error instanceof RequestError) {
return (
<>
<h1>Sorry.. there was an error</h1>
{error.message}
</>
);
}
return (
<>
@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons';
import type React from 'react';
import { useCurrentUser } from '../../../hooks/current/use-current-user';
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
interface EnableAffineCloudModalProps {
@@ -18,7 +17,6 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
onClose,
}) => {
const t = useAFFiNEI18N();
const user = useCurrentUser();
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
@@ -39,7 +37,7 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
type="primary"
onClick={onConfirm}
>
{user ? t.Enable() : t['Sign in and Enable']()}
{t['Sign in and Enable']()}
</StyleButton>
<StyleButton shape="round" onClick={onClose}>
{t['Not now']()}
@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { type FC, useState } from 'react';
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
import type { AffineOfficialWorkspace } from '../../../../shared';
import type { WorkspaceSettingDetailProps } from '../index';
import { WorkspaceDeleteModal } from './delete';
@@ -14,7 +13,8 @@ export const DeleteLeaveWorkspace: FC<{
onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace'];
}> = ({ workspace, onDeleteWorkspace }) => {
const t = useAFFiNEI18N();
const isOwner = useIsWorkspaceOwner(workspace);
// fixme: cloud regression
const isOwner = true;
const [showDelete, setShowDelete] = useState(false);
const [showLeave, setShowLeave] = useState(false);
@@ -14,7 +14,6 @@ import type { FC } from 'react';
import type { AffineOfficialWorkspace } from '../../../shared';
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
import { ExportPanel } from './export';
import { MembersPanel } from './members';
import { ProfilePanel } from './profile';
import { PublishPanel } from './publish';
import { StoragePanel } from './storage';
@@ -63,11 +62,6 @@ export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({
onDeleteWorkspace={onDeleteWorkspace}
{...props}
/>
<MembersPanel
workspace={workspace}
onDeleteWorkspace={onDeleteWorkspace}
{...props}
/>
</SettingWrapper>
{environment.isDesktop ? (
<SettingWrapper title={t['Storage and Export']()}>
@@ -1,160 +0,0 @@
import { Button, IconButton, Menu, MenuItem } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { UserAvatar } from '@affine/component/user-avatar';
import { Unreachable } from '@affine/env/constant';
import type { AffineLegacyCloudWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { PermissionType } from '@affine/env/workspace/legacy-cloud';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DeleteTemporarilyIcon, MoreVerticalIcon } from '@blocksuite/icons';
import type { FC } from 'react';
import React, { useCallback, useState } from 'react';
import { useMembers } from '../../../../hooks/affine/use-members';
import type { AffineOfficialWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import type { WorkspaceSettingDetailProps } from '../index';
import { fakeWrapper } from '../style.css';
import { InviteMemberModal } from './invite-member-modal';
import * as style from './style.css';
export type AffineRemoteMembersProps = WorkspaceSettingDetailProps & {
workspace: AffineLegacyCloudWorkspace;
};
export type MemberPanelProps = WorkspaceSettingDetailProps & {
workspace: AffineOfficialWorkspace;
};
const MemberList: FC<{
workspace: AffineLegacyCloudWorkspace;
}> = ({ workspace }) => {
const t = useAFFiNEI18N();
const { members, removeMember } = useMembers(workspace.id);
if (members.length) {
return null;
}
return (
<ul className={style.memberList}>
{members
.sort((b, a) => a.type - b.type)
.map(member => {
const { id, name, email, avatar_url } = {
name: '',
email: '',
avatar_url: '',
...member,
};
return (
<li className="member-list-item" key={id}>
<div className="left-col">
<UserAvatar size={36} name={name} url={avatar_url} />
<div className="user-info-wrapper">
<p className="user-name">{name}</p>
<p className="email">{email}</p>
</div>
</div>
<div className="right-col">
<div className="user-identity">
{member.accepted
? member.type !== PermissionType.Owner
? t['Member']()
: t['Owner']()
: t['Pending']()}
</div>
<Menu
content={
<>
<MenuItem
onClick={async () => {
await removeMember(Number(id));
toast(
t['Member has been removed']({
name,
})
);
}}
icon={<DeleteTemporarilyIcon />}
>
{t['Remove from workspace']()}
</MenuItem>
</>
}
placement="bottom"
disablePortal={true}
trigger="click"
>
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
</div>
</li>
);
})}
</ul>
);
};
export const AffineRemoteMembers: FC<AffineRemoteMembersProps> = ({
workspace,
}) => {
const t = useAFFiNEI18N();
const { members } = useMembers(workspace.id);
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
return (
<>
<SettingRow
name={`${t['Members']()} (${members.length})`}
desc={t['Members hint']()}
style={{ marginTop: '25px' }}
>
<Button
size="middle"
onClick={() => {
setIsInviteModalShow(true);
}}
>
{t['Invite']()}
</Button>
</SettingRow>
<MemberList workspace={workspace} />
<InviteMemberModal
onClose={useCallback(() => {
setIsInviteModalShow(false);
}, [])}
onInviteSuccess={useCallback(() => {
setIsInviteModalShow(false);
}, [])}
workspaceId={workspace.id}
open={isInviteModalShow}
/>
</>
);
};
export const FakeMembers: FC = () => {
const t = useAFFiNEI18N();
return (
<div className={fakeWrapper} style={{ marginTop: '25px' }}>
<SettingRow name={`${t['Members']()} (0)`} desc={t['Members hint']()}>
<Button size="middle">{t['Invite']()}</Button>
</SettingRow>
</div>
);
};
export const MembersPanel: FC<MemberPanelProps> = props => {
switch (props.workspace.flavour) {
case WorkspaceFlavour.AFFINE: {
const workspace = props.workspace as AffineLegacyCloudWorkspace;
return <AffineRemoteMembers {...props} workspace={workspace} />;
}
case WorkspaceFlavour.LOCAL: {
return <FakeMembers />;
}
}
throw new Unreachable();
};
@@ -1,222 +0,0 @@
import {
Button,
Input,
Modal,
ModalCloseButton,
ModalWrapper,
MuiAvatar,
styled,
} from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EmailIcon } from '@blocksuite/icons';
import type React from 'react';
import { 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|example)\.(com|org)$/;
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 = useAFFiNEI18N();
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
data-testid="invite-member-input"
width={360}
value={email}
onChange={inputChange}
onFocus={useCallback(() => {
setShowMemberPreview(true);
}, [])}
onBlur={useCallback(() => {
setShowMemberPreview(false);
}, [])}
placeholder={t['Invite placeholder']()}
/>
{showMemberPreview && gmailReg.test(email) && (
<Suspense fallback="loading...">
<Result workspaceId={workspaceId} queryEmail={email} />
</Suspense>
)}
</InviteBox>
</Content>
<Footer>
<Button
data-testid="invite-member-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')(() => {
return {
position: 'absolute',
width: '100%',
background: 'var(--affine-background-primary-color)',
textAlign: 'left',
zIndex: 1,
borderRadius: '0px 10px 10px 10px',
height: '56px',
padding: '8px 12px',
input: {
'&::placeholder': {
color: 'var(--affine-placeholder-color)',
},
},
};
});
// const NoFind = styled('div')(({ theme }) => {
// return {
// color: 'var(--affine-icon-color)',
// fontSize: 'var(--affine-font-sm)',
// lineHeight: '40px',
// userSelect: 'none',
// width: '100%',
// };
// });
const Member = styled('div')(() => {
return {
color: 'var(--affine-icon-color)',
fontSize: 'var(--affine-font-sm)',
lineHeight: '40px',
userSelect: 'none',
display: 'flex',
};
});
const MemberIcon = styled('div')(() => {
return {
width: '40px',
height: '40px',
borderRadius: '50%',
color: 'var(--affine-primary-color)',
background: '#F5F5F5',
textAlign: 'center',
lineHeight: '45px',
// icon size
fontSize: '20px',
overflow: 'hidden',
img: {
width: '100%',
height: '100%',
},
};
});
const Email = styled('div')(() => {
return {
flex: '1',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
marginLeft: '8px',
};
});
@@ -1,54 +0,0 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const memberList = style({
marginTop: '12px',
});
globalStyle(`${memberList} .member-list-item`, {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
globalStyle(`${memberList} .member-list-item:not(:last-of-type)`, {
marginBottom: '8px',
});
globalStyle(`${memberList} .left-col`, {
display: 'flex',
alignItems: 'center',
width: '60%',
});
globalStyle(`${memberList} .right-col`, {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
width: '35%',
});
globalStyle(`${memberList} .user-info-wrapper`, {
flexGrow: 1,
marginLeft: '12px',
overflow: 'hidden',
});
globalStyle(`${memberList} .user-info-wrapper p`, {
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
globalStyle(`${memberList} .user-name`, {
fontSize: 'var(--affine-font-sm)',
});
globalStyle(`${memberList} .email`, {
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-secondary-color)',
});
globalStyle(`${memberList} .user-identity`, {
fontSize: 'var(--affine-font-sm)',
marginRight: '15px',
flexGrow: '1',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
@@ -6,17 +6,33 @@ import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-s
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { type FC, useCallback, useState } from 'react';
import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner';
import type { AffineOfficialWorkspace } from '../../../shared';
import { Upload } from '../../pure/file-upload';
import { CameraIcon } from '../workspace-setting-detail/panel/general/icons';
import * as style from './style.css';
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>
);
};
export const ProfilePanel: FC<{
workspace: AffineOfficialWorkspace;
}> = ({ workspace }) => {
const t = useAFFiNEI18N();
const isOwner = useIsWorkspaceOwner(workspace);
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
workspace.blockSuiteWorkspace
@@ -38,22 +54,18 @@ export const ProfilePanel: FC<{
return (
<div className={style.profileWrapper}>
<div className={style.avatarWrapper}>
{isOwner ? (
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={update}
data-testid="upload-avatar"
>
<>
<div className="camera-icon-wrapper">
<CameraIcon />
</div>
<WorkspaceAvatar size={56} workspace={workspace} />
</>
</Upload>
) : (
<WorkspaceAvatar size={56} workspace={workspace} />
)}
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={update}
data-testid="upload-avatar"
>
<>
<div className="camera-icon-wrapper">
<CameraIcon />
</div>
<WorkspaceAvatar size={56} workspace={workspace} />
</>
</Upload>
</div>
<div className={style.profileHandlerWrapper}>
<Input
@@ -2,7 +2,7 @@ import { Button, FlexWrapper, Switch } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { Unreachable } from '@affine/env/constant';
import type {
AffineLegacyCloudWorkspace,
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
@@ -11,7 +11,6 @@ import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-
import type { FC } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useToggleWorkspacePublish } from '../../../hooks/affine/use-toggle-workspace-publish';
import type { AffineOfficialWorkspace } from '../../../shared';
import { toast } from '../../../utils';
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
@@ -26,13 +25,13 @@ export type PublishPanelLocalProps = WorkspaceSettingDetailProps & {
workspace: LocalWorkspace;
};
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
workspace: AffineLegacyCloudWorkspace;
workspace: AffineCloudWorkspace;
};
const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
const { workspace } = props;
const t = useAFFiNEI18N();
const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
// const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
const [origin, setOrigin] = useState('');
const shareUrl = origin + '/public-workspace/' + workspace.id;
@@ -54,13 +53,14 @@ const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
<SettingRow
name={t['Publish']()}
desc={
workspace.public ? t['Unpublished hint']() : t['Published hint']()
// workspace.public ? t['Unpublished hint']() : t['Published hint']()
'UNFINISHED'
}
>
<Switch
{/* <Switch
checked={workspace.public}
onChange={checked => toggleWorkspacePublish(checked)}
/>
/> */}
</SettingRow>
<FlexWrapper justifyContent="space-between">
<Button
@@ -150,7 +150,7 @@ const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
onConfirm={() => {
onTransferWorkspace(
WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE,
WorkspaceFlavour.AFFINE_CLOUD,
workspace
);
setOpen(false);
@@ -169,7 +169,7 @@ const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
};
export const PublishPanel: FC<PublishPanelProps> = props => {
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
return <PublishPanelAffine {...props} workspace={props.workspace} />;
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return <PublishPanelLocal {...props} workspace={props.workspace} />;
@@ -3,7 +3,7 @@ import {
type SettingModalProps,
} from '@affine/component/setting-components';
import type {
AffineLegacyCloudWorkspace,
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
@@ -77,7 +77,7 @@ export const SettingModal: React.FC<SettingModalProps> = ({
generalSettingList={generalSettingList}
onGeneralSettingClick={onGeneralSettingClick}
currentWorkspace={
currentWorkspace as AffineLegacyCloudWorkspace | LocalWorkspace
currentWorkspace as AffineCloudWorkspace | LocalWorkspace
}
workspaceList={workspaceList}
onWorkspaceSettingClick={onWorkspaceSettingClick}
@@ -1,7 +1,7 @@
import { UserAvatar } from '@affine/component/user-avatar';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import type {
AffineLegacyCloudWorkspace,
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -37,7 +37,7 @@ export const SettingSidebar = ({
currentWorkspace: Workspace;
workspaceList: Workspace[];
onWorkspaceSettingClick: (
workspace: AffineLegacyCloudWorkspace | LocalWorkspace
workspace: AffineCloudWorkspace | LocalWorkspace
) => void;
selectedWorkspace: Workspace | null;
@@ -118,7 +118,7 @@ const WorkspaceListItem = ({
isCurrent,
isActive,
}: {
workspace: AffineLegacyCloudWorkspace | LocalWorkspace;
workspace: AffineCloudWorkspace | LocalWorkspace;
onClick: () => void;
isCurrent: boolean;
isActive: boolean;
@@ -1,6 +1,6 @@
import type {
AffineLegacyCloudWorkspace,
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
export type Workspace = AffineLegacyCloudWorkspace | LocalWorkspace;
export type Workspace = AffineCloudWorkspace | LocalWorkspace;
@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons';
import type React from 'react';
import { useCurrentUser } from '../../../hooks/current/use-current-user';
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
export type TransformWorkspaceToAffineModalProps = {
@@ -16,7 +15,6 @@ export const TransformWorkspaceToAffineModal: React.FC<
TransformWorkspaceToAffineModalProps
> = ({ open, onClose, onConform }) => {
const t = useAFFiNEI18N();
const user = useCurrentUser();
return (
<Modal
@@ -41,7 +39,7 @@ export const TransformWorkspaceToAffineModal: React.FC<
type="primary"
onClick={onConform}
>
{user ? t['Enable']() : t['Sign in and Enable']()}
{t['Sign in and Enable']()}
</StyleButton>
<StyleButton
shape="round"
@@ -1,208 +0,0 @@
// import { styled } from '@affine/component';
// import { FlexWrapper } from '@affine/component';
import { globalStyle, style, styleVariants } from '@vanilla-extract/css';
export const container = style({
display: 'flex',
flexDirection: 'column',
marginTop: '52px',
padding: '0 52px 52px 52px',
height: 'calc(100vh - 52px)',
overflow: 'auto',
});
export const sidebar = style({
marginTop: '52px',
});
export const content = style({
flex: 1,
marginTop: '40px',
});
const baseAvatar = style({
position: 'relative',
marginRight: '20px',
cursor: 'pointer',
});
globalStyle(`${baseAvatar} .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',
zIndex: 10,
});
globalStyle(`${baseAvatar}:hover .camera-icon`, {
display: 'flex',
});
export const avatar = styleVariants({
disabled: [
baseAvatar,
{
cursor: 'default',
},
],
enabled: [
baseAvatar,
{
cursor: 'pointer',
},
],
});
const baseTagItem = style({
display: 'flex',
margin: '0 48px 0 0',
height: '34px',
fontWeight: '500',
fontSize: 'var(--affine-font-h6)',
lineHeight: 'var(--affine-line-height)',
cursor: 'pointer',
transition: 'all 0.15s ease',
});
export const tagItem = styleVariants({
active: [
baseTagItem,
{
color: 'var(--affine-primary-color)',
},
],
inactive: [
baseTagItem,
{
color: 'var(--affine-text-secondary-color)',
},
],
});
export const settingKey = style({
width: '140px',
fontSize: 'var(--affine-font-base)',
fontWeight: 500,
marginRight: '56px',
flexShrink: 0,
});
export const settingItemLabel = style({
fontSize: 'var(--affine-font-base)',
fontWeight: 600,
flexShrink: 0,
});
export const settingItemLabelHint = style({
fontSize: 'var(--affine-font-sm)',
color: 'var(--affine-text-secondary-color)',
fontWeight: 400,
flexShrink: 0,
marginTop: '4px',
});
export const settingsCannotDelete = style([
settingItemLabelHint,
{
color: 'var(--affine-warning-color)',
},
]);
export const row = style({
padding: '40px 0',
display: 'flex',
columnGap: '60px',
rowGap: '12px',
selectors: {
'&': {
borderBottom: '1px solid var(--affine-border-color)',
},
'&:first-child': {
paddingTop: 0,
},
},
flexWrap: 'wrap',
});
export const col = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
selectors: {
[`${row} &:nth-child(1)`]: {
flex: '3 0 200px',
},
[`${row} &:nth-child(2)`]: {
flex: '5 0 240px',
},
[`${row} &:nth-child(3)`]: {
flex: '2 0 200px',
alignItems: 'flex-end',
},
},
});
export const workspaceName = style({
fontWeight: '400',
fontSize: 'var(--affine-font-h6)',
});
export const indicator = style({
height: '2px',
background: 'var(--affine-primary-color)',
position: 'absolute',
left: '0',
bottom: '0',
transition: 'left .3s, width .3s',
});
export const tabButtonWrapper = style({
display: 'flex',
position: 'sticky',
top: '0',
background: 'var(--affine-background-primary-color)',
zIndex: 1,
});
export const storageTypeWrapper = style({
width: '100%',
display: 'flex',
alignItems: 'flex-start',
padding: '12px',
borderRadius: '10px',
gap: '12px',
boxShadow: 'var(--affine-shadow-1)',
cursor: 'pointer',
selectors: {
'&:hover': {
boxShadow: 'var(--affine-shadow-2)',
},
'&:not(:last-child)': {
marginBottom: '12px',
},
'&[data-disabled="true"]': {
cursor: 'default',
pointerEvents: 'none',
},
},
});
export const storageTypeLabelWrapper = style({
flex: 1,
});
export const storageTypeLabel = style({
fontSize: 'var(--affine-font-base)',
});
export const storageTypeLabelHint = style({
fontSize: 'var(--affine-font-sm)',
color: 'var(--affine-text-secondary-color)',
});
@@ -1,169 +0,0 @@
import type { SettingPanel, WorkspaceRegistry } from '@affine/env/workspace';
import { settingPanel, WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { MouseEvent } from 'react';
import type React from 'react';
import { Suspense, useCallback, useEffect, useMemo, useRef } from 'react';
import { preload } from 'swr';
import { fetcher, QueryKey } from '../../../adapters/affine/fetcher';
import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner';
import type { AffineOfficialWorkspace } from '../../../shared';
import * as style from './index.css';
import { CollaborationPanel } from './panel/collaboration';
import { ExportPanel } from './panel/export';
import { GeneralPanel } from './panel/general';
import { PublishPanel } from './panel/publish';
import { SyncPanel } from './panel/sync';
export type WorkspaceSettingDetailProps = {
workspace: AffineOfficialWorkspace;
currentTab: SettingPanel;
onChangeTab: (tab: SettingPanel) => void;
onDeleteWorkspace: () => Promise<void>;
onTransferWorkspace: <
From extends WorkspaceFlavour,
To extends WorkspaceFlavour
>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
) => void;
};
export type PanelProps = WorkspaceSettingDetailProps;
type Name = 'General' | 'Sync' | 'Collaboration' | 'Publish' | 'Export';
const panelMap = {
[settingPanel.General]: {
name: 'General',
ui: GeneralPanel,
},
[settingPanel.Sync]: {
name: 'Sync',
enable: (flavour: WorkspaceFlavour) => flavour === WorkspaceFlavour.AFFINE,
ui: SyncPanel,
},
[settingPanel.Collaboration]: {
name: 'Collaboration',
ui: CollaborationPanel,
},
[settingPanel.Publish]: {
name: 'Publish',
ui: PublishPanel,
},
[settingPanel.Export]: {
name: 'Export',
ui: ExportPanel,
},
} satisfies {
[Key in SettingPanel]: {
name: Name;
enable?: (flavour: WorkspaceFlavour) => boolean;
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 = useAFFiNEI18N();
const workspaceId = workspace.id;
useEffect(() => {
if (isAffine && isOwner) {
preload([QueryKey.getMembers, workspaceId], fetcher).catch(console.error);
}
}, [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 (
<div
className={style.container}
aria-label="workspace-setting-detail"
ref={containerRef}
>
<div className={style.tabButtonWrapper}>
{Object.entries(panelMap).map(([key, value]) => {
if ('enable' in value && !value.enable(workspace.flavour)) {
return null;
}
return (
<div
className={
style.tagItem[currentTab === key ? 'active' : 'inactive']
}
key={key}
data-tab-key={key}
onClick={handleTabClick}
>
{t[value.name]()}
</div>
);
})}
<div
className={style.indicator}
ref={ref => {
indicatorRef.current = ref;
startTransaction();
}}
/>
</div>
<div className={style.content}>
{/* todo: add skeleton */}
<Suspense fallback="loading panel...">
<Component {...props} key={currentTab} data-tab-ui={currentTab} />
</Suspense>
</div>
</div>
);
};
@@ -1,229 +0,0 @@
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
import { Unreachable } from '@affine/env/constant';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { PermissionType } from '@affine/env/workspace/legacy-cloud';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeleteTemporarilyIcon,
EmailIcon,
MoreVerticalIcon,
} from '@blocksuite/icons';
import type React from 'react';
import { useCallback, useState } from 'react';
import { useMembers } from '../../../../../hooks/affine/use-members';
import { toast } from '../../../../../utils';
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal';
import type { 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: AffineLegacyCloudWorkspace;
}
> = ({ workspace }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const t = useAFFiNEI18N();
const { members, removeMember } = useMembers(workspace.id);
return (
<>
<StyledMemberContainer>
<ul>
<StyledMemberTitleContainer>
<StyledMemberNameContainer>
{t['Users']()} (
<span data-testid="member-length">{members.length}</span>)
</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
await removeMember(Number(member.id));
toast(
t['Member has been removed']({
name: user.name,
})
);
}}
icon={<DeleteTemporarilyIcon />}
>
{t['Remove from workspace']()}
</MenuItem>
</>
}
placement="bottom"
disablePortal={true}
trigger="click"
>
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
</StyledMoreVerticalButton>
)}
</StyledMemberListItem>
);
})}
</>
)}
</StyledMemberListContainer>
<StyledMemberButtonContainer>
<Button
onClick={() => {
setIsInviteModalShow(true);
}}
type="primary"
data-testid="invite-members"
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 = useAFFiNEI18N();
const [open, setOpen] = useState(false);
return (
<>
<Wrapper marginBottom="42px">{t['Collaboration Description']()}</Wrapper>
<Button
data-testid="local-workspace-enable-cloud-button"
type="light"
shape="circle"
onClick={() => {
setOpen(true);
}}
>
{t['Enable AFFiNE Cloud']()}
</Button>
{runtimeConfig.enableLegacyCloud ? (
<TransformWorkspaceToAffineModal
open={open}
onClose={() => {
setOpen(false);
}}
onConform={() => {
onTransferWorkspace(
WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE,
workspace
);
setOpen(false);
}}
/>
) : (
<TmpDisableAffineCloudModal
open={open}
onClose={() => {
setOpen(false);
}}
/>
)}
</>
);
};
export const CollaborationPanel: React.FC<PanelProps> = props => {
switch (props.workspace.flavour) {
case WorkspaceFlavour.AFFINE: {
const workspace = props.workspace as AffineLegacyCloudWorkspace;
return (
<AffineRemoteCollaborationPanel {...props} workspace={workspace} />
);
}
case WorkspaceFlavour.LOCAL: {
const workspace = props.workspace as LocalWorkspace;
return <LocalCollaborationPanel {...props} workspace={workspace} />;
}
}
throw new Unreachable();
};
@@ -1,218 +0,0 @@
import {
Button,
Input,
Modal,
ModalCloseButton,
ModalWrapper,
MuiAvatar,
styled,
} from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EmailIcon } from '@blocksuite/icons';
import type React from 'react';
import { 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|example)\.(com|org)$/;
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 = useAFFiNEI18N();
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
data-testid="invite-member-input"
width={360}
value={email}
onChange={inputChange}
onFocus={useCallback(() => {
setShowMemberPreview(true);
}, [])}
onBlur={useCallback(() => {
setShowMemberPreview(false);
}, [])}
placeholder={t['Invite placeholder']()}
/>
{showMemberPreview && gmailReg.test(email) && (
<Suspense fallback="loading...">
<Result workspaceId={workspaceId} queryEmail={email} />
</Suspense>
)}
</InviteBox>
</Content>
<Footer>
<Button
data-testid="invite-member-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')(() => {
return {
position: 'absolute',
width: '100%',
background: 'var(--affine-background-primary-color)',
textAlign: 'left',
zIndex: 1,
borderRadius: '0px 10px 10px 10px',
height: '56px',
padding: '8px 12px',
input: {
'&::placeholder': {
color: 'var(--affine-placeholder-color)',
},
},
};
});
// const NoFind = styled('div')(({ theme }) => {
// return {
// color: 'var(--affine-icon-color)',
// fontSize: 'var(--affine-font-sm)',
// lineHeight: '40px',
// userSelect: 'none',
// width: '100%',
// };
// });
const Member = styled('div')(() => {
return {
color: 'var(--affine-icon-color)',
fontSize: 'var(--affine-font-sm)',
lineHeight: '40px',
userSelect: 'none',
display: 'flex',
};
});
const MemberIcon = styled('div')(() => {
return {
width: '40px',
height: '40px',
borderRadius: '50%',
color: 'var(--affine-primary-color)',
background: '#F5F5F5',
textAlign: 'center',
lineHeight: '45px',
// icon size
fontSize: '20px',
overflow: 'hidden',
img: {
width: '100%',
height: '100%',
},
};
});
const Email = styled('div')(() => {
return {
flex: '1',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
marginLeft: '8px',
};
});
@@ -1,101 +0,0 @@
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')(() => {
return {
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
color: 'var(--affine-text-primary-color)',
};
});
export const StyledMemberEmail = styled('div')(() => {
return {
fontWeight: '400',
fontSize: '16px',
lineHeight: '22px',
color: 'var(--affine-icon-color)',
};
});
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)``;
@@ -1,35 +0,0 @@
import { Button, toast, Wrapper } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import { useAtomValue } from 'jotai';
export const ExportPanel = () => {
const id = useAtomValue(rootCurrentWorkspaceIdAtom);
const t = useAFFiNEI18N();
return (
<>
<Wrapper marginBottom="42px"> {t['Export Description']()}</Wrapper>
<Button
type="light"
shape="circle"
disabled={
!environment.isDesktop || !id || !runtimeConfig.enableSQLiteProvider
}
data-testid="export-affine-backup"
onClick={async () => {
if (id) {
const result = await window.apis?.dialog.saveDBFileAs(id);
if (result?.error) {
// @ts-expect-error: result.error is dynamic
toast(t[result.error]());
} else if (!result?.canceled) {
toast(t['Export success']());
}
}
}}
>
{t['Export AFFiNE backup file']()}
</Button>
</>
);
};
@@ -1,112 +0,0 @@
import { Button, Input, Modal, ModalCloseButton } from '@affine/component';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useCallback, useState } from 'react';
import type { AffineOfficialWorkspace } from '../../../../../../shared';
import { toast } from '../../../../../../utils';
import {
StyledButtonContent,
StyledInputContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
StyledWorkspaceName,
} from './style';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
workspace: AffineOfficialWorkspace;
onDeleteWorkspace: () => Promise<void>;
}
export const WorkspaceDeleteModal = ({
open,
onClose,
workspace,
onDeleteWorkspace,
}: WorkspaceDeleteProps) => {
const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace ?? null
);
const [deleteStr, setDeleteStr] = useState<string>('');
const allowDelete = deleteStr === workspaceName;
const t = useAFFiNEI18N();
const handleDelete = useCallback(() => {
onDeleteWorkspace()
.then(() => {
toast(t['Successfully deleted'](), {
portal: document.body,
});
})
.catch(() => {
// ignore error
});
}, [onDeleteWorkspace, t]);
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t['Delete Workspace']()}?</StyledModalHeader>
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspaceName } 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: workspaceName } as any}
</StyledWorkspaceName>
) will delete both local and cloud data, this operation cannot be
undone, please proceed with caution.
</Trans>
</StyledTextContent>
)}
<StyledInputContent>
<Input
ref={ref => {
if (ref) {
setTimeout(() => ref.focus(), 0);
}
}}
onChange={setDeleteStr}
data-testid="delete-workspace-input"
placeholder={t['Placeholder of delete workspace']()}
value={deleteStr}
width={315}
height={42}
/>
</StyledInputContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t['Cancel']()}
</Button>
<Button
data-testid="delete-workspace-confirm-button"
disabled={!allowDelete}
onClick={handleDelete}
type="danger"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t['Delete']()}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};
@@ -1,76 +0,0 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(() => {
return {
position: 'relative',
padding: '0px',
width: '560px',
background: 'var(--affine-white)',
borderRadius: '12px',
// height: '312px',
};
});
export const StyledModalHeader = styled('div')(() => {
return {
margin: '44px 0px 12px 0px',
width: '560px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
};
});
// 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')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '24px 0',
fontSize: 'var(--affine-font-base)',
};
});
export const StyledButtonContent = styled('div')(() => {
return {
marginBottom: '42px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
};
});
export const StyledWorkspaceName = styled('span')(() => {
return {
fontWeight: '600',
};
});
// export const StyledCancelButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });
// export const StyledDeleteButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });
@@ -1,18 +0,0 @@
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>
);
};
@@ -1,283 +0,0 @@
import { Button, toast } from '@affine/component';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
ArrowRightSmallIcon,
DeleteIcon,
FolderIcon,
MoveToIcon,
SaveIcon,
} from '@blocksuite/icons';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx';
import type React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner';
import { Upload } from '../../../../pure/file-upload';
import type { PanelProps } from '../../index';
import * as style from '../../index.css';
import { WorkspaceDeleteModal } from './delete';
import { CameraIcon } from './icons';
import { WorkspaceLeave } from './leave';
import { StyledInput } from './style';
const useShowOpenDBFile = (workspaceId: string) => {
const [show, setShow] = useState(false);
useEffect(() => {
if (
window.apis &&
window.events &&
environment.isDesktop &&
runtimeConfig.enableSQLiteProvider
) {
window.apis.workspace
.getMeta(workspaceId)
.then(meta => {
setShow(!!meta.secondaryDBPath);
})
.catch(err => {
console.error(err);
});
return window.events.workspace.onMetaChange((newMeta: any) => {
if (newMeta.workspaceId === workspaceId) {
const meta = newMeta.meta;
setShow(!!meta.secondaryDBPath);
}
});
}
}, [workspaceId]);
return show;
};
export const GeneralPanel: React.FC<PanelProps> = ({
workspace,
onDeleteWorkspace,
}) => {
const [showDelete, setShowDelete] = useState<boolean>(false);
const [showLeave, setShowLeave] = useState<boolean>(false);
const [name, setName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace
);
const [input, setInput] = useState<string>(name);
const isOwner = useIsWorkspaceOwner(workspace);
const t = useAFFiNEI18N();
const handleUpdateWorkspaceName = (name: string) => {
setName(name);
toast(t['Update workspace name success']());
};
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
workspace.blockSuiteWorkspace
);
return (
<>
<div data-testid="avatar-row" className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>
{t['Workspace Avatar']()}
</div>
<div className={style.settingItemLabelHint}>
{t['Change avatar hint']()}
</div>
</div>
<div className={clsx(style.col)}>
<div className={style.avatar[isOwner ? 'enabled' : 'disabled']}>
{isOwner ? (
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={update}
data-testid="upload-avatar"
>
<>
<div className="camera-icon">
<CameraIcon></CameraIcon>
</div>
<WorkspaceAvatar size={72} workspace={workspace} />
</>
</Upload>
) : (
<WorkspaceAvatar size={72} workspace={workspace} />
)}
</div>
</div>
<div className={clsx(style.col)}></div>
</div>
<div data-testid="workspace-name-row" className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>{t['Workspace Name']()}</div>
<div className={style.settingItemLabelHint}>
{t['Change workspace name hint']()}
</div>
</div>
<div className={style.col}>
<StyledInput
height={38}
value={input}
data-testid="workspace-name-input"
placeholder={t['Workspace Name']()}
maxLength={64}
minLength={0}
onChange={setInput}
></StyledInput>
</div>
<div className={style.col}>
<Button
type="light"
size="middle"
data-testid="save-workspace-name"
icon={<SaveIcon />}
disabled={input === workspace.blockSuiteWorkspace.meta.name}
onClick={() => {
handleUpdateWorkspaceName(input);
}}
>
{t['Save']()}
</Button>
</div>
</div>
{environment.isDesktop && runtimeConfig.enableSQLiteProvider ? (
<DesktopClientOnly workspaceId={workspace.id} />
) : null}
<div className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>
{t['Delete Workspace']()}
</div>
<div className={style.settingItemLabelHint}>
{t['Delete Workspace Label Hint']()}
</div>
</div>
<div className={style.col}></div>
<div className={style.col}>
{isOwner ? (
<>
<Button
type="warning"
data-testid="delete-workspace-button"
size="middle"
icon={<DeleteIcon />}
onClick={() => {
setShowDelete(true);
}}
>
{t['Delete']()}
</Button>
<WorkspaceDeleteModal
onDeleteWorkspace={onDeleteWorkspace}
open={showDelete}
onClose={() => {
setShowDelete(false);
}}
workspace={workspace}
/>
</>
) : (
<>
<Button
type="warning"
size="middle"
onClick={() => {
setShowLeave(true);
}}
>
{t['Leave']()}
</Button>
<WorkspaceLeave
open={showLeave}
onClose={() => {
setShowLeave(false);
}}
/>
</>
)}
</div>
</div>
</>
);
};
function DesktopClientOnly({ workspaceId }: { workspaceId: string }) {
const t = useAFFiNEI18N();
const showOpenFolder = useShowOpenDBFile(workspaceId);
const onRevealDBFile = useCallback(() => {
if (environment.isDesktop && runtimeConfig.enableSQLiteProvider) {
window.apis?.dialog.revealDBFile(workspaceId).catch(err => {
console.error(err);
});
}
}, [workspaceId]);
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(false);
const handleMoveTo = useCallback(() => {
if (moveToInProgress) {
return;
}
setMoveToInProgress(true);
window.apis?.dialog
.moveDBFile(workspaceId)
.then(result => {
if (!result?.error && !result?.canceled) {
toast(t['Move folder success']());
} else if (result?.error) {
// @ts-expect-error: result.error is dynamic
toast(t[result.error]());
}
})
.catch(() => {
toast(t['UNKNOWN_ERROR']());
})
.finally(() => {
setMoveToInProgress(false);
});
}, [moveToInProgress, t, workspaceId]);
const openFolderNode = showOpenFolder ? (
<div className={style.storageTypeWrapper} onClick={onRevealDBFile}>
<FolderIcon color="var(--affine-primary-color)" />
<div className={style.storageTypeLabelWrapper}>
<div className={style.storageTypeLabel}>{t['Open folder']()}</div>
<div className={style.storageTypeLabelHint}>
{t['Open folder hint']()}
</div>
</div>
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
</div>
) : null;
return (
<div className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>{t['Storage Folder']()}</div>
<div className={style.settingItemLabelHint}>
{t['Storage Folder Hint']()}
</div>
</div>
<div className={style.col}>
{openFolderNode}
<div
data-testid="move-folder"
data-disabled={moveToInProgress}
className={style.storageTypeWrapper}
onClick={handleMoveTo}
>
<MoveToIcon color="var(--affine-primary-color)" />
<div className={style.storageTypeLabelWrapper}>
<div className={style.storageTypeLabel}>{t['Move folder']()}</div>
<div className={style.storageTypeLabelHint}>
{t['Move folder hint']()}
</div>
</div>
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
</div>
</div>
<div className={style.col}></div>
</div>
);
}
@@ -1,50 +0,0 @@
import { Modal } from '@affine/component';
import { ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
StyledButtonContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
} from './style';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
}
export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
// const { leaveWorkSpace } = useWorkspaceHelper();
const t = useAFFiNEI18N();
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>
);
};
@@ -1,45 +0,0 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(() => {
return {
position: 'relative',
padding: '0px',
width: '460px',
background: 'var(--affine-white)',
borderRadius: '12px',
};
});
export const StyledModalHeader = styled('div')(() => {
return {
margin: '44px 0px 12px 0px',
width: '460px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
};
});
// 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',
};
});
@@ -1,51 +0,0 @@
import { displayFlex, styled } from '@affine/component';
import { Input } from '@affine/component';
export const StyledInput = Input;
export const StyledWorkspaceInfo = styled('div')(() => {
return {
...displayFlex('flex-start', 'center'),
fontSize: '20px',
span: {
fontSize: 'var(--affine-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',
zIndex: 10,
},
};
}
);
export const StyledEditButton = styled('div')(() => {
return {
color: 'var(--affine-primary-color)',
cursor: 'pointer',
marginLeft: '36px',
};
});
@@ -1,171 +0,0 @@
import {
Button,
Content,
FlexWrapper,
Input,
Wrapper,
} from '@affine/component';
import { isBrowser, Unreachable } from '@affine/env/constant';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Box } from '@mui/material';
import type React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useToggleWorkspacePublish } from '../../../../../hooks/affine/use-toggle-workspace-publish';
import type { AffineOfficialWorkspace } from '../../../../../shared';
import { toast } from '../../../../../utils';
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
import type { WorkspaceSettingDetailProps } from '../../index';
export type PublishPanelProps = WorkspaceSettingDetailProps & {
workspace: AffineOfficialWorkspace;
};
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
workspace: AffineLegacyCloudWorkspace;
};
const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
workspace,
}) => {
const [origin, setOrigin] = useState('');
useEffect(() => {
setOrigin(
isBrowser && window.location.origin ? window.location.origin : ''
);
}, []);
const shareUrl = origin + '/public-workspace/' + workspace.id;
const t = useAFFiNEI18N();
const publishWorkspace = useToggleWorkspacePublish(workspace);
const copyUrl = useCallback(async () => {
await navigator.clipboard.writeText(shareUrl);
toast(t['Copied link to clipboard']());
}, [shareUrl, t]);
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
data-testid="share-url"
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 () => {
await publishWorkspace(false);
}}
loading={false}
type="danger"
shape="circle"
style={{ marginTop: '38px' }}
>
{t['Stop publishing']()}
</Button>
</>
);
}
return (
<>
<Wrapper marginBottom="42px">{t['Publishing Description']()}</Wrapper>
<Button
data-testid="publish-to-web-button"
onClick={async () => {
await publishWorkspace(true);
}}
type="light"
shape="circle"
>
{t['Publish to web']()}
</Button>
</>
);
};
export type PublishPanelLocalProps = WorkspaceSettingDetailProps & {
workspace: LocalWorkspace;
};
const PublishPanelLocal: React.FC<PublishPanelLocalProps> = ({
workspace,
onTransferWorkspace,
}) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
return (
<>
<Box
sx={{
marginBottom: '42px',
}}
>
{t['Publishing']()}
</Box>
{/* TmpDisableAffineCloudModal */}
<Button
data-testid="publish-enable-affine-cloud-button"
type="light"
shape="circle"
onClick={() => {
setOpen(true);
}}
>
{t['Enable AFFiNE Cloud']()}
</Button>
{runtimeConfig.enableLegacyCloud ? (
<EnableAffineCloudModal
open={open}
onClose={() => {
setOpen(false);
}}
onConfirm={() => {
onTransferWorkspace(
WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE,
workspace
);
setOpen(false);
}}
/>
) : (
<TmpDisableAffineCloudModal
open={open}
onClose={() => {
setOpen(false);
}}
/>
)}
</>
);
};
export const PublishPanel: React.FC<PublishPanelProps> = props => {
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
return <PublishPanelAffine {...props} workspace={props.workspace} />;
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return <PublishPanelLocal {...props} workspace={props.workspace} />;
}
throw new Unreachable();
};
@@ -1,51 +0,0 @@
import { Content, FlexWrapper, styled } from '@affine/component';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import type React from 'react';
import { useCurrentUser } from '../../../../../hooks/current/use-current-user';
import { WorkspaceAvatar } from '../../../../pure/footer';
import type { PanelProps } from '../../index';
export const StyledWorkspaceName = styled('span')(() => {
return {
fontWeight: '400',
fontSize: 'var(--affine-font-h6)',
};
});
export const SyncPanel: React.FC<PanelProps> = ({ workspace }) => {
if (workspace.flavour !== WorkspaceFlavour.AFFINE) {
throw new TypeError('SyncPanel can only be used with Affine workspace');
}
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(
workspace.blockSuiteWorkspace
);
const user = useCurrentUser();
const t = useAFFiNEI18N();
return (
<>
<FlexWrapper alignItems="center" style={{ marginBottom: '12px' }}>
<WorkspaceAvatar
size={32}
name={name}
avatar={avatar}
style={{ marginRight: '12px' }}
/>
<StyledWorkspaceName>{name}</StyledWorkspaceName>
&nbsp;
<Content weight={500}>{t['is a Cloud Workspace']()}</Content>
</FlexWrapper>
<Trans i18nKey="Cloud Workspace Description">
All data will be synchronised and saved to the AFFiNE account
{{
email: user?.email,
}}
</Trans>
</>
);
};