mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: migrate workspace setting with new design to setting modal (#2900)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
// };
|
||||
// });
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
});
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -140,11 +140,7 @@ export const Setting: FC = () => {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SettingModal
|
||||
open={openSettingModal}
|
||||
setOpen={setOpenSettingModalAtom}
|
||||
router={router}
|
||||
/>
|
||||
<SettingModal open={openSettingModal} setOpen={setOpenSettingModalAtom} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export { SettingModal, type SettingModalProps } from './modal';
|
||||
export { SettingHeader } from './setting-header';
|
||||
export { SettingRow } from './setting-row';
|
||||
export { SettingWrapper } from './wrapper';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -41,14 +41,26 @@ globalStyle(`${wrapper} .title`, {
|
||||
});
|
||||
|
||||
export const settingRow = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '25px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
borderRadius: '8px',
|
||||
selectors: {
|
||||
'&.two-col': {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
'&: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,
|
||||
});
|
||||
@@ -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}
|
||||
37
packages/component/src/components/user-avatar/index.tsx
Normal file
37
packages/component/src/components/user-avatar/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
31
packages/component/src/components/user-avatar/style.css.ts
Normal file
31
packages/component/src/components/user-avatar/style.css.ts
Normal 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',
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user