feat: support remove user & workspace avatar (#4302)

This commit is contained in:
Qi 2023-09-14 22:35:05 +08:00 committed by GitHub
parent e1a330a0a6
commit 465098cc9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 504 additions and 808 deletions

View File

@ -39,7 +39,7 @@
"@mui/material": "^5.14.7", "@mui/material": "^5.14.7",
"@radix-ui/react-select": "^1.2.2", "@radix-ui/react-select": "^1.2.2",
"@react-hookz/web": "^23.1.0", "@react-hookz/web": "^23.1.0",
"@toeverything/components": "^0.0.38", "@toeverything/components": "^0.0.41",
"async-call-rpc": "^6.3.1", "async-call-rpc": "^6.3.1",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",
"css-spring": "^4.1.0", "css-spring": "^4.1.0",

View File

@ -2,7 +2,7 @@ import { globalStyle, style } from '@vanilla-extract/css';
export const header = style({ export const header = style({
position: 'relative', position: 'relative',
height: '44px', marginTop: '44px',
}); });
export const content = style({ export const content = style({

View File

@ -1,17 +1,16 @@
import { FlexWrapper, Input, Wrapper } from '@affine/component'; import { FlexWrapper, Input, Wrapper } from '@affine/component';
import { pushNotificationAtom } from '@affine/component/notification-center'; import { pushNotificationAtom } from '@affine/component/notification-center';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CameraIcon } from '@blocksuite/icons'; import { CameraIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { Button } from '@toeverything/components/button'; import { Button } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url'; import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { import {
type KeyboardEvent, type KeyboardEvent,
type MouseEvent,
startTransition, startTransition,
useCallback, useCallback,
useState, useState,
@ -29,7 +28,7 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const pushNotification = useSetAtom(pushNotificationAtom); const pushNotification = useSetAtom(pushNotificationAtom);
const [, update] = useBlockSuiteWorkspaceAvatarUrl( const [workspaceAvatar, update] = useBlockSuiteWorkspaceAvatarUrl(
workspace.blockSuiteWorkspace workspace.blockSuiteWorkspace
); );
@ -38,8 +37,6 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
); );
const [input, setInput] = useState<string>(name); const [input, setInput] = useState<string>(name);
const [tooltipContainer, setTooltipContainer] =
useState<HTMLDivElement | null>(null);
const handleUpdateWorkspaceName = useCallback( const handleUpdateWorkspaceName = useCallback(
(name: string) => { (name: string) => {
@ -71,35 +68,38 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
handleUpdateWorkspaceName(input); handleUpdateWorkspaceName(input);
}, [handleUpdateWorkspaceName, input]); }, [handleUpdateWorkspaceName, input]);
const handleRemoveUserAvatar = useCallback(
async (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
await update(null);
},
[update]
);
return ( return (
<div className={style.profileWrapper}> <div className={style.profileWrapper}>
<Tooltip <Upload
content={t['Click to replace photo']()} accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
portalOptions={{ fileChange={update}
container: tooltipContainer, data-testid="upload-avatar"
}} disabled={!isOwner}
> >
<div <Avatar
className={clsx(style.avatarWrapper, { disable: !isOwner })} size={56}
ref={setTooltipContainer} url={workspaceAvatar}
> name={name}
<Upload colorfulFallback
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg" hoverIcon={isOwner ? <CameraIcon /> : undefined}
fileChange={update} onRemove={
data-testid="upload-avatar" workspaceAvatar && isOwner ? handleRemoveUserAvatar : undefined
> }
<> avatarTooltipOptions={{ content: t['Click to replace photo']() }}
<div className="camera-icon-wrapper"> removeTooltipOptions={{ content: t['Remove photo']() }}
<CameraIcon /> data-testid="workspace-setting-avatar"
</div> removeButtonProps={{
<WorkspaceAvatar ['data-testid' as string]: 'workspace-setting-remove-avatar-button',
size={56} }}
workspace={workspace.blockSuiteWorkspace} />
/> </Upload>
</>
</Upload>
</div>
</Tooltip>
<Wrapper marginLeft={20}> <Wrapper marginLeft={20}>
<div className={style.label}>{t['Workspace Name']()}</div> <div className={style.label}>{t['Workspace Name']()}</div>

View File

@ -4,15 +4,24 @@ import {
SettingRow, SettingRow,
StorageProgress, StorageProgress,
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import { UserAvatar } from '@affine/component/user-avatar'; import {
import { allBlobSizesQuery, uploadAvatarMutation } from '@affine/graphql'; allBlobSizesQuery,
removeAvatarMutation,
uploadAvatarMutation,
} from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation, useQuery } from '@affine/workspace/affine/gql'; import { useMutation, useQuery } from '@affine/workspace/affine/gql';
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons'; import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { Button } from '@toeverything/components/button'; import { Button } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { type FC, Suspense, useCallback, useState } from 'react'; import {
type FC,
type MouseEvent,
Suspense,
useCallback,
useState,
} from 'react';
import { authAtom } from '../../../../atoms'; import { authAtom } from '../../../../atoms';
import { useCurrentUser } from '../../../../hooks/affine/use-current-user'; import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
@ -21,24 +30,16 @@ import { signOutCloud } from '../../../../utils/cloud-utils';
import { Upload } from '../../../pure/file-upload'; import { Upload } from '../../../pure/file-upload';
import * as style from './style.css'; import * as style from './style.css';
export const AvatarAndName = () => { export const UserAvatar = () => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const user = useCurrentUser(); const user = useCurrentUser();
const [tooltipContainer, setTooltipContainer] =
useState<HTMLDivElement | null>(null);
const [input, setInput] = useState<string>(user.name);
const { trigger: avatarTrigger } = useMutation({ const { trigger: avatarTrigger } = useMutation({
mutation: uploadAvatarMutation, mutation: uploadAvatarMutation,
}); });
const { trigger: removeAvatarTrigger } = useMutation({
const handleUpdateUserName = useCallback( mutation: removeAvatarMutation,
(newName: string) => { });
user.update({ name: newName }).catch(console.error);
},
[user]
);
const handleUpdateUserAvatar = useCallback( const handleUpdateUserAvatar = useCallback(
async (file: File) => { async (file: File) => {
@ -51,6 +52,50 @@ export const AvatarAndName = () => {
}, },
[avatarTrigger, user] [avatarTrigger, user]
); );
const handleRemoveUserAvatar = useCallback(
async (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
await removeAvatarTrigger();
// XXX: This is a hack to force the user to update, since next-auth can not only use update function without params
user.update({ name: user.name }).catch(console.error);
},
[removeAvatarTrigger, user]
);
return (
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={handleUpdateUserAvatar}
data-testid="upload-user-avatar"
>
<Avatar
size={56}
name={user.name}
url={user.image}
hoverIcon={<CameraIcon />}
onRemove={user.image ? handleRemoveUserAvatar : undefined}
avatarTooltipOptions={{ content: t['Click to replace photo']() }}
removeTooltipOptions={{ content: t['Remove photo']() }}
data-testid="user-setting-avatar"
removeButtonProps={{
['data-testid' as string]: 'user-setting-remove-avatar-button',
}}
/>
</Upload>
);
};
export const AvatarAndName = () => {
const t = useAFFiNEI18N();
const user = useCurrentUser();
const [input, setInput] = useState<string>(user.name);
const handleUpdateUserName = useCallback(
(newName: string) => {
user.update({ name: newName }).catch(console.error);
},
[user]
);
return ( return (
<> <>
<SettingRow <SettingRow
@ -59,32 +104,9 @@ export const AvatarAndName = () => {
spreadCol={false} spreadCol={false}
> >
<FlexWrapper style={{ margin: '12px 0 24px 0' }} alignItems="center"> <FlexWrapper style={{ margin: '12px 0 24px 0' }} alignItems="center">
<Tooltip <Suspense>
content={t['Click to replace photo']()} <UserAvatar />
portalOptions={{ </Suspense>
container: tooltipContainer,
}}
>
<div className={style.avatarWrapper} ref={setTooltipContainer}>
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={handleUpdateUserAvatar}
data-testid="upload-user-avatar"
>
<>
<div className="camera-icon-wrapper">
<CameraIcon />
</div>
<UserAvatar
size={56}
name={user.name}
url={user.image}
className="avatar"
/>
</>
</Upload>
</div>
</Tooltip>
<div className={style.profileInputWrapper}> <div className={style.profileInputWrapper}>
<label>{t['com.affine.settings.profile.name']()}</label> <label>{t['com.affine.settings.profile.name']()}</label>

View File

@ -2,14 +2,14 @@ import {
WorkspaceListItemSkeleton, WorkspaceListItemSkeleton,
WorkspaceListSkeleton, WorkspaceListSkeleton,
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import { UserAvatar } from '@affine/component/user-avatar';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { Logo1Icon } from '@blocksuite/icons'; import { Logo1Icon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { Tooltip } from '@toeverything/components/tooltip'; import { Tooltip } from '@toeverything/components/tooltip';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react'; import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import clsx from 'clsx'; import clsx from 'clsx';
@ -55,11 +55,13 @@ export const UserInfo = ({
className={accountButton} className={accountButton}
onClick={onAccountSettingClick} onClick={onAccountSettingClick}
> >
<UserAvatar <Avatar
size={28} size={28}
name={user.name} name={user.name}
url={user.image} url={user.image}
className="avatar" style={{
marginRight: '10px',
}}
/> />
<div className="content"> <div className="content">
@ -221,6 +223,7 @@ const WorkspaceListItem = ({
isActive: boolean; isActive: boolean;
}) => { }) => {
const workspace = useStaticBlockSuiteWorkspace(meta.id); const workspace = useStaticBlockSuiteWorkspace(meta.id);
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
const [workspaceName] = useBlockSuiteWorkspaceName(workspace); const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
const ref = useRef(null); const ref = useRef(null);
@ -232,7 +235,15 @@ const WorkspaceListItem = ({
data-testid="workspace-list-item" data-testid="workspace-list-item"
ref={ref} ref={ref}
> >
<WorkspaceAvatar size={14} workspace={workspace} className="icon" /> <Avatar
size={14}
url={workspaceAvatar}
name={workspaceName}
colorfulFallback
style={{
marginRight: '10px',
}}
/>
<span className="setting-name">{workspaceName}</span> <span className="setting-name">{workspaceName}</span>
{isCurrent ? ( {isCurrent ? (
<Tooltip <Tooltip

View File

@ -62,12 +62,6 @@ export const sidebarSelectItem = style({
}, },
}); });
globalStyle(`${settingSlideBar} .icon`, {
width: '16px',
height: '16px',
marginRight: '10px',
flexShrink: 0,
});
globalStyle(`${settingSlideBar} .setting-name`, { globalStyle(`${settingSlideBar} .setting-name`, {
minWidth: 0, minWidth: 0,
overflow: 'hidden', overflow: 'hidden',
@ -108,13 +102,6 @@ export const accountButton = style({
}, },
}); });
globalStyle(`${accountButton} .avatar`, {
border: '1px solid',
borderColor: 'var(--affine-white)',
marginRight: '10px',
flexShrink: 0,
});
globalStyle(`${accountButton} .avatar.not-sign`, { globalStyle(`${accountButton} .avatar.not-sign`, {
width: '28px', width: '28px',
height: '28px', height: '28px',

View File

@ -1,6 +1,6 @@
import { UserAvatar } from '@affine/component/user-avatar';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloudWorkspaceIcon } from '@blocksuite/icons'; import { CloudWorkspaceIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { useCurrentLoginStatus } from '../../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../../hooks/affine/use-current-login-status';
import { useCurrentUser } from '../../hooks/affine/use-current-user'; import { useCurrentUser } from '../../hooks/affine/use-current-user';
@ -37,12 +37,7 @@ const UserCard = () => {
alignItems: 'center', alignItems: 'center',
}} }}
> >
<UserAvatar <Avatar size={28} name={user.name} url={user.image} />
size={28}
name={user.name}
url={user.image}
className="avatar"
/>
<div style={{ marginLeft: '15px' }}> <div style={{ marginLeft: '15px' }}>
<div>{user.name}</div> <div>{user.name}</div>
<div>{user.email}</div> <div>{user.email}</div>

View File

@ -8,12 +8,14 @@ export interface UploadProps {
uploadType?: string; uploadType?: string;
accept?: string; accept?: string;
fileChange: (file: File) => void; fileChange: (file: File) => void;
disabled?: boolean;
} }
export const Upload = ({ export const Upload = ({
fileChange, fileChange,
accept, accept,
children, children,
disabled,
...props ...props
}: PropsWithChildren<UploadProps>) => { }: PropsWithChildren<UploadProps>) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@ -35,6 +37,10 @@ export const Upload = ({
} }
}; };
if (disabled) {
return children ?? <Button>{t['Upload']()}</Button>;
}
return ( return (
<UploadStyle onClick={_chooseFile}> <UploadStyle onClick={_chooseFile}>
{children ?? <Button>{t['Upload']()}</Button>} {children ?? <Button>{t['Upload']()}</Button>}

View File

@ -1 +0,0 @@
export * from './workspace-selector';

View File

@ -3,9 +3,10 @@ import type {
AffineCloudWorkspace, AffineCloudWorkspace,
LocalWorkspace, LocalWorkspace,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { import {
AccountIcon, AccountIcon,
ImportIcon, ImportIcon,
@ -15,23 +16,28 @@ import {
SignOutIcon, SignOutIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
import { Popover } from '@mui/material'; import { arrayMove } from '@dnd-kit/sortable';
import { IconButton } from '@toeverything/components/button'; import { IconButton } from '@toeverything/components/button';
import { Divider } from '@toeverything/components/divider'; import { Divider } from '@toeverything/components/divider';
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu'; import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
import { useSetAtom } from 'jotai'; import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/infra/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useCallback } from 'react'; import { startTransition, useCallback, useMemo, useTransition } from 'react';
import { import {
authAtom, authAtom,
openCreateWorkspaceModalAtom,
openDisableCloudAlertModalAtom, openDisableCloudAlertModalAtom,
openSettingModalAtom, openSettingModalAtom,
} from '../../../atoms'; } from '../../../../atoms';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import type { AllWorkspace } from '../../../../shared';
import type { AllWorkspace } from '../../../shared'; import { signOutCloud } from '../../../../utils/cloud-utils';
import { signOutCloud } from '../../../utils/cloud-utils'; import { useNavigateHelper } from '../.././../../hooks/use-navigate-helper';
import { import {
StyledCreateWorkspaceCardPill, StyledCreateWorkspaceCardPill,
StyledCreateWorkspaceCardPillContent, StyledCreateWorkspaceCardPillContent,
@ -57,8 +63,6 @@ interface WorkspaceModalProps {
disabled?: boolean; disabled?: boolean;
workspaces: RootWorkspaceMetadata[]; workspaces: RootWorkspaceMetadata[];
currentWorkspaceId: AllWorkspace['id'] | null; currentWorkspaceId: AllWorkspace['id'] | null;
open: boolean;
onClose: () => void;
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void; onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void; onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
onNewWorkspace: () => void; onNewWorkspace: () => void;
@ -66,10 +70,15 @@ interface WorkspaceModalProps {
onMoveWorkspace: (activeId: string, overId: string) => void; onMoveWorkspace: (activeId: string, overId: string) => void;
} }
const AccountMenu = () => { const AccountMenu = ({
onOpenAccountSetting,
onSignOut,
}: {
onOpenAccountSetting: () => void;
onSignOut: () => void;
}) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const setOpen = useSetAtom(openSettingModalAtom);
const { jumpToIndex } = useNavigateHelper();
return ( return (
<div> <div>
<MenuItem <MenuItem
@ -79,9 +88,7 @@ const AccountMenu = () => {
</MenuIcon> </MenuIcon>
} }
data-testid="editor-option-menu-import" data-testid="editor-option-menu-import"
onClick={useCallback(() => { onClick={onOpenAccountSetting}
setOpen(prev => ({ ...prev, open: true, activeTab: 'account' }));
}, [setOpen])}
> >
{t['com.affine.workspace.cloud.account.settings']()} {t['com.affine.workspace.cloud.account.settings']()}
</MenuItem> </MenuItem>
@ -93,13 +100,7 @@ const AccountMenu = () => {
</MenuIcon> </MenuIcon>
} }
data-testid="editor-option-menu-import" data-testid="editor-option-menu-import"
onClick={useCallback(() => { onClick={onSignOut}
signOutCloud()
.then(() => {
jumpToIndex();
})
.catch(console.error);
}, [jumpToIndex])}
> >
{t['com.affine.workspace.cloud.account.logout']()} {t['com.affine.workspace.cloud.account.logout']()}
</MenuItem> </MenuItem>
@ -152,52 +153,108 @@ const CloudWorkSpaceList = ({
); );
}; };
export const WorkspaceListModal = ({ export const UserWithWorkspaceList = ({
disabled, onEventEnd,
open, }: {
onClose, onEventEnd?: () => void;
workspaces, }) => {
onClickWorkspace, const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
onClickWorkspaceSetting,
onNewWorkspace, const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
onAddWorkspace, const workspaces = useAtomValue(rootWorkspacesMetadataAtom, {
currentWorkspaceId, delay: 0,
onMoveWorkspace, });
}: WorkspaceModalProps) => { const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
currentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(currentPageIdAtom);
const [, startCloseTransition] = useTransition();
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const setOpen = useSetAtom(authAtom); const setOpen = useSetAtom(authAtom);
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom); const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
// TODO: AFFiNE Cloud support // TODO: AFFiNE Cloud support
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const isLoggedIn = status === 'authenticated' ? true : false; const isLoggedIn = useMemo(() => status === 'authenticated', [status]);
const anchorEl = document.getElementById('current-workspace'); const cloudWorkspaces = useMemo(
const cloudWorkspaces = workspaces.filter( () =>
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD workspaces.filter(
) as (AffineCloudWorkspace | LocalWorkspace)[]; ({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
const localWorkspaces = workspaces.filter( ) as (AffineCloudWorkspace | LocalWorkspace)[],
({ flavour }) => flavour === WorkspaceFlavour.LOCAL [workspaces]
) as (AffineCloudWorkspace | LocalWorkspace)[]; );
// FIXME: replace mui popover const localWorkspaces = useMemo(
() =>
workspaces.filter(
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
) as (AffineCloudWorkspace | LocalWorkspace)[],
[workspaces]
);
const onClickWorkspaceSetting = useCallback(
(workspaceId: string) => {
setOpenSettingModalAtom({
open: true,
activeTab: 'workspace',
workspaceId,
});
onEventEnd?.();
},
[onEventEnd, setOpenSettingModalAtom]
);
const onMoveWorkspace = useCallback(
(activeId: string, overId: string) => {
const oldIndex = workspaces.findIndex(w => w.id === activeId);
const newIndex = workspaces.findIndex(w => w.id === overId);
startTransition(() => {
setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex));
});
},
[setWorkspaces, workspaces]
);
const onClickWorkspace = useCallback(
(workspaceId: string) => {
startCloseTransition(() => {
setCurrentWorkspaceId(workspaceId);
setCurrentPageId(null);
jumpToSubPath(workspaceId, WorkspaceSubPath.ALL);
});
onEventEnd?.();
},
[jumpToSubPath, onEventEnd, setCurrentPageId, setCurrentWorkspaceId]
);
const onNewWorkspace = useCallback(() => {
setOpenCreateWorkspaceModal('new');
onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]);
const onAddWorkspace = useCallback(async () => {
setOpenCreateWorkspaceModal('add');
onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]);
const onOpenAccountSetting = useCallback(() => {
setSettingModalAtom(prev => ({
...prev,
open: true,
activeTab: 'account',
}));
onEventEnd?.();
}, [onEventEnd, setSettingModalAtom]);
const onSignOut = useCallback(async () => {
signOutCloud()
.then(() => {
jumpToIndex();
})
.catch(console.error);
onEventEnd?.();
}, [onEventEnd, jumpToIndex]);
return ( return (
<Popover <>
sx={{
color: 'success.main',
zIndex: 999,
'& .MuiPopover-paper': {
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
boxShadow: 'var(--affine-shadow-2)',
backgroundColor: 'var(--affine-background-overlay-panel-color)',
padding: '16px 12px',
},
maxHeight: '90vh',
}}
open={open}
anchorEl={anchorEl}
onClose={onClose}
disableEnforceFocus
>
{!isLoggedIn ? ( {!isLoggedIn ? (
<StyledModalHeaderContent> <StyledModalHeaderContent>
<StyledSignInCardPill> <StyledSignInCardPill>
@ -211,7 +268,6 @@ export const WorkspaceListModal = ({
openModal: true, openModal: true,
})); }));
} }
onClose();
}} }}
data-testid="cloud-signin-button" data-testid="cloud-signin-button"
> >
@ -242,7 +298,12 @@ export const WorkspaceListModal = ({
<StyledModalTitle>{session?.user.email}</StyledModalTitle> <StyledModalTitle>{session?.user.email}</StyledModalTitle>
<StyledOperationWrapper> <StyledOperationWrapper>
<Menu <Menu
items={<AccountMenu />} items={
<AccountMenu
onOpenAccountSetting={onOpenAccountSetting}
onSignOut={onSignOut}
/>
}
contentOptions={{ contentOptions={{
side: 'right', side: 'right',
sideOffset: 30, sideOffset: 30,
@ -263,9 +324,6 @@ export const WorkspaceListModal = ({
{isLoggedIn && cloudWorkspaces.length !== 0 ? ( {isLoggedIn && cloudWorkspaces.length !== 0 ? (
<> <>
<CloudWorkSpaceList <CloudWorkSpaceList
disabled={disabled}
open={open}
onClose={onClose}
workspaces={workspaces} workspaces={workspaces}
onClickWorkspace={onClickWorkspace} onClickWorkspace={onClickWorkspace}
onClickWorkspaceSetting={onClickWorkspaceSetting} onClickWorkspaceSetting={onClickWorkspaceSetting}
@ -288,7 +346,6 @@ export const WorkspaceListModal = ({
</StyledModalHeader> </StyledModalHeader>
<StyledModalContent> <StyledModalContent>
<WorkspaceList <WorkspaceList
disabled={disabled}
items={localWorkspaces} items={localWorkspaces}
currentWorkspaceId={currentWorkspaceId} currentWorkspaceId={currentWorkspaceId}
onClick={onClickWorkspace} onClick={onClickWorkspace}
@ -335,6 +392,6 @@ export const WorkspaceListModal = ({
</StyledItem> </StyledItem>
</StyledCreateWorkspaceCardPill> </StyledCreateWorkspaceCardPill>
</StyledModalFooterContent> </StyledModalFooterContent>
</Popover> </>
); );
}; };

View File

@ -1,7 +1,4 @@
import { createVar, keyframes, style } from '@vanilla-extract/css'; import { createVar, keyframes, style } from '@vanilla-extract/css';
export const workspaceAvatarStyle = style({
flexShrink: 0,
});
export const speedVar = createVar('speedVar'); export const speedVar = createVar('speedVar');

View File

@ -1,4 +1,3 @@
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { import {
CloudWorkspaceIcon, CloudWorkspaceIcon,
@ -6,11 +5,14 @@ import {
NoNetworkIcon, NoNetworkIcon,
UnsyncIcon, UnsyncIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { Tooltip } from '@toeverything/components/tooltip'; import { Tooltip } from '@toeverything/components/tooltip';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { atom, useAtomValue, useSetAtom } from 'jotai'; import { atom, useSetAtom } from 'jotai';
import { import {
type KeyboardEvent, forwardRef,
type HTMLAttributes,
type MouseEvent, type MouseEvent,
useCallback, useCallback,
useMemo, useMemo,
@ -20,7 +22,6 @@ import {
import { useDatasourceSync } from '../../../../hooks/use-datasource-sync'; import { useDatasourceSync } from '../../../../hooks/use-datasource-sync';
import { useSystemOnline } from '../../../../hooks/use-system-online'; import { useSystemOnline } from '../../../../hooks/use-system-online';
import type { AllWorkspace } from '../../../../shared'; import type { AllWorkspace } from '../../../../shared';
import { workspaceAvatarStyle } from './index.css';
import { Loading } from './loading-icon'; import { Loading } from './loading-icon';
import { import {
StyledSelectorContainer, StyledSelectorContainer,
@ -29,13 +30,10 @@ import {
StyledWorkspaceStatus, StyledWorkspaceStatus,
} from './styles'; } from './styles';
export interface WorkspaceSelectorProps {
currentWorkspace: AllWorkspace;
onClick: () => void;
}
const hoverAtom = atom(false); const hoverAtom = atom(false);
// FIXME:
// 1. Remove mui style
// 2. Refactor the code to improve readability
const CloudWorkspaceStatus = () => { const CloudWorkspaceStatus = () => {
return ( return (
<> <>
@ -161,47 +159,37 @@ const WorkspaceStatus = ({
); );
}; };
/** export const WorkspaceCard = forwardRef<
* @todo-Doma Co-locate WorkspaceListModal with {@link WorkspaceSelector}, HTMLDivElement,
* because it's never used elsewhere. {
*/ currentWorkspace: AllWorkspace;
export const WorkspaceSelector = ({ } & HTMLAttributes<HTMLDivElement>
currentWorkspace, >(({ currentWorkspace, ...props }, ref) => {
onClick,
}: WorkspaceSelectorProps) => {
const [name] = useBlockSuiteWorkspaceName( const [name] = useBlockSuiteWorkspaceName(
currentWorkspace.blockSuiteWorkspace currentWorkspace.blockSuiteWorkspace
); );
// Open dialog when `Enter` or `Space` pressed const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(
// TODO-Doma Refactor with `@radix-ui/react-dialog` or other libraries that handle these out of the box and be accessible by default currentWorkspace.blockSuiteWorkspace
// TODO: Delete this?
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
// TODO-Doma Rename this callback to `onOpenDialog` or something to reduce ambiguity.
onClick();
}
},
[onClick]
); );
const isHovered = useAtomValue(hoverAtom);
return ( return (
<StyledSelectorContainer <StyledSelectorContainer
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={onClick}
onKeyDown={handleKeyDown}
disableHoverBackground={isHovered}
data-testid="current-workspace" data-testid="current-workspace"
id="current-workspace" id="current-workspace"
ref={ref}
{...props}
> >
<WorkspaceAvatar <Avatar
data-testid="workspace-avatar" data-testid="workspace-avatar"
className={workspaceAvatarStyle}
size={40} size={40}
workspace={currentWorkspace.blockSuiteWorkspace} url={workspaceAvatar}
name={name}
colorfulFallback
style={{
marginRight: '10px',
}}
/> />
<StyledSelectorWrapper> <StyledSelectorWrapper>
<StyledWorkspaceName data-testid="workspace-name"> <StyledWorkspaceName data-testid="workspace-name">
@ -211,4 +199,6 @@ export const WorkspaceSelector = ({
</StyledSelectorWrapper> </StyledSelectorWrapper>
</StyledSelectorContainer> </StyledSelectorContainer>
); );
}; });
WorkspaceCard.displayName = 'WorkspaceCard';

View File

@ -1,22 +1,16 @@
import { displayFlex, textEllipsis } from '@affine/component'; import { displayFlex, textEllipsis } from '@affine/component';
import { styled } from '@affine/component'; import { styled } from '@affine/component';
export const StyledSelectorContainer = styled('div')(({ export const StyledSelectorContainer = styled('div')({
disableHoverBackground, height: '58px',
}: { display: 'flex',
disableHoverBackground: boolean; alignItems: 'center',
}) => { padding: '0 6px',
return { borderRadius: '8px',
height: '58px', color: 'var(--affine-text-primary-color)',
display: 'flex', ':hover': {
alignItems: 'center', cursor: 'pointer',
padding: '0 6px', background: 'var(--affine-hover-color)',
borderRadius: '8px', },
color: 'var(--affine-text-primary-color)',
':hover': {
cursor: 'pointer',
background: disableHoverBackground ? '' : 'var(--affine-hover-color)',
},
};
}); });
export const StyledSelectorWrapper = styled('div')(() => { export const StyledSelectorWrapper = styled('div')(() => {

View File

@ -20,10 +20,17 @@ import {
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import { NoSsr } from '@mui/material'; import { Popover } from '@toeverything/components/popover';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import type { ReactElement } from 'react'; import type { HTMLAttributes, ReactElement } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react'; import {
forwardRef,
Suspense,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useHistoryAtom } from '../../atoms/history'; import { useHistoryAtom } from '../../atoms/history';
import { useAppSetting } from '../../atoms/settings'; import { useAppSetting } from '../../atoms/settings';
@ -33,14 +40,14 @@ import { CollectionsList } from '../pure/workspace-slider-bar/collections';
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button'; import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button'; import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button';
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list'; import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector'; import { UserWithWorkspaceList } from '../pure/workspace-slider-bar/user-with-workspace-list';
import { WorkspaceCard } from '../pure/workspace-slider-bar/workspace-card';
import ImportPage from './import-page'; import ImportPage from './import-page';
export type RootAppSidebarProps = { export type RootAppSidebarProps = {
isPublicWorkspace: boolean; isPublicWorkspace: boolean;
onOpenQuickSearchModal: () => void; onOpenQuickSearchModal: () => void;
onOpenSettingModal: () => void; onOpenSettingModal: () => void;
onOpenWorkspaceListModal: () => void;
currentWorkspace: AllWorkspace; currentWorkspace: AllWorkspace;
openPage: (pageId: string) => void; openPage: (pageId: string) => void;
createPage: () => Page; createPage: () => Page;
@ -52,7 +59,7 @@ export type RootAppSidebarProps = {
}; };
}; };
const RouteMenuLinkItem = React.forwardRef< const RouteMenuLinkItem = forwardRef<
HTMLDivElement, HTMLDivElement,
{ {
currentPath: string; // todo: pass through useRouter? currentPath: string; // todo: pass through useRouter?
@ -60,7 +67,7 @@ const RouteMenuLinkItem = React.forwardRef<
icon: ReactElement; icon: ReactElement;
children?: ReactElement; children?: ReactElement;
isDraggedOver?: boolean; isDraggedOver?: boolean;
} & React.HTMLAttributes<HTMLDivElement> } & HTMLAttributes<HTMLDivElement>
>(({ currentPath, path, icon, children, isDraggedOver, ...props }, ref) => { >(({ currentPath, path, icon, children, isDraggedOver, ...props }, ref) => {
// Force active style when a page is dragged over // Force active style when a page is dragged over
const active = isDraggedOver || currentPath === path; const active = isDraggedOver || currentPath === path;
@ -94,7 +101,6 @@ export const RootAppSidebar = ({
currentPath, currentPath,
paths, paths,
onOpenQuickSearchModal, onOpenQuickSearchModal,
onOpenWorkspaceListModal,
onOpenSettingModal, onOpenSettingModal,
}: RootAppSidebarProps): ReactElement => { }: RootAppSidebarProps): ReactElement => {
const currentWorkspaceId = currentWorkspace.id; const currentWorkspaceId = currentWorkspace.id;
@ -102,6 +108,7 @@ export const RootAppSidebar = ({
const { backToAll } = useCollectionManager(currentCollectionsAtom); const { backToAll } = useCollectionManager(currentCollectionsAtom);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace; const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [openUserWorkspaceList, setOpenUserWorkspaceList] = useState(false);
const onClickNewPage = useCallback(async () => { const onClickNewPage = useCallback(async () => {
const page = createPage(); const page = createPage();
await page.waitForLoaded(); await page.waitForLoaded();
@ -152,6 +159,9 @@ export const RootAppSidebar = ({
const trashDroppable = useDroppable({ const trashDroppable = useDroppable({
id: DROPPABLE_SIDEBAR_TRASH, id: DROPPABLE_SIDEBAR_TRASH,
}); });
const closeUserWorkspaceList = useCallback(() => {
setOpenUserWorkspaceList(false);
}, []);
return ( return (
<> <>
@ -166,12 +176,27 @@ export const RootAppSidebar = ({
} }
> >
<SidebarContainer> <SidebarContainer>
<NoSsr> <Popover
<WorkspaceSelector open={openUserWorkspaceList}
content={
<Suspense>
<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />
</Suspense>
}
contentOptions={{
// hide trigger
sideOffset: -58,
onInteractOutside: closeUserWorkspaceList,
onEscapeKeyDown: closeUserWorkspaceList,
}}
>
<WorkspaceCard
currentWorkspace={currentWorkspace} currentWorkspace={currentWorkspace}
onClick={onOpenWorkspaceListModal} onClick={useCallback(() => {
setOpenUserWorkspaceList(true);
}, [])}
/> />
</NoSsr> </Popover>
<QuickSearchInput <QuickSearchInput
data-testid="slider-bar-quick-search-button" data-testid="slider-bar-quick-search-button"
onClick={onOpenQuickSearchModal} onClick={onOpenQuickSearchModal}

View File

@ -36,11 +36,7 @@ import { lazy, Suspense, useCallback, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import { Map as YMap } from 'yjs'; import { Map as YMap } from 'yjs';
import { import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms';
openQuickSearchModalAtom,
openSettingModalAtom,
openWorkspacesModalAtom,
} from '../atoms';
import { mainContainerAtom } from '../atoms/element'; import { mainContainerAtom } from '../atoms/element';
import { useAppSetting } from '../atoms/settings'; import { useAppSetting } from '../atoms/settings';
import { AdapterProviderWrapper } from '../components/adapter-worksapce-wrapper'; import { AdapterProviderWrapper } from '../components/adapter-worksapce-wrapper';
@ -167,7 +163,6 @@ export const WorkspaceLayoutInner = ({
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace); usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
const helper = usePageHelper(currentWorkspace.blockSuiteWorkspace); const helper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
const handleCreatePage = useCallback(() => { const handleCreatePage = useCallback(() => {
@ -178,10 +173,6 @@ export const WorkspaceLayoutInner = ({
return page; return page;
}, [currentWorkspace.blockSuiteWorkspace, helper]); }, [currentWorkspace.blockSuiteWorkspace, helper]);
const handleOpenWorkspaceListModal = useCallback(() => {
setOpenWorkspacesModal(true);
}, [setOpenWorkspacesModal]);
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom); const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
const handleOpenQuickSearchModal = useCallback(() => { const handleOpenQuickSearchModal = useCallback(() => {
setOpenQuickSearchModalAtom(true); setOpenQuickSearchModalAtom(true);
@ -255,7 +246,6 @@ export const WorkspaceLayoutInner = ({
onOpenQuickSearchModal={handleOpenQuickSearchModal} onOpenQuickSearchModal={handleOpenQuickSearchModal}
onOpenSettingModal={handleOpenSettingModal} onOpenSettingModal={handleOpenSettingModal}
currentWorkspace={currentWorkspace} currentWorkspace={currentWorkspace}
onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
openPage={useCallback( openPage={useCallback(
(pageId: string) => { (pageId: string) => {
assertExists(currentWorkspace); assertExists(currentWorkspace);

View File

@ -7,6 +7,8 @@ import { lazy } from 'react';
import type { LoaderFunction } from 'react-router-dom'; import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom'; import { redirect } from 'react-router-dom';
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
const AllWorkspaceModals = lazy(() => const AllWorkspaceModals = lazy(() =>
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({ import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
default: AllWorkspaceModals, default: AllWorkspaceModals,
@ -54,5 +56,22 @@ export const loader: LoaderFunction = async () => {
}; };
export const Component = () => { export const Component = () => {
return <AllWorkspaceModals />; // TODO: We need a no workspace page
return (
<>
<div
style={{
width: 300,
margin: '80px auto',
borderRadius: '8px',
boxShadow: 'var(--affine-shadow-2)',
backgroundColor: 'var(--affine-background-overlay-panel-color)',
padding: '16px 12px',
}}
>
<UserWithWorkspaceList />
</div>
<AllWorkspaceModals />
</>
);
}; };

View File

@ -1,20 +1,8 @@
import { WorkspaceSubPath } from '@affine/env/workspace'; import { WorkspaceSubPath } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { arrayMove } from '@dnd-kit/sortable'; import { useAtom } from 'jotai';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/infra/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { import { lazy, Suspense, useCallback } from 'react';
lazy,
startTransition,
Suspense,
useCallback,
useTransition,
} from 'react';
import type { SettingAtom } from '../atoms'; import type { SettingAtom } from '../atoms';
import { import {
@ -22,7 +10,6 @@ import {
openCreateWorkspaceModalAtom, openCreateWorkspaceModalAtom,
openDisableCloudAlertModalAtom, openDisableCloudAlertModalAtom,
openSettingModalAtom, openSettingModalAtom,
openWorkspacesModalAtom,
} from '../atoms'; } from '../atoms';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../hooks/use-navigate-helper'; import { useNavigateHelper } from '../hooks/use-navigate-helper';
@ -38,12 +25,6 @@ const Auth = lazy(() =>
})) }))
); );
const WorkspaceListModal = lazy(() =>
import('../components/pure/workspace-list-modal').then(module => ({
default: module.WorkspaceListModal,
}))
);
const CreateWorkspaceModal = lazy(() => const CreateWorkspaceModal = lazy(() =>
import('../components/affine/create-workspace-modal').then(module => ({ import('../components/affine/create-workspace-modal').then(module => ({
default: module.CreateWorkspaceModal, default: module.CreateWorkspaceModal,
@ -161,90 +142,14 @@ export function CurrentWorkspaceModals() {
} }
export const AllWorkspaceModals = (): ReactElement => { export const AllWorkspaceModals = (): ReactElement => {
const [openWorkspacesModal, setOpenWorkspacesModal] = useAtom(
openWorkspacesModalAtom
);
const [isOpenCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom( const [isOpenCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom(
openCreateWorkspaceModalAtom openCreateWorkspaceModalAtom
); );
const { jumpToSubPath } = useNavigateHelper(); const { jumpToSubPath } = useNavigateHelper();
const workspaces = useAtomValue(rootWorkspacesMetadataAtom, {
delay: 0,
});
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
currentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(currentPageIdAtom);
const [, startCloseTransition] = useTransition();
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
const handleOpenSettingModal = useCallback(
(workspaceId: string) => {
setOpenWorkspacesModal(false);
setOpenSettingModalAtom({
open: true,
activeTab: 'workspace',
workspaceId,
});
},
[setOpenSettingModalAtom, setOpenWorkspacesModal]
);
return ( return (
<> <>
<Suspense>
<WorkspaceListModal
workspaces={workspaces}
currentWorkspaceId={currentWorkspaceId}
open={
(openWorkspacesModal || workspaces.length === 0) &&
isOpenCreateWorkspaceModal === false
}
onClose={useCallback(() => {
startCloseTransition(() => {
setOpenWorkspacesModal(false);
});
}, [setOpenWorkspacesModal])}
onMoveWorkspace={useCallback(
(activeId, overId) => {
const oldIndex = workspaces.findIndex(w => w.id === activeId);
const newIndex = workspaces.findIndex(w => w.id === overId);
startTransition(() => {
setWorkspaces(workspaces =>
arrayMove(workspaces, oldIndex, newIndex)
);
});
},
[setWorkspaces, workspaces]
)}
onClickWorkspace={useCallback(
workspaceId => {
startCloseTransition(() => {
setOpenWorkspacesModal(false);
setCurrentWorkspaceId(workspaceId);
setCurrentPageId(null);
jumpToSubPath(workspaceId, WorkspaceSubPath.ALL);
});
},
[
jumpToSubPath,
setCurrentPageId,
setCurrentWorkspaceId,
setOpenWorkspacesModal,
]
)}
onClickWorkspaceSetting={handleOpenSettingModal}
onNewWorkspace={useCallback(() => {
setOpenCreateWorkspaceModal('new');
}, [setOpenCreateWorkspaceModal])}
onAddWorkspace={useCallback(async () => {
setOpenCreateWorkspaceModal('add');
}, [setOpenCreateWorkspaceModal])}
/>
</Suspense>
<Suspense> <Suspense>
<CreateWorkspaceModal <CreateWorkspaceModal
mode={isOpenCreateWorkspaceModal} mode={isOpenCreateWorkspaceModal}
@ -254,14 +159,13 @@ export const AllWorkspaceModals = (): ReactElement => {
onCreate={useCallback( onCreate={useCallback(
id => { id => {
setOpenCreateWorkspaceModal(false); setOpenCreateWorkspaceModal(false);
setOpenWorkspacesModal(false);
// if jumping immediately, the page may stuck in loading state // if jumping immediately, the page may stuck in loading state
// not sure why yet .. here is a workaround // not sure why yet .. here is a workaround
setTimeout(() => { setTimeout(() => {
jumpToSubPath(id, WorkspaceSubPath.ALL); jumpToSubPath(id, WorkspaceSubPath.ALL);
}); });
}, },
[jumpToSubPath, setOpenCreateWorkspaceModal, setOpenWorkspacesModal] [jumpToSubPath, setOpenCreateWorkspaceModal]
)} )}
/> />
</Suspense> </Suspense>

View File

@ -63,6 +63,11 @@ export class DeleteAccount {
@Field() @Field()
success!: boolean; success!: boolean;
} }
@ObjectType()
export class RemoveAvatar {
@Field()
success!: boolean;
}
@ObjectType() @ObjectType()
export class AddToNewFeaturesWaitingList { export class AddToNewFeaturesWaitingList {
@ -151,6 +156,22 @@ export class UserResolver {
}); });
} }
@Throttle(10, 60)
@Mutation(() => RemoveAvatar, {
name: 'removeAvatar',
description: 'Remove user avatar',
})
async removeAvatar(@CurrentUser() user: UserType) {
if (!user) {
throw new BadRequestException(`User not found`);
}
await this.prisma.user.update({
where: { id: user.id },
data: { avatarUrl: null },
});
return { success: true };
}
@Throttle(10, 60) @Throttle(10, 60)
@Mutation(() => DeleteAccount) @Mutation(() => DeleteAccount)
async deleteAccount(@CurrentUser() user: UserType): Promise<DeleteAccount> { async deleteAccount(@CurrentUser() user: UserType): Promise<DeleteAccount> {

View File

@ -34,6 +34,10 @@ type DeleteAccount {
success: Boolean! success: Boolean!
} }
type RemoveAvatar {
success: Boolean!
}
type AddToNewFeaturesWaitingList { type AddToNewFeaturesWaitingList {
email: String! email: String!
@ -187,6 +191,9 @@ type Mutation {
"""Upload user avatar""" """Upload user avatar"""
uploadAvatar(id: String!, avatar: Upload!): UserType! uploadAvatar(id: String!, avatar: Upload!): UserType!
"""Remove user avatar"""
removeAvatar: RemoveAvatar!
deleteAccount: DeleteAccount! deleteAccount: DeleteAccount!
addToNewFeaturesWaitingList(type: NewFeaturesKind!, email: String!): AddToNewFeaturesWaitingList! addToNewFeaturesWaitingList(type: NewFeaturesKind!, email: String!): AddToNewFeaturesWaitingList!
signUp(name: String!, email: String!, password: String!): UserType! signUp(name: String!, email: String!, password: String!): UserType!

View File

@ -124,7 +124,7 @@ test('should find default user', async t => {
}); });
}); });
test('should be able to upload avatar', async t => { test('should be able to upload avatar and remove it', async t => {
const { token, id } = await createToken(t.context.app); const { token, id } = await createToken(t.context.app);
const png = await Transformer.fromRgbaPixels( const png = await Transformer.fromRgbaPixels(
Buffer.alloc(400 * 400 * 4).fill(255), Buffer.alloc(400 * 400 * 4).fill(255),
@ -157,6 +157,27 @@ test('should be able to upload avatar', async t => {
.expect(res => { .expect(res => {
t.is(res.body.data.uploadAvatar.id, id); t.is(res.body.data.uploadAvatar.id, id);
}); });
await request(t.context.app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation removeAvatar {
removeAvatar {
id
name
avatarUrl
email
}
}
`,
})
.expect(200)
.expect(res => {
t.is(res.body.data.removeAvatar.avatarUrl, '');
});
}); });
async function createToken(app: INestApplication<Express>): Promise<{ async function createToken(app: INestApplication<Express>): Promise<{

View File

@ -1,76 +0,0 @@
import type { WorkspaceAvatarProps } from '@affine/component/workspace-avatar';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { Schema, Workspace } from '@blocksuite/store';
import type { Meta, StoryFn } from '@storybook/react';
export default {
title: 'AFFiNE/WorkspaceAvatar',
component: WorkspaceAvatar,
argTypes: {
size: {
control: {
type: 'range',
min: 20,
max: 100,
},
},
},
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta<WorkspaceAvatarProps>;
const schema = new Schema();
schema.register(AffineSchemas).register(__unstableSchemas);
const basicBlockSuiteWorkspace = new Workspace({
id: 'blocksuite-local',
schema,
});
basicBlockSuiteWorkspace.meta.setName('Hello World');
export const Basic: StoryFn<WorkspaceAvatarProps> = props => {
return <WorkspaceAvatar {...props} workspace={basicBlockSuiteWorkspace} />;
};
Basic.args = {
size: 40,
};
const avatarBlockSuiteWorkspace = new Workspace({
id: 'blocksuite-local',
schema,
});
avatarBlockSuiteWorkspace.meta.setName('Hello World');
fetch(new URL('@affine-test/fixtures/smile.png', import.meta.url))
.then(res => res.arrayBuffer())
.then(async buffer => {
const id = await avatarBlockSuiteWorkspace.blobs.set(
new Blob([buffer], { type: 'image/png' })
);
avatarBlockSuiteWorkspace.meta.setAvatar(id);
})
.catch(() => {
// just ignore
console.error('Failed to load smile.png');
});
export const BlobExample: StoryFn<WorkspaceAvatarProps> = props => {
return <WorkspaceAvatar {...props} workspace={avatarBlockSuiteWorkspace} />;
};
BlobExample.args = {
size: 40,
};
export const Empty: StoryFn<WorkspaceAvatarProps> = props => {
return <WorkspaceAvatar {...props} workspace={null} />;
};
Empty.args = {
size: 40,
};

View File

@ -2,11 +2,12 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { SettingsIcon } from '@blocksuite/icons'; import { SettingsIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react'; import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { WorkspaceAvatar } from '../../workspace-avatar';
import { import {
StyledCard, StyledCard,
StyledSettingLink, StyledSettingLink,
@ -68,6 +69,7 @@ export const WorkspaceCard = ({
// const t = useAFFiNEI18N(); // const t = useAFFiNEI18N();
const workspace = useStaticBlockSuiteWorkspace(meta.id); const workspace = useStaticBlockSuiteWorkspace(meta.id);
const [name] = useBlockSuiteWorkspaceName(workspace); const [name] = useBlockSuiteWorkspaceName(workspace);
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
return ( return (
<StyledCard <StyledCard
@ -77,8 +79,7 @@ export const WorkspaceCard = ({
}, [onClick, meta.id])} }, [onClick, meta.id])}
active={workspace.id === currentWorkspaceId} active={workspace.id === currentWorkspaceId}
> >
<WorkspaceAvatar size={28} workspace={workspace} /> <Avatar size={28} url={workspaceAvatar} name={name} colorfulFallback />
<StyledWorkspaceInfo> <StyledWorkspaceInfo>
<StyledWorkspaceTitleArea style={{ display: 'flex' }}> <StyledWorkspaceTitleArea style={{ display: 'flex' }}>
<StyledWorkspaceTitle>{name}</StyledWorkspaceTitle> <StyledWorkspaceTitle>{name}</StyledWorkspaceTitle>

View File

@ -23,12 +23,6 @@ export const AcceptInvitePage = ({
url={inviteInfo.user.avatarUrl || ''} url={inviteInfo.user.avatarUrl || ''}
name={inviteInfo.user.name} name={inviteInfo.user.name}
size={20} size={20}
// FIXME: fix it in @toeverything/components/avatar
imageProps={{
style: {
objectFit: 'cover',
},
}}
/> />
<span className={styles.inviteName}>{inviteInfo.user.name}</span> <span className={styles.inviteName}>{inviteInfo.user.name}</span>
{t['invited you to join']()} {t['invited you to join']()}
@ -37,7 +31,7 @@ export const AcceptInvitePage = ({
name={inviteInfo.workspace.name} name={inviteInfo.workspace.name}
size={20} size={20}
style={{ marginLeft: 4 }} style={{ marginLeft: 4 }}
colorfulFallback={true} colorfulFallback
/> />
<span className={styles.inviteName}>{inviteInfo.workspace.name}</span> <span className={styles.inviteName}>{inviteInfo.workspace.name}</span>
</FlexWrapper> </FlexWrapper>

View File

@ -1,37 +0,0 @@
import * as Avatar from '@radix-ui/react-avatar';
import clsx from 'clsx';
import type { CSSProperties } from 'react';
import * as style from './style.css';
export interface UserAvatar {
size?: number;
url?: string;
name?: string;
className?: string;
style?: CSSProperties;
}
export const UserAvatar = ({
size = 20,
style: propsStyles = {},
url,
name,
className,
}: UserAvatar) => {
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

@ -1,31 +0,0 @@
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

@ -1,69 +0,0 @@
import clsx from 'clsx';
import { useMemo, useRef, useState } from 'react';
import {
DefaultAvatarBottomItemStyle,
DefaultAvatarBottomItemWithAnimationStyle,
DefaultAvatarContainerStyle,
DefaultAvatarMiddleItemStyle,
DefaultAvatarMiddleItemWithAnimationStyle,
DefaultAvatarTopItemStyle,
} from './index.css';
const colorsSchema = [
['#FF0000', '#FF00E5', '#FFAE73'],
['#FF5C00', '#FFC700', '#FFE073'],
['#FFDA16', '#FFFBA6', '#FFBE73'],
['#8CD317', '#FCFF5C', '#67CAE9'],
['#28E19F', '#89FFC6', '#39A880'],
['#35B7E0', '#77FFCE', '#5076FF'],
['#3D39FF', '#77BEFF', '#3502FF'],
['#BD08EB', '#755FFF', '#6967E4'],
];
export const DefaultAvatar = ({ name: propsName }: { name: string }) => {
// Sometimes name is ' '
const name = propsName || 'A';
const colors = useMemo(() => {
const index = name[0].toUpperCase().charCodeAt(0);
return colorsSchema[index % colorsSchema.length];
}, [name]);
const timer = useRef<number>();
const [topColor, middleColor, bottomColor] = colors;
const [isHover, setIsHover] = useState(false);
return (
<div
className={DefaultAvatarContainerStyle}
onMouseEnter={() => {
timer.current = window.setTimeout(() => {
setIsHover(true);
}, 300);
}}
onMouseLeave={() => {
clearTimeout(timer.current);
setIsHover(false);
}}
>
<div
className={DefaultAvatarTopItemStyle}
style={{ background: bottomColor }}
></div>
<div
className={clsx(DefaultAvatarMiddleItemStyle, {
[DefaultAvatarMiddleItemWithAnimationStyle]: isHover,
})}
style={{ background: middleColor }}
></div>
<div
className={clsx(DefaultAvatarBottomItemStyle, {
[DefaultAvatarBottomItemWithAnimationStyle]: isHover,
})}
style={{ background: topColor }}
></div>
</div>
);
};
export default DefaultAvatar;

View File

@ -1,146 +0,0 @@
import { keyframes, style } from '@vanilla-extract/css';
export const avatarStyle = style({
width: '100%',
height: '100%',
color: '#fff',
borderRadius: '50%',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
});
export const avatarImageStyle = style({
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center',
display: 'block',
});
const bottomAnimation = keyframes({
'0%': {
top: '-44%',
left: '-11%',
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
},
'16%': {
left: '-18%',
top: '-51%',
transform: 'matrix(-0.73, -0.69, 0.64, -0.77, 0, 0)',
},
'32%': {
left: '-7%',
top: '-40%',
transform: 'matrix(-0.97, -0.23, 0.16, -0.99, 0, 0)',
},
'48%': {
left: '-15%',
top: '-39%',
transform: 'matrix(-0.88, 0.48, -0.6, -0.8, 0, 0)',
},
'64%': {
left: '-7%',
top: '-40%',
transform: 'matrix(-0.97, -0.23, 0.16, -0.99, 0, 0)',
},
'80%': {
left: '-18%',
top: '-51%',
transform: 'matrix(-0.73, -0.69, 0.64, -0.77, 0, 0)',
},
'100%': {
top: '-44%',
left: '-11%',
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
},
});
const middleAnimation = keyframes({
'0%': {
left: '-30px',
top: '-30px',
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
},
'16%': {
left: '-37px',
top: '-37px',
transform: 'matrix(-0.86, -0.52, 0.39, -0.92, 0, 0)',
},
'32%': {
left: '-20px',
top: '-10px',
transform: 'matrix(-1, -0.02, -0.12, -0.99, 0, 0)',
},
'48%': {
left: '-27px',
top: '-2px',
transform: 'matrix(-0.88, 0.48, -0.6, -0.8, 0, 0)',
},
'64%': {
left: '-20px',
top: '-10px',
transform: 'matrix(-1, -0.02, -0.12, -0.99, 0, 0)',
},
'80%': {
left: '-37px',
top: '-37px',
transform: 'matrix(-0.86, -0.52, 0.39, -0.92, 0, 0)',
},
'100%': {
left: '-30px',
top: '-30px',
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
},
});
export const DefaultAvatarContainerStyle = style({
width: '100%',
height: '100%',
position: 'relative',
borderRadius: '50%',
overflow: 'hidden',
});
export const DefaultAvatarMiddleItemStyle = style({
width: '83%',
height: '81%',
position: 'absolute',
left: '-30%',
top: '-30%',
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
opacity: '0.8',
filter: 'blur(12px)',
transformOrigin: 'center center',
animation: `${middleAnimation} 3s ease-in-out forwards infinite`,
animationPlayState: 'paused',
});
export const DefaultAvatarMiddleItemWithAnimationStyle = style({
animationPlayState: 'running',
});
export const DefaultAvatarBottomItemStyle = style({
width: '98%',
height: '97%',
position: 'absolute',
top: '-44%',
left: '-11%',
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
opacity: '0.8',
filter: 'blur(12px)',
transformOrigin: 'center center',
willChange: 'left, top, transform',
animation: `${bottomAnimation} 3s ease-in-out forwards infinite`,
animationPlayState: 'paused',
});
export const DefaultAvatarBottomItemWithAnimationStyle = style({
animationPlayState: 'running',
});
export const DefaultAvatarTopItemStyle = style({
width: '104%',
height: '94%',
position: 'absolute',
right: '-30%',
top: '-30%',
opacity: '0.8',
filter: 'blur(12px)',
transform: 'matrix(-0.28, -0.96, 0.93, -0.37, 0, 0)',
transformOrigin: 'center center',
});

View File

@ -1,70 +0,0 @@
import type { Workspace } from '@blocksuite/store';
import * as RadixAvatar from '@radix-ui/react-avatar';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx';
import { DefaultAvatar } from './default-avatar';
import { avatarImageStyle, avatarStyle } from './index.css';
export interface WorkspaceAvatarProps {
size?: number;
workspace: Workspace | null;
className?: string;
}
export interface BlockSuiteWorkspaceAvatar
extends Omit<WorkspaceAvatarProps, 'workspace'> {
workspace: Workspace;
}
export const BlockSuiteWorkspaceAvatar = ({
size,
workspace,
...props
}: BlockSuiteWorkspaceAvatar) => {
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
const [name] = useBlockSuiteWorkspaceName(workspace);
return (
<RadixAvatar.Root
{...props}
className={clsx(avatarStyle, props.className)}
style={{
height: size,
width: size,
}}
>
<RadixAvatar.Image className={avatarImageStyle} src={avatar} alt={name} />
<RadixAvatar.Fallback>
<DefaultAvatar name={name}></DefaultAvatar>
</RadixAvatar.Fallback>
</RadixAvatar.Root>
);
};
export const WorkspaceAvatar = ({
size = 20,
workspace,
...props
}: WorkspaceAvatarProps) => {
if (workspace) {
return (
<BlockSuiteWorkspaceAvatar {...props} size={size} workspace={workspace} />
);
}
return (
<RadixAvatar.Root
{...props}
className={clsx(avatarStyle, props.className)}
style={{
height: size,
width: size,
}}
>
<RadixAvatar.Fallback>
<DefaultAvatar name="A"></DefaultAvatar>
</RadixAvatar.Fallback>
</RadixAvatar.Root>
);
};

View File

@ -336,6 +336,19 @@ mutation leaveWorkspace($workspaceId: String!, $workspaceName: String!, $sendLea
}`, }`,
}; };
export const removeAvatarMutation = {
id: 'removeAvatarMutation' as const,
operationName: 'removeAvatar',
definitionName: 'removeAvatar',
containsFile: false,
query: `
mutation removeAvatar {
removeAvatar {
success
}
}`,
};
export const revokeMemberPermissionMutation = { export const revokeMemberPermissionMutation = {
id: 'revokeMemberPermissionMutation' as const, id: 'revokeMemberPermissionMutation' as const,
operationName: 'revokeMemberPermission', operationName: 'revokeMemberPermission',

View File

@ -0,0 +1,5 @@
mutation removeAvatar {
removeAvatar {
success
}
}

View File

@ -308,6 +308,13 @@ export type LeaveWorkspaceMutation = {
leaveWorkspace: boolean; leaveWorkspace: boolean;
}; };
export type RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>;
export type RemoveAvatarMutation = {
__typename?: 'Mutation';
removeAvatar: { __typename?: 'RemoveAvatar'; success: boolean };
};
export type RevokeMemberPermissionMutationVariables = Exact<{ export type RevokeMemberPermissionMutationVariables = Exact<{
workspaceId: Scalars['String']['input']; workspaceId: Scalars['String']['input'];
userId: Scalars['String']['input']; userId: Scalars['String']['input'];
@ -596,6 +603,11 @@ export type Mutations =
variables: LeaveWorkspaceMutationVariables; variables: LeaveWorkspaceMutationVariables;
response: LeaveWorkspaceMutation; response: LeaveWorkspaceMutation;
} }
| {
name: 'removeAvatarMutation';
variables: RemoveAvatarMutationVariables;
response: RemoveAvatarMutation;
}
| { | {
name: 'revokeMemberPermissionMutation'; name: 'revokeMemberPermissionMutation';
variables: RevokeMemberPermissionMutationVariables; variables: RevokeMemberPermissionMutationVariables;

View File

@ -24,8 +24,12 @@ export function useBlockSuiteWorkspaceAvatarUrl(
fallbackData: null, fallbackData: null,
}); });
const setAvatar = useCallback( const setAvatar = useCallback(
async (file: File) => { async (file: File | null) => {
assertExists(blockSuiteWorkspace); assertExists(blockSuiteWorkspace);
if (!file) {
blockSuiteWorkspace.meta.setAvatar('');
return;
}
const blob = new Blob([file], { type: file.type }); const blob = new Blob([file], { type: file.type });
const blobs = await blockSuiteWorkspace.blobs; const blobs = await blockSuiteWorkspace.blobs;
const blobId = await blobs.set(blob); const blobId = await blobs.set(blob);

View File

@ -16,7 +16,7 @@
"dependencies": { "dependencies": {
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@affine/sdk": "workspace:*", "@affine/sdk": "workspace:*",
"@toeverything/components": "^0.0.38", "@toeverything/components": "^0.0.41",
"idb": "^7.1.1", "idb": "^7.1.1",
"langchain": "^0.0.138", "langchain": "^0.0.138",
"marked": "^7.0.5", "marked": "^7.0.5",

View File

@ -18,7 +18,7 @@
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@affine/sdk": "workspace:*", "@affine/sdk": "workspace:*",
"@blocksuite/icons": "^2.1.33", "@blocksuite/icons": "^2.1.33",
"@toeverything/components": "^0.0.38" "@toeverything/components": "^0.0.41"
}, },
"devDependencies": { "devDependencies": {
"@affine/plugin-cli": "workspace:*" "@affine/plugin-cli": "workspace:*"

View File

@ -17,7 +17,7 @@
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@affine/sdk": "workspace:*", "@affine/sdk": "workspace:*",
"@blocksuite/icons": "^2.1.33", "@blocksuite/icons": "^2.1.33",
"@toeverything/components": "^0.0.38", "@toeverything/components": "^0.0.41",
"@toeverything/theme": "^0.7.15", "@toeverything/theme": "^0.7.15",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"foxact": "^0.2.20", "foxact": "^0.2.20",

View File

@ -18,7 +18,7 @@
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@affine/sdk": "workspace:*", "@affine/sdk": "workspace:*",
"@blocksuite/icons": "^2.1.33", "@blocksuite/icons": "^2.1.33",
"@toeverything/components": "^0.0.38" "@toeverything/components": "^0.0.41"
}, },
"devDependencies": { "devDependencies": {
"@affine/plugin-cli": "workspace:*", "@affine/plugin-cli": "workspace:*",

View File

@ -8,7 +8,7 @@ import {
} from '@affine-test/kit/utils/page-logic'; } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
test('should create a page with a local first avatar', async ({ test('should create a page with a local first avatar and remove it', async ({
page, page,
workspace, workspace,
}) => { }) => {
@ -40,6 +40,22 @@ test('should create a page with a local first avatar', async ({
.getAttribute('src'); .getAttribute('src');
// out user uploaded avatar // out user uploaded avatar
expect(blobUrl).toContain('blob:'); expect(blobUrl).toContain('blob:');
// Click remove button to remove workspace avatar
await page.getByTestId('settings-modal-trigger').click();
await page.getByTestId('current-workspace-label').click();
await page.getByTestId('workspace-setting-avatar').hover();
await page.getByTestId('workspace-setting-remove-avatar-button').click();
await page.mouse.click(0, 0);
await page.waitForTimeout(1000);
await page.getByTestId('workspace-name').click();
await page.getByTestId('workspace-card').nth(1).click();
const removedAvatarImage = await page
.getByTestId('workspace-avatar')
.locator('img')
.count();
expect(removedAvatarImage).toBe(0);
const currentWorkspace = await workspace.current(); const currentWorkspace = await workspace.current();
expect(currentWorkspace.flavour).toContain('local'); expect(currentWorkspace.flavour).toContain('local');

View File

@ -231,7 +231,7 @@ __metadata:
"@affine/component": "workspace:*" "@affine/component": "workspace:*"
"@affine/plugin-cli": "workspace:*" "@affine/plugin-cli": "workspace:*"
"@affine/sdk": "workspace:*" "@affine/sdk": "workspace:*"
"@toeverything/components": ^0.0.38 "@toeverything/components": ^0.0.41
"@types/marked": ^5.0.1 "@types/marked": ^5.0.1
idb: ^7.1.1 idb: ^7.1.1
jotai: ^2.4.1 jotai: ^2.4.1
@ -282,7 +282,7 @@ __metadata:
"@sentry/webpack-plugin": ^2.7.0 "@sentry/webpack-plugin": ^2.7.0
"@svgr/webpack": ^8.1.0 "@svgr/webpack": ^8.1.0
"@swc/core": ^1.3.81 "@swc/core": ^1.3.81
"@toeverything/components": ^0.0.38 "@toeverything/components": ^0.0.41
"@types/lodash-es": ^4.17.9 "@types/lodash-es": ^4.17.9
"@types/webpack-env": ^1.18.1 "@types/webpack-env": ^1.18.1
async-call-rpc: ^6.3.1 async-call-rpc: ^6.3.1
@ -458,7 +458,7 @@ __metadata:
"@affine/plugin-cli": "workspace:*" "@affine/plugin-cli": "workspace:*"
"@affine/sdk": "workspace:*" "@affine/sdk": "workspace:*"
"@blocksuite/icons": ^2.1.33 "@blocksuite/icons": ^2.1.33
"@toeverything/components": ^0.0.38 "@toeverything/components": ^0.0.41
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -484,7 +484,7 @@ __metadata:
"@affine/plugin-cli": "workspace:*" "@affine/plugin-cli": "workspace:*"
"@affine/sdk": "workspace:*" "@affine/sdk": "workspace:*"
"@blocksuite/icons": ^2.1.33 "@blocksuite/icons": ^2.1.33
"@toeverything/components": ^0.0.38 "@toeverything/components": ^0.0.41
"@toeverything/theme": ^0.7.15 "@toeverything/theme": ^0.7.15
clsx: ^2.0.0 clsx: ^2.0.0
foxact: ^0.2.20 foxact: ^0.2.20
@ -580,7 +580,7 @@ __metadata:
"@affine/plugin-cli": "workspace:*" "@affine/plugin-cli": "workspace:*"
"@affine/sdk": "workspace:*" "@affine/sdk": "workspace:*"
"@blocksuite/icons": ^2.1.33 "@blocksuite/icons": ^2.1.33
"@toeverything/components": ^0.0.38 "@toeverything/components": ^0.0.41
jotai: ^2.4.1 jotai: ^2.4.1
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0 react-dom: 18.2.0
@ -9356,6 +9356,40 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-popover@npm:^1.0.6":
version: 1.0.6
resolution: "@radix-ui/react-popover@npm:1.0.6"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-dismissable-layer": 1.0.4
"@radix-ui/react-focus-guards": 1.0.1
"@radix-ui/react-focus-scope": 1.0.3
"@radix-ui/react-id": 1.0.1
"@radix-ui/react-popper": 1.1.2
"@radix-ui/react-portal": 1.0.3
"@radix-ui/react-presence": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-slot": 1.0.2
"@radix-ui/react-use-controllable-state": 1.0.1
aria-hidden: ^1.1.1
react-remove-scroll: 2.5.5
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: fbe4264d0f943d8be1a3ea2fce161f8031fdee72756aaf16251910e62de5030ae81a40e691f2cd65bf3a53d4ecc19c001b87be61d7aef5cc474da81d2d7e964d
languageName: node
linkType: hard
"@radix-ui/react-popper@npm:1.1.2": "@radix-ui/react-popper@npm:1.1.2":
version: 1.1.2 version: 1.1.2
resolution: "@radix-ui/react-popper@npm:1.1.2" resolution: "@radix-ui/react-popper@npm:1.1.2"
@ -12474,20 +12508,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@toeverything/components@npm:^0.0.38": "@toeverything/components@npm:^0.0.41":
version: 0.0.38 version: 0.0.41
resolution: "@toeverything/components@npm:0.0.38" resolution: "@toeverything/components@npm:0.0.41"
dependencies: dependencies:
"@blocksuite/icons": ^2.1.33 "@blocksuite/icons": ^2.1.33
"@radix-ui/react-dialog": ^1.0.4 "@radix-ui/react-dialog": ^1.0.4
"@radix-ui/react-dropdown-menu": ^2.0.5 "@radix-ui/react-dropdown-menu": ^2.0.5
"@radix-ui/react-popover": ^1.0.6
"@radix-ui/react-tooltip": ^1.0.6 "@radix-ui/react-tooltip": ^1.0.6
peerDependencies: peerDependencies:
"@radix-ui/react-avatar": ^1 "@radix-ui/react-avatar": ^1
clsx: ^2 clsx: ^2
react: ^18 react: ^18
react-dom: ^18 react-dom: ^18
checksum: 8771f0439be2db0acffa1d6f73a2c49155acf35c5a0538c3391f98f5dfc7961f6e88e48dfc9e5dae2847b3a31a720a58dc79075db024758b74b72bcfe15eadd0 checksum: 15ee1cba8ea7880c9aae9fb18f8f8c44a183b64972e29e852d55e097a74ec994c1b2d22d6708746055e741112decffed1db14c345f5300733969bac69ddecf87
languageName: node languageName: node
linkType: hard linkType: hard