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 GitHub
parent aabac9e921
commit db40cd35c6
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,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,24 +72,7 @@ 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}
@@ -134,7 +107,6 @@ export const SettingModal: React.FC<QuickSearchModalProps> = ({
</div>
</div>
</div>
</ModalWrapper>
</Modal>
</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} />
);
};

View File

@@ -0,0 +1,4 @@
export { SettingModal, type SettingModalProps } from './modal';
export { SettingHeader } from './setting-header';
export { SettingRow } from './setting-row';
export { SettingWrapper } from './wrapper';

View File

@@ -0,0 +1,42 @@
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
import type { FC, PropsWithChildren } from 'react';
import { useCallback } from 'react';
export type SettingModalProps = {
open: boolean;
setOpen: (value: boolean) => void;
};
export const SettingModal: FC<PropsWithChildren<SettingModalProps>> = ({
children,
open,
setOpen,
}) => {
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
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',
backgroundColor: 'var(--affine-white)',
}}
>
<ModalCloseButton top={16} right={20} onClick={handleClose} />
{children}
</ModalWrapper>
</Modal>
);
};

View File

@@ -1,22 +1,30 @@
import clsx from 'clsx';
import type { CSSProperties, FC, PropsWithChildren, ReactElement } from 'react';
import { settingRow } from './share.css';
export const SettingRow: FC<
PropsWithChildren<{
name: string;
name: string | ReactElement;
desc: string | ReactElement;
style?: CSSProperties;
onClick?: () => void;
spreadCol?: boolean;
}>
> = ({ name, desc, children, onClick, style }) => {
> = ({ name, desc, children, onClick, style, spreadCol = true }) => {
return (
<div className={settingRow} style={style} onClick={onClick}>
<div
className={clsx(settingRow, {
'two-col': spreadCol,
})}
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>
{spreadCol ? <div className="right-col">{children}</div> : children}
</div>
);
};

View File

@@ -41,14 +41,26 @@ globalStyle(`${wrapper} .title`, {
});
export const settingRow = style({
marginBottom: '25px',
color: 'var(--affine-text-primary-color)',
borderRadius: '8px',
selectors: {
'&.two-col': {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '25px',
color: 'var(--affine-text-primary-color)',
},
'&:last-of-type': {
marginBottom: '0',
},
},
});
globalStyle(`${settingRow} .left-col`, {
flexShrink: 0,
maxWidth: '100%',
});
globalStyle(`${settingRow}.two-col .left-col`, {
flexShrink: 0,
maxWidth: '80%',
});
@@ -66,4 +78,5 @@ globalStyle(`${settingRow} .right-col`, {
display: 'flex',
justifyContent: 'flex-end',
paddingLeft: '15px',
flexShrink: 0,
});

View File

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

View File

@@ -0,0 +1,37 @@
import * as Avatar from '@radix-ui/react-avatar';
import clsx from 'clsx';
import type { CSSProperties, FC } from 'react';
import * as style from './style.css';
export type UserAvatar = {
size?: number;
url?: string;
name?: string;
className?: string;
style?: CSSProperties;
};
export const UserAvatar: FC<UserAvatar> = ({
size = 20,
style: propsStyles = {},
url,
name,
className,
}) => {
return (
<Avatar.Root
style={{
width: size,
height: size,
...propsStyles,
}}
className={clsx(style.avatarRoot, className)}
>
<Avatar.Image className={style.avatarImage} src={url} alt={name} />
<Avatar.Fallback className={style.avatarFallback} delayMs={600}>
{name?.slice(0, 1) || 'A'}
</Avatar.Fallback>
</Avatar.Root>
);
};

View File

@@ -0,0 +1,31 @@
import { style } from '@vanilla-extract/css';
export const avatarRoot = style({
display: 'inline-flex',
flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
verticalAlign: 'middle',
overflow: 'hidden',
userSelect: 'none',
borderRadius: '100%',
});
export const avatarImage = style({
width: '100%',
height: '100%',
objectFit: 'cover',
borderRadius: 'inherit',
});
export const avatarFallback = style({
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'var(--affine-primary-color)',
color: 'var(--affine-white)',
fontSize: 'var(--affine-font-base)',
lineHeight: '1',
fontWeight: '500',
});

View File

@@ -10,14 +10,14 @@ export const SIZE_CONFIG = {
fontSize: 16,
borderRadius: 4,
height: 26,
padding: 24,
padding: 6,
},
[SIZE_MIDDLE]: {
iconSize: 20,
fontSize: 16,
borderRadius: 4,
height: 32,
padding: 24,
padding: 12,
},
[SIZE_DEFAULT]: {
iconSize: 24,

View File

@@ -144,6 +144,7 @@
"Publishing": "Publishing to web requires AFFiNE Cloud service.",
"Share with link": "Share with link",
"Copy Link": "Copy Link",
"Copy": "Copy",
"Publishing Description": "After publishing to the web, everyone can view the content of this workspace through the link.",
"Stop publishing": "Stop publishing",
"Publish to web": "Publish to web",
@@ -245,7 +246,7 @@
"Sync across devices with AFFiNE Cloud": "Sync across devices with AFFiNE Cloud",
"Update workspace name success": "Update workspace name success",
"Create your own workspace": "Create your own workspace",
"Storage Folder Hint": "Check or change storage location.",
"Storage Folder Hint": "Check or change storage location. Click path to edit location.",
"Save": "Save",
"Customize": "Customize",
"Move folder success": "Move folder success",
@@ -359,5 +360,17 @@
"Discover what's new": "Discover what's new",
"View the AFFiNE Changelog.": "View the AFFiNE Changelog.",
"Privacy": "Privacy",
"Terms of Use": "Terms of Use"
"Terms of Use": "Terms of Use",
"Workspace Settings with name": "{{name}}'s Settings",
"You can customize your workspace here.": "You can customize your workspace here.",
"Info": "Info",
"Storage and Export": "Storage and Export",
"Workspace Profile": "Workspace Profile",
"Only an owner can edit the the Workspace avatar and name.Changes will be shown for everyone.": "Only an owner can edit the the Workspace avatar and name.Changes will be shown for everyone.",
"Storage": "Storage",
"Workspace saved locally": "{{name}} is saved locally",
"Enable cloud hint": "The following functions rely on AFFiNE Cloud. All data is stored on the current device. You can enable AFFiNE Cloud for this workspace to keep data in sync with the cloud.",
"Unpublished hint": "Once published to the web, visitors can view the contents through the provided link.",
"Published hint": "Visitors can view the contents through the provided link.",
"Members hint": "Manage members here, invite new member by email."
}