feat: migrate workspace setting with new design to setting modal (#2900)

Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
Qi
2023-06-28 22:45:33 +08:00
committed by Alex Yang
parent 5a3e5a1565
commit 4cca8a16ab
33 changed files with 1540 additions and 141 deletions

View File

@@ -0,0 +1,112 @@
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>
);
};

View File

@@ -0,0 +1,76 @@
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',
// };
// });

View File

@@ -0,0 +1,58 @@
import {
SettingRow,
} from '@affine/component/setting-components';
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';
import { WorkspaceLeave } from './leave';
export const DeleteLeaveWorkspace: FC<{
workspace: AffineOfficialWorkspace;
onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace'];
}> = ({ workspace, onDeleteWorkspace }) => {
const t = useAFFiNEI18N();
const isOwner = useIsWorkspaceOwner(workspace);
const [showDelete, setShowDelete] = useState(false);
const [showLeave, setShowLeave] = useState(false);
return (
<>
<SettingRow
name={
<span style={{ color: 'var(--affine-error-color)' }}>
{isOwner ? t['Delete Workspace']() : t['Leave Workspace']()}
</span>
}
desc={t['None yet']()}
style={{ cursor: 'pointer' }}
onClick={() => {
setShowDelete(true);
}}
>
<ArrowRightSmallIcon />
</SettingRow>
{isOwner ? (
<WorkspaceDeleteModal
onDeleteWorkspace={onDeleteWorkspace}
open={showDelete}
onClose={() => {
setShowDelete(false);
}}
workspace={workspace}
/>
) : (
<WorkspaceLeave
open={showLeave}
onClose={() => {
setShowLeave(false);
}}
/>
)}
</>
);
};

View File

@@ -0,0 +1,50 @@
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>
);
};

View File

@@ -0,0 +1,45 @@
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',
};
});

View File

@@ -0,0 +1,34 @@
import { Button, toast } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { FC } from 'react';
import type { AffineOfficialWorkspace } from '../../../shared';
export const ExportPanel: FC<{
workspace: AffineOfficialWorkspace;
}> = ({ workspace }) => {
const workspaceId = workspace.id;
const t = useAFFiNEI18N();
return (
<>
<SettingRow name={t['Export']()} desc={t['Export Description']()}>
<Button
size="small"
data-testid="export-affine-backup"
onClick={async () => {
const result = await window.apis?.dialog.saveDBFileAs(workspaceId);
if (result?.error) {
// @ts-expect-error: result.error is dynamic
toast(t[result.error]());
} else if (!result?.canceled) {
toast(t['Export success']());
}
}}
>
{t['Export']()}
</Button>
</SettingRow>
</>
);
};

View File

@@ -1,11 +1,23 @@
import {
SettingHeader,
SettingRow,
SettingWrapper,
} from '@affine/component/setting-components';
import type {
WorkspaceFlavour,
WorkspaceRegistry,
} from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
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';
export type WorkspaceSettingDetailProps = {
workspace: AffineOfficialWorkspace;
@@ -22,15 +34,54 @@ export type WorkspaceSettingDetailProps = {
export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({
workspace,
onDeleteWorkspace,
...props
}) => {
const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace ?? null
);
return (
<div>
<h2>New Workspace Setting Coming Soon!</h2>
const t = useAFFiNEI18N();
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
{workspaceName}
</div>
return (
<>
<SettingHeader
title={t[`Workspace Settings with name`]({ name })}
subtitle={t['You can customize your workspace here.']()}
/>
<SettingWrapper title={t['Info']()}>
<SettingRow
name={t['Workspace Profile']()}
desc={t[
'Only an owner can edit the the Workspace avatar and name.Changes will be shown for everyone.'
]()}
spreadCol={false}
>
<ProfilePanel workspace={workspace} />
</SettingRow>
</SettingWrapper>
<SettingWrapper title={t['AFFiNE Cloud']()}>
<PublishPanel
workspace={workspace}
onDeleteWorkspace={onDeleteWorkspace}
{...props}
/>
<MembersPanel
workspace={workspace}
onDeleteWorkspace={onDeleteWorkspace}
{...props}
/>
</SettingWrapper>
{environment.isDesktop ? (
<SettingWrapper title={t['Storage and Export']()}>
<StoragePanel workspace={workspace} />
<ExportPanel workspace={workspace} />
</SettingWrapper>
) : null}
<SettingWrapper>
<DeleteLeaveWorkspace
workspace={workspace}
onDeleteWorkspace={onDeleteWorkspace}
/>
</SettingWrapper>
</>
);
};

View File

@@ -0,0 +1,160 @@
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();
};

View File

@@ -0,0 +1,222 @@
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',
};
});

View File

@@ -0,0 +1,54 @@
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',
});

View File

@@ -0,0 +1,87 @@
import { IconButton, Input, toast } from '@affine/component';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DoneIcon } 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 { 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';
export const ProfilePanel: FC<{
workspace: AffineOfficialWorkspace;
}> = ({ workspace }) => {
const t = useAFFiNEI18N();
const isOwner = useIsWorkspaceOwner(workspace);
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
workspace.blockSuiteWorkspace
);
const [name, setName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace
);
const [input, setInput] = useState<string>(name);
const handleUpdateWorkspaceName = useCallback(
(name: string) => {
setName(name);
toast(t['Update workspace name success']());
},
[setName, t]
);
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} />
)}
</div>
<div className={style.profileHandlerWrapper}>
<Input
width={280}
height={32}
value={input}
data-testid="workspace-name-input"
placeholder={t['Workspace Name']()}
maxLength={64}
minLength={0}
onChange={setInput}
/>
{input === workspace.blockSuiteWorkspace.meta.name ? null : (
<IconButton
size="middle"
data-testid="save-workspace-name"
onClick={() => {
handleUpdateWorkspaceName(input);
}}
style={{
color: 'var(--affine-primary-color)',
marginLeft: '12px',
}}
>
<DoneIcon />
</IconButton>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,180 @@
import { Button, FlexWrapper, Switch } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { 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 { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
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';
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
import type { WorkspaceSettingDetailProps } from './index';
import * as style from './style.css';
export type PublishPanelProps = WorkspaceSettingDetailProps & {
workspace: AffineOfficialWorkspace;
};
export type PublishPanelLocalProps = WorkspaceSettingDetailProps & {
workspace: LocalWorkspace;
};
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
workspace: AffineLegacyCloudWorkspace;
};
const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
const { workspace } = props;
const t = useAFFiNEI18N();
const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
const [origin, setOrigin] = useState('');
const shareUrl = origin + '/public-workspace/' + workspace.id;
useEffect(() => {
setOrigin(
typeof window !== 'undefined' && window.location.origin
? window.location.origin
: ''
);
}, []);
const copyUrl = useCallback(async () => {
await navigator.clipboard.writeText(shareUrl);
toast(t['Copied link to clipboard']());
}, [shareUrl, t]);
return (
<>
<SettingRow
name={t['Publish']()}
desc={
workspace.public ? t['Unpublished hint']() : t['Published hint']()
}
>
<Switch
checked={workspace.public}
onChange={checked => toggleWorkspacePublish(checked)}
/>
</SettingRow>
<FlexWrapper justifyContent="space-between">
<Button
className={style.urlButton}
size="middle"
onClick={useCallback(() => {
window.open(shareUrl, '_blank');
}, [shareUrl])}
title={shareUrl}
>
{shareUrl}
</Button>
<Button size="middle" onClick={copyUrl}>
{t['Copy']()}
</Button>
</FlexWrapper>
</>
);
};
const FakePublishPanelAffine: FC<{
workspace: AffineOfficialWorkspace;
}> = ({ workspace }) => {
const t = useAFFiNEI18N();
const [origin, setOrigin] = useState('');
const shareUrl = origin + '/public-workspace/' + workspace.id;
useEffect(() => {
setOrigin(
typeof window !== 'undefined' && window.location.origin
? window.location.origin
: ''
);
}, []);
return (
<div className={style.fakeWrapper}>
<SettingRow name={t['Publish']()} desc={t['Unpublished hint']()}>
<Switch checked={false} />
</SettingRow>
<FlexWrapper justifyContent="space-between">
<Button className={style.urlButton} size="middle" title={shareUrl}>
{shareUrl}
</Button>
<Button size="middle">{t['Copy']()}</Button>
</FlexWrapper>
</div>
);
};
const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
workspace,
onTransferWorkspace,
}) => {
const t = useAFFiNEI18N();
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
const [open, setOpen] = useState(false);
return (
<>
<SettingRow
name={t['Workspace saved locally']({ name })}
desc={t['Enable cloud hint']()}
spreadCol={false}
style={{
padding: '10px',
background: 'var(--affine-background-secondary-color)',
}}
>
<Button
data-testid="publish-enable-affine-cloud-button"
type="primary"
onClick={() => {
setOpen(true);
}}
style={{ marginTop: '12px' }}
>
{runtimeConfig.enableLegacyCloud
? t['Enable AFFiNE Cloud']()
: 'Disable AFFiNE Cloud'}
</Button>
</SettingRow>
<FakePublishPanelAffine workspace={workspace} />
{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: 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();
};

View File

@@ -0,0 +1,87 @@
import { Button, toast } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { type FC, useCallback, useEffect, useState } from 'react';
import type { AffineOfficialWorkspace } from '../../../shared';
const useShowOpenDBFile = (workspaceId: string) => {
const [show, setShow] = useState(false);
useEffect(() => {
if (window.apis && window.events && environment.isDesktop) {
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 StoragePanel: FC<{
workspace: AffineOfficialWorkspace;
}> = ({ workspace }) => {
const workspaceId = workspace.id;
const t = useAFFiNEI18N();
const showOpenFolder = useShowOpenDBFile(workspaceId);
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(false);
const onRevealDBFile = useCallback(() => {
window.apis?.dialog.revealDBFile(workspaceId).catch(err => {
console.error(err);
});
}, [workspaceId]);
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]);
if (!showOpenFolder) {
return null;
}
return (
<SettingRow
name={t['Storage']()}
desc={t['Storage Folder Hint']()}
spreadCol={false}
>
<Button
data-testid="move-folder"
data-disabled={moveToInProgress}
onClick={handleMoveTo}
>
{t['Move folder']()}
</Button>
<Button onClick={onRevealDBFile}>{t['Open folder']()}</Button>
</SettingRow>
);
};

View File

@@ -0,0 +1,67 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const profileWrapper = style({
display: 'flex',
alignItems: 'flex-end',
marginTop: '12px',
});
export const profileHandlerWrapper = style({
flexGrow: '1',
display: 'flex',
alignItems: 'center',
marginLeft: '20px',
});
export const avatarWrapper = style({
width: '56px',
height: '56px',
borderRadius: '50%',
position: 'relative',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
selectors: {
'&.disable': {
cursor: 'default',
pointerEvents: 'none',
},
},
});
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
display: 'flex',
});
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
width: '100%',
height: '100%',
position: 'absolute',
display: 'none',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(60, 61, 63, 0.5)',
zIndex: '1',
});
export const urlButton = style({
width: 'calc(100% - 64px - 15px)',
});
globalStyle(`${urlButton} span`, {
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const fakeWrapper = style({
position: 'relative',
selectors: {
'&::after': {
content: '""',
width: '100%',
height: '100%',
position: 'absolute',
left: 0,
top: 0,
background: 'var(--affine-white-60)',
},
},
});

View File

@@ -1,14 +0,0 @@
import type { FC } from 'react';
import { settingHeader } from './share.css';
export const SettingHeader: FC<{ title: string; subtitle?: string }> = ({
title,
subtitle,
}) => {
return (
<div className={settingHeader}>
<div className="title">{title}</div>
<div className="subtitle">{subtitle}</div>
</div>
);
};

View File

@@ -1,22 +0,0 @@
import type { CSSProperties, FC, PropsWithChildren, ReactElement } from 'react';
import { settingRow } from './share.css';
export const SettingRow: FC<
PropsWithChildren<{
name: string;
desc: string | ReactElement;
style?: CSSProperties;
onClick?: () => void;
}>
> = ({ name, desc, children, onClick, style }) => {
return (
<div className={settingRow} style={style} onClick={onClick}>
<div className="left-col">
<div className="name">{name}</div>
<div className="desc">{desc}</div>
</div>
<div className="right-col">{children}</div>
</div>
);
};

View File

@@ -1,69 +0,0 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const settingHeader = style({
height: '68px',
borderBottom: '1px solid var(--affine-border-color)',
marginBottom: '24px',
});
globalStyle(`${settingHeader} .title`, {
fontSize: 'var(--affine-font-base)',
fontWeight: 600,
lineHeight: '24px',
marginBottom: '4px',
});
globalStyle(`${settingHeader} .subtitle`, {
fontSize: 'var(--affine-font-xs)',
lineHeight: '16px',
color: 'var(--affine-text-secondary-color)',
});
export const wrapper = style({
borderBottom: '1px solid var(--affine-border-color)',
paddingBottom: '24px',
marginBottom: '24px',
selectors: {
'&:last-of-type': {
borderBottom: 'none',
paddingBottom: '0',
marginBottom: '0',
},
},
});
globalStyle(`${wrapper} .title`, {
fontSize: 'var(--affine-font-sm)',
fontWeight: 600,
lineHeight: '18px',
color: 'var(--affine-text-secondary-color)',
marginBottom: '16px',
});
export const settingRow = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '25px',
color: 'var(--affine-text-primary-color)',
});
globalStyle(`${settingRow} .left-col`, {
flexShrink: 0,
maxWidth: '80%',
});
globalStyle(`${settingRow} .name`, {
marginBottom: '2px',
fontSize: 'var(--affine-font-sm)',
fontWeight: 600,
});
globalStyle(`${settingRow} .desc`, {
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-secondary-color)',
});
globalStyle(`${settingRow} .right-col`, {
flexGrow: 1,
display: 'flex',
justifyContent: 'flex-end',
paddingLeft: '15px',
});

View File

@@ -1,14 +0,0 @@
import type { FC, PropsWithChildren } from 'react';
import { wrapper } from './share.css';
export const Wrapper: FC<PropsWithChildren<{ title?: string }>> = ({
title,
children,
}) => {
return (
<div className={wrapper}>
{title ? <div className="title">{title}</div> : null}
{children}
</div>
);
};

View File

@@ -1,2 +1,2 @@
// Some settings are not implemented yet, but need to show in the setting modal when boss is watching.
export const IS_EXHIBITION = false;
export const IS_EXHIBITION = true;

View File

@@ -1,14 +1,13 @@
import { Switch } from '@affine/component';
import { relatedLinks } from '@affine/component/contact-modal';
import { isDesktop } from '@affine/env/constant';
import { SettingHeader } from '@affine/component/setting-components';
import { SettingRow } from '@affine/component/setting-components';
import { SettingWrapper } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons';
import { useCallback } from 'react';
import { type AppSetting, useAppSetting } from '../../../../../atoms/settings';
import { SettingHeader } from '../../common/setting-header';
import { SettingRow } from '../../common/setting-row';
import { Wrapper } from '../../common/wrapper';
import { IS_EXHIBITION } from '../../config';
import { communityItem, communityWrapper, link } from './style.css';
@@ -24,8 +23,8 @@ export const AboutAffine = () => {
return (
<>
<SettingHeader title={t['About AFFiNE']()} subtitle={t['None yet']()} />
{IS_EXHIBITION && isDesktop ? (
<Wrapper title={t['Version']()}>
{IS_EXHIBITION && environment.isDesktop ? (
<SettingWrapper title={t['Version']()}>
<SettingRow
name={t['Check for updates']()}
desc={t['New version is ready']()}
@@ -65,9 +64,9 @@ export const AboutAffine = () => {
>
<ArrowRightSmallIcon />
</SettingRow>
</Wrapper>
</SettingWrapper>
) : null}
<Wrapper title={t['Contact with us']()}>
<SettingWrapper title={t['Contact with us']()}>
<a className={link} href="https://affine.pro" target="_blank">
{t['Official Website']()}
<OpenInNewIcon className="icon" />
@@ -76,8 +75,8 @@ export const AboutAffine = () => {
{t['AFFiNE Community']()}
<OpenInNewIcon className="icon" />
</a>
</Wrapper>
<Wrapper title={t['Communities']()}>
</SettingWrapper>
<SettingWrapper title={t['Communities']()}>
<div className={communityWrapper}>
{relatedLinks.map(({ icon, title, link }) => {
return (
@@ -94,8 +93,8 @@ export const AboutAffine = () => {
);
})}
</div>
</Wrapper>
<Wrapper title={t['Info of legal']()}>
</SettingWrapper>
<SettingWrapper title={t['Info of legal']()}>
<a className={link} href="https://affine.pro/privacy" target="_blank">
{t['Privacy']()}
<OpenInNewIcon className="icon" />
@@ -104,7 +103,7 @@ export const AboutAffine = () => {
{t['Terms of Use']()}
<OpenInNewIcon className="icon" />
</a>
</Wrapper>
</SettingWrapper>
</>
);
};

View File

@@ -1,5 +1,7 @@
import { RadioButton, RadioButtonGroup, Switch } from '@affine/component';
import { isDesktop } from '@affine/env/constant';
import { SettingHeader } from '@affine/component/setting-components';
import { SettingRow } from '@affine/component/setting-components';
import { SettingWrapper } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useTheme } from 'next-themes';
import { useCallback } from 'react';
@@ -10,9 +12,6 @@ import {
windowFrameStyleOptions,
} from '../../../../../atoms/settings';
import { LanguageMenu } from '../../../language-menu';
import { SettingHeader } from '../../common/setting-header';
import { SettingRow } from '../../common/setting-row';
import { Wrapper } from '../../common/wrapper';
import { IS_EXHIBITION } from '../../config';
import { DateFormatSetting } from './date-format-setting';
import { settingWrapper } from './style.css';
@@ -56,7 +55,7 @@ export const AppearanceSettings = () => {
subtitle={t['Customize your AFFiNE Appearance']()}
/>
<Wrapper title={t['Theme']()}>
<SettingWrapper title={t['Theme']()}>
<SettingRow
name={t['Color Scheme']()}
desc={t['Choose your color scheme']()}
@@ -71,7 +70,7 @@ export const AppearanceSettings = () => {
<LanguageMenu />
</div>
</SettingRow>
{IS_EXHIBITION && isDesktop ? (
{IS_EXHIBITION && environment.isDesktop ? (
<SettingRow
name={t['Client Border Style']()}
desc={t['Customize the appearance of the client.']()}
@@ -92,7 +91,7 @@ export const AppearanceSettings = () => {
onChange={checked => changeSwitch('fullWidthLayout', checked)}
/>
</SettingRow>
{IS_EXHIBITION && isDesktop ? (
{IS_EXHIBITION && environment.isDesktop ? (
<SettingRow
name={t['Window frame style']()}
desc={t['Customize appearance of Windows Client.']()}
@@ -114,9 +113,9 @@ export const AppearanceSettings = () => {
</RadioButtonGroup>
</SettingRow>
) : null}
</Wrapper>
</SettingWrapper>
{IS_EXHIBITION ? (
<Wrapper title={t['Date']()}>
<SettingWrapper title={t['Date']()}>
<SettingRow
name={t['Date Format']()}
desc={t['Customize your date style.']()}
@@ -134,11 +133,11 @@ export const AppearanceSettings = () => {
onChange={checked => changeSwitch('startWeekOnMonday', checked)}
/>
</SettingRow>
</Wrapper>
</SettingWrapper>
) : null}
{isDesktop ? (
<Wrapper title={t['Sidebar']()}>
{environment.isDesktop ? (
<SettingWrapper title={t['Sidebar']()}>
<SettingRow
name={t['Disable the noise background on the sidebar']()}
desc={t['None yet']()}
@@ -161,7 +160,7 @@ export const AppearanceSettings = () => {
}
/>
</SettingRow>
</Wrapper>
</SettingWrapper>
) : null}
</>
);

View File

@@ -3,7 +3,7 @@ import { style } from '@vanilla-extract/css';
export const settingWrapper = style({
flexGrow: 1,
display: 'flex',
width: '50%',
justifyContent: 'flex-end',
minWidth: '150px',
maxWidth: '250px',
});

View File

@@ -1,3 +1,5 @@
import { SettingHeader } from '@affine/component/setting-components';
import { SettingWrapper } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
@@ -6,8 +8,6 @@ import {
useMarkdownShortcuts,
usePageShortcuts,
} from '../../../../../hooks/affine/use-shortcuts';
import { SettingHeader } from '../../common/setting-header';
import { Wrapper } from '../../common/wrapper';
import { shortcutRow } from './style.css';
export const Shortcuts = () => {
@@ -24,7 +24,7 @@ export const Shortcuts = () => {
title={t['Keyboard Shortcuts']()}
subtitle={t['Check Keyboard Shortcuts quickly']()}
/>
<Wrapper title={t['General']()}>
<SettingWrapper title={t['General']()}>
{Object.entries(generalShortcuts).map(([title, shortcuts]) => {
return (
<div key={title} className={shortcutRow}>
@@ -33,8 +33,8 @@ export const Shortcuts = () => {
</div>
);
})}
</Wrapper>
<Wrapper title={t['Page']()}>
</SettingWrapper>
<SettingWrapper title={t['Page']()}>
{Object.entries(pageShortcuts).map(([title, shortcuts]) => {
return (
<div key={title} className={shortcutRow}>
@@ -43,8 +43,8 @@ export const Shortcuts = () => {
</div>
);
})}
</Wrapper>
<Wrapper title={t['Edgeless']()}>
</SettingWrapper>
<SettingWrapper title={t['Edgeless']()}>
{Object.entries(edgelessShortcuts).map(([title, shortcuts]) => {
return (
<div key={title} className={shortcutRow}>
@@ -53,8 +53,8 @@ export const Shortcuts = () => {
</div>
);
})}
</Wrapper>
<Wrapper title={t['Markdown Syntax']()}>
</SettingWrapper>
<SettingWrapper title={t['Markdown Syntax']()}>
{Object.entries(markdownShortcuts).map(([title, shortcuts]) => {
return (
<div key={title} className={shortcutRow}>
@@ -63,7 +63,7 @@ export const Shortcuts = () => {
</div>
);
})}
</Wrapper>
</SettingWrapper>
</>
);
};

View File

@@ -1,4 +1,7 @@
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
import {
SettingModal as SettingModalBase,
type SettingModalProps,
} from '@affine/component/setting-components';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
@@ -6,13 +9,11 @@ import type {
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ContactWithUsIcon } from '@blocksuite/icons';
import type { NextRouter } from 'next/router';
import type React from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useWorkspaces } from '../../../hooks/use-workspaces';
import type { BlockSuiteWorkspace } from '../../../shared';
import { AccountSetting } from './account-setting';
import {
GeneralSetting,
@@ -24,15 +25,7 @@ import { settingContent } from './style.css';
import type { Workspace } from './type';
import { WorkSpaceSetting } from './workspace-setting';
export type QuickSearchModalProps = {
currentWorkspace?: BlockSuiteWorkspace;
workspaceList?: BlockSuiteWorkspace[];
open: boolean;
setOpen: (value: boolean) => void;
router: NextRouter;
};
export const SettingModal: React.FC<QuickSearchModalProps> = ({
export const SettingModal: React.FC<SettingModalProps> = ({
open,
setOpen,
}) => {
@@ -55,9 +48,6 @@ export const SettingModal: React.FC<QuickSearchModalProps> = ({
generalKey: generalSettingList[0].key,
isAccount: false,
});
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
const onGeneralSettingClick = useCallback((key: GeneralSettingKeys) => {
setCurrentRef({
@@ -82,59 +72,41 @@ export const SettingModal: React.FC<QuickSearchModalProps> = ({
}, []);
return (
<Modal
open={open}
onClose={handleClose}
wrapperPosition={['center', 'center']}
data-testid="setting-modal"
>
<ModalWrapper
width={1080}
height={760}
style={{
maxHeight: '85vh',
maxWidth: '70vw',
overflow: 'hidden',
display: 'flex',
}}
>
<ModalCloseButton top={16} right={20} onClick={handleClose} />
<SettingModalBase open={open} setOpen={setOpen}>
<SettingSidebar
generalSettingList={generalSettingList}
onGeneralSettingClick={onGeneralSettingClick}
currentWorkspace={
currentWorkspace as AffineLegacyCloudWorkspace | LocalWorkspace
}
workspaceList={workspaceList}
onWorkspaceSettingClick={onWorkspaceSettingClick}
selectedGeneralKey={currentRef.generalKey}
selectedWorkspace={currentRef.workspace}
onAccountSettingClick={onAccountSettingClick}
/>
<SettingSidebar
generalSettingList={generalSettingList}
onGeneralSettingClick={onGeneralSettingClick}
currentWorkspace={
currentWorkspace as AffineLegacyCloudWorkspace | LocalWorkspace
}
workspaceList={workspaceList}
onWorkspaceSettingClick={onWorkspaceSettingClick}
selectedGeneralKey={currentRef.generalKey}
selectedWorkspace={currentRef.workspace}
onAccountSettingClick={onAccountSettingClick}
/>
<div className={settingContent}>
<div className="wrapper">
<div className="content">
{currentRef.workspace ? (
<WorkSpaceSetting workspace={currentRef.workspace} />
) : null}
{currentRef.generalKey ? (
<GeneralSetting generalKey={currentRef.generalKey} />
) : null}
{currentRef.isAccount ? <AccountSetting /> : null}
</div>
<div className="footer">
<ContactWithUsIcon />
<a href="https://community.affine.pro/home" target="_blank">
{t[
'Need more customization options? You can suggest them to us in the community.'
]()}
</a>
</div>
<div className={settingContent}>
<div className="wrapper">
<div className="content">
{currentRef.workspace ? (
<WorkSpaceSetting workspace={currentRef.workspace} />
) : null}
{currentRef.generalKey ? (
<GeneralSetting generalKey={currentRef.generalKey} />
) : null}
{currentRef.isAccount ? <AccountSetting /> : null}
</div>
<div className="footer">
<ContactWithUsIcon />
<a href="https://community.affine.pro/home" target="_blank">
{t[
'Need more customization options? You can suggest them to us in the community.'
]()}
</a>
</div>
</div>
</ModalWrapper>
</Modal>
</div>
</SettingModalBase>
);
};

View File

@@ -1,3 +1,4 @@
import { UserAvatar } from '@affine/component/user-avatar';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import type {
AffineLegacyCloudWorkspace,
@@ -5,6 +6,7 @@ import type {
} from '@affine/env/workspace';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx';
import React from 'react';
import type {
GeneralSettingKeys,
@@ -84,7 +86,13 @@ export const SettingSidebar = ({
</div>
<div className={accountButton} onClick={onAccountSettingClick}>
<div className="avatar"></div>
<UserAvatar
size={28}
name="Account NameAccount Name"
url={''}
className="avatar"
/>
<div className="content">
<div className="name" title="xxx">
Account NameAccount Name

View File

@@ -3,9 +3,7 @@ import { globalStyle, style } from '@vanilla-extract/css';
export const settingSlideBar = style({
width: '25%',
maxWidth: '242px',
// TODO: use color variable
// background: 'var(--affine-background-secondary-color)',
backgroundColor: '#F4F4F5',
background: 'var(--affine-background-secondary-color)',
padding: '20px 16px',
height: '100%',
flexShrink: 0,
@@ -107,14 +105,9 @@ export const accountButton = style({
});
globalStyle(`${accountButton} .avatar`, {
width: '28px',
height: '28px',
border: '1px solid',
borderColor: 'var(--affine-white)',
borderRadius: '14px',
flexShrink: '0',
marginRight: '10px',
background: 'red',
});
globalStyle(`${accountButton} .content`, {
flexGrow: '1',

View File

@@ -140,11 +140,7 @@ export const Setting: FC = () => {
return null;
}
return (
<SettingModal
open={openSettingModal}
setOpen={setOpenSettingModalAtom}
router={router}
/>
<SettingModal open={openSettingModal} setOpen={setOpenSettingModalAtom} />
);
};