mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 21:02:09 +03:00
feat: support remove user & workspace avatar (#4302)
This commit is contained in:
parent
e1a330a0a6
commit
465098cc9a
@ -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",
|
||||||
|
@ -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({
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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>}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export * from './workspace-selector';
|
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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');
|
||||||
|
|
@ -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';
|
@ -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')(() => {
|
@ -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}
|
||||||
|
@ -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);
|
||||||
|
@ -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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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> {
|
||||||
|
@ -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!
|
||||||
|
@ -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<{
|
||||||
|
@ -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,
|
|
||||||
};
|
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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',
|
|
||||||
});
|
|
@ -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;
|
|
@ -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',
|
|
||||||
});
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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',
|
||||||
|
5
packages/graphql/src/graphql/remove-avatar.gql
Normal file
5
packages/graphql/src/graphql/remove-avatar.gql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mutation removeAvatar {
|
||||||
|
removeAvatar {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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",
|
||||||
|
@ -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:*"
|
||||||
|
@ -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",
|
||||||
|
@ -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:*",
|
||||||
|
@ -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');
|
||||||
|
53
yarn.lock
53
yarn.lock
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user