From 465098cc9a59841ad71ab3aef03aa731dfa8f648 Mon Sep 17 00:00:00 2001 From: Qi <474021214@qq.com> Date: Thu, 14 Sep 2023 22:35:05 +0800 Subject: [PATCH] feat: support remove user & workspace avatar (#4302) --- apps/core/package.json | 2 +- .../create-workspace-modal/index.css.ts | 2 +- .../new-workspace-setting-detail/profile.tsx | 64 +++--- .../setting-modal/account-setting/index.tsx | 106 ++++++---- .../setting-modal/setting-sidebar/index.tsx | 21 +- .../setting-sidebar/style.css.ts | 13 -- apps/core/src/components/cloud/login-card.tsx | 9 +- .../src/components/pure/file-upload/index.tsx | 6 + .../WorkspaceSelector/index.ts | 1 - .../user-with-workspace-list}/index.tsx | 197 +++++++++++------- .../user-with-workspace-list}/styles.ts | 0 .../index.css.ts | 3 - .../index.tsx} | 66 +++--- .../loading-icon.tsx | 0 .../styles.ts | 28 +-- .../src/components/root-app-sidebar/index.tsx | 49 +++-- apps/core/src/layouts/workspace-layout.tsx | 12 +- apps/core/src/pages/index.tsx | 21 +- apps/core/src/providers/modal-provider.tsx | 102 +-------- apps/server/src/modules/users/resolver.ts | 21 ++ apps/server/src/schema.gql | 7 + apps/server/src/tests/app.e2e.ts | 23 +- .../src/stories/workspace-avatar.stories.tsx | 76 ------- .../components/card/workspace-card/index.tsx | 7 +- .../member-components/accept-invite-page.tsx | 8 +- .../src/components/user-avatar/index.tsx | 37 ---- .../src/components/user-avatar/style.css.ts | 31 --- .../workspace-avatar/default-avatar.tsx | 69 ------ .../components/workspace-avatar/index.css.ts | 146 ------------- .../src/components/workspace-avatar/index.tsx | 70 ------- packages/graphql/src/graphql/index.ts | 13 ++ .../graphql/src/graphql/remove-avatar.gql | 5 + packages/graphql/src/schema.ts | 12 ++ .../use-block-suite-workspace-avatar-url.ts | 6 +- plugins/copilot/package.json | 2 +- plugins/hello-world/package.json | 2 +- plugins/image-preview/package.json | 2 +- plugins/outline/package.json | 2 +- .../e2e/local-first-avatar.spec.ts | 18 +- yarn.lock | 53 ++++- 40 files changed, 504 insertions(+), 808 deletions(-) delete mode 100644 apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/index.ts rename apps/core/src/components/pure/{workspace-list-modal => workspace-slider-bar/user-with-workspace-list}/index.tsx (68%) rename apps/core/src/components/pure/{workspace-list-modal => workspace-slider-bar/user-with-workspace-list}/styles.ts (100%) rename apps/core/src/components/pure/workspace-slider-bar/{WorkspaceSelector => workspace-card}/index.css.ts (88%) rename apps/core/src/components/pure/workspace-slider-bar/{WorkspaceSelector/workspace-selector.tsx => workspace-card/index.tsx} (76%) rename apps/core/src/components/pure/workspace-slider-bar/{WorkspaceSelector => workspace-card}/loading-icon.tsx (100%) rename apps/core/src/components/pure/workspace-slider-bar/{WorkspaceSelector => workspace-card}/styles.ts (68%) delete mode 100644 apps/storybook/src/stories/workspace-avatar.stories.tsx delete mode 100644 packages/component/src/components/user-avatar/index.tsx delete mode 100644 packages/component/src/components/user-avatar/style.css.ts delete mode 100644 packages/component/src/components/workspace-avatar/default-avatar.tsx delete mode 100644 packages/component/src/components/workspace-avatar/index.css.ts delete mode 100644 packages/component/src/components/workspace-avatar/index.tsx create mode 100644 packages/graphql/src/graphql/remove-avatar.gql diff --git a/apps/core/package.json b/apps/core/package.json index 6557886545..09b78c5c32 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -39,7 +39,7 @@ "@mui/material": "^5.14.7", "@radix-ui/react-select": "^1.2.2", "@react-hookz/web": "^23.1.0", - "@toeverything/components": "^0.0.38", + "@toeverything/components": "^0.0.41", "async-call-rpc": "^6.3.1", "cmdk": "^0.2.0", "css-spring": "^4.1.0", diff --git a/apps/core/src/components/affine/create-workspace-modal/index.css.ts b/apps/core/src/components/affine/create-workspace-modal/index.css.ts index 580c927675..6f49c49fd1 100644 --- a/apps/core/src/components/affine/create-workspace-modal/index.css.ts +++ b/apps/core/src/components/affine/create-workspace-modal/index.css.ts @@ -2,7 +2,7 @@ import { globalStyle, style } from '@vanilla-extract/css'; export const header = style({ position: 'relative', - height: '44px', + marginTop: '44px', }); export const content = style({ diff --git a/apps/core/src/components/affine/new-workspace-setting-detail/profile.tsx b/apps/core/src/components/affine/new-workspace-setting-detail/profile.tsx index f05a7ead7b..be07d7922e 100644 --- a/apps/core/src/components/affine/new-workspace-setting-detail/profile.tsx +++ b/apps/core/src/components/affine/new-workspace-setting-detail/profile.tsx @@ -1,17 +1,16 @@ import { FlexWrapper, Input, Wrapper } from '@affine/component'; import { pushNotificationAtom } from '@affine/component/notification-center'; -import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CameraIcon } from '@blocksuite/icons'; +import { Avatar } from '@toeverything/components/avatar'; import { Button } from '@toeverything/components/button'; -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 clsx from 'clsx'; import { useSetAtom } from 'jotai'; import { type KeyboardEvent, + type MouseEvent, startTransition, useCallback, useState, @@ -29,7 +28,7 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => { const t = useAFFiNEI18N(); const pushNotification = useSetAtom(pushNotificationAtom); - const [, update] = useBlockSuiteWorkspaceAvatarUrl( + const [workspaceAvatar, update] = useBlockSuiteWorkspaceAvatarUrl( workspace.blockSuiteWorkspace ); @@ -38,8 +37,6 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => { ); const [input, setInput] = useState(name); - const [tooltipContainer, setTooltipContainer] = - useState(null); const handleUpdateWorkspaceName = useCallback( (name: string) => { @@ -71,35 +68,38 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => { handleUpdateWorkspaceName(input); }, [handleUpdateWorkspaceName, input]); + const handleRemoveUserAvatar = useCallback( + async (e: MouseEvent) => { + e.stopPropagation(); + await update(null); + }, + [update] + ); return (
- -
- - <> -
- -
- - -
-
-
+ : undefined} + onRemove={ + workspaceAvatar && isOwner ? handleRemoveUserAvatar : undefined + } + avatarTooltipOptions={{ content: t['Click to replace photo']() }} + removeTooltipOptions={{ content: t['Remove photo']() }} + data-testid="workspace-setting-avatar" + removeButtonProps={{ + ['data-testid' as string]: 'workspace-setting-remove-avatar-button', + }} + /> +
{t['Workspace Name']()}
diff --git a/apps/core/src/components/affine/setting-modal/account-setting/index.tsx b/apps/core/src/components/affine/setting-modal/account-setting/index.tsx index d698902b26..9942322eca 100644 --- a/apps/core/src/components/affine/setting-modal/account-setting/index.tsx +++ b/apps/core/src/components/affine/setting-modal/account-setting/index.tsx @@ -4,15 +4,24 @@ import { SettingRow, StorageProgress, } from '@affine/component/setting-components'; -import { UserAvatar } from '@affine/component/user-avatar'; -import { allBlobSizesQuery, uploadAvatarMutation } from '@affine/graphql'; +import { + allBlobSizesQuery, + removeAvatarMutation, + uploadAvatarMutation, +} from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useMutation, useQuery } from '@affine/workspace/affine/gql'; import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons'; +import { Avatar } from '@toeverything/components/avatar'; import { Button } from '@toeverything/components/button'; -import { Tooltip } from '@toeverything/components/tooltip'; 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 { useCurrentUser } from '../../../../hooks/affine/use-current-user'; @@ -21,24 +30,16 @@ import { signOutCloud } from '../../../../utils/cloud-utils'; import { Upload } from '../../../pure/file-upload'; import * as style from './style.css'; -export const AvatarAndName = () => { +export const UserAvatar = () => { const t = useAFFiNEI18N(); const user = useCurrentUser(); - const [tooltipContainer, setTooltipContainer] = - useState(null); - const [input, setInput] = useState(user.name); - const { trigger: avatarTrigger } = useMutation({ mutation: uploadAvatarMutation, }); - - const handleUpdateUserName = useCallback( - (newName: string) => { - user.update({ name: newName }).catch(console.error); - }, - [user] - ); + const { trigger: removeAvatarTrigger } = useMutation({ + mutation: removeAvatarMutation, + }); const handleUpdateUserAvatar = useCallback( async (file: File) => { @@ -51,6 +52,50 @@ export const AvatarAndName = () => { }, [avatarTrigger, user] ); + const handleRemoveUserAvatar = useCallback( + async (e: MouseEvent) => { + 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 ( + + } + 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', + }} + /> + + ); +}; + +export const AvatarAndName = () => { + const t = useAFFiNEI18N(); + const user = useCurrentUser(); + const [input, setInput] = useState(user.name); + + const handleUpdateUserName = useCallback( + (newName: string) => { + user.update({ name: newName }).catch(console.error); + }, + [user] + ); + return ( <> { spreadCol={false} > - -
- - <> -
- -
- - -
-
-
+ + +
diff --git a/apps/core/src/components/affine/setting-modal/setting-sidebar/index.tsx b/apps/core/src/components/affine/setting-modal/setting-sidebar/index.tsx index b094941fd0..461415cabb 100644 --- a/apps/core/src/components/affine/setting-modal/setting-sidebar/index.tsx +++ b/apps/core/src/components/affine/setting-modal/setting-sidebar/index.tsx @@ -2,14 +2,14 @@ import { WorkspaceListItemSkeleton, WorkspaceListSkeleton, } 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 { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { Logo1Icon } from '@blocksuite/icons'; +import { Avatar } from '@toeverything/components/avatar'; 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 { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react'; import clsx from 'clsx'; @@ -55,11 +55,13 @@ export const UserInfo = ({ className={accountButton} onClick={onAccountSettingClick} > -
@@ -221,6 +223,7 @@ const WorkspaceListItem = ({ isActive: boolean; }) => { const workspace = useStaticBlockSuiteWorkspace(meta.id); + const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace); const [workspaceName] = useBlockSuiteWorkspaceName(workspace); const ref = useRef(null); @@ -232,7 +235,15 @@ const WorkspaceListItem = ({ data-testid="workspace-list-item" ref={ref} > - + {workspaceName} {isCurrent ? ( { alignItems: 'center', }} > - +
{user.name}
{user.email}
diff --git a/apps/core/src/components/pure/file-upload/index.tsx b/apps/core/src/components/pure/file-upload/index.tsx index 87d9f60815..76ca15ed0d 100644 --- a/apps/core/src/components/pure/file-upload/index.tsx +++ b/apps/core/src/components/pure/file-upload/index.tsx @@ -8,12 +8,14 @@ export interface UploadProps { uploadType?: string; accept?: string; fileChange: (file: File) => void; + disabled?: boolean; } export const Upload = ({ fileChange, accept, children, + disabled, ...props }: PropsWithChildren) => { const t = useAFFiNEI18N(); @@ -35,6 +37,10 @@ export const Upload = ({ } }; + if (disabled) { + return children ?? ; + } + return ( {children ?? } diff --git a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/index.ts b/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/index.ts deleted file mode 100644 index 1942bc9030..0000000000 --- a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './workspace-selector'; diff --git a/apps/core/src/components/pure/workspace-list-modal/index.tsx b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx similarity index 68% rename from apps/core/src/components/pure/workspace-list-modal/index.tsx rename to apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx index 9496890f15..1a38ead079 100644 --- a/apps/core/src/components/pure/workspace-list-modal/index.tsx +++ b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx @@ -3,9 +3,10 @@ import type { AffineCloudWorkspace, LocalWorkspace, } from '@affine/env/workspace'; -import { WorkspaceFlavour } from '@affine/env/workspace'; +import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; +import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { AccountIcon, ImportIcon, @@ -15,23 +16,28 @@ import { SignOutIcon, } from '@blocksuite/icons'; 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 { Divider } from '@toeverything/components/divider'; 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 import { useSession } from 'next-auth/react'; -import { useCallback } from 'react'; +import { startTransition, useCallback, useMemo, useTransition } from 'react'; import { authAtom, + openCreateWorkspaceModalAtom, openDisableCloudAlertModalAtom, openSettingModalAtom, -} from '../../../atoms'; -import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; -import type { AllWorkspace } from '../../../shared'; -import { signOutCloud } from '../../../utils/cloud-utils'; +} from '../../../../atoms'; +import type { AllWorkspace } from '../../../../shared'; +import { signOutCloud } from '../../../../utils/cloud-utils'; +import { useNavigateHelper } from '../.././../../hooks/use-navigate-helper'; import { StyledCreateWorkspaceCardPill, StyledCreateWorkspaceCardPillContent, @@ -57,8 +63,6 @@ interface WorkspaceModalProps { disabled?: boolean; workspaces: RootWorkspaceMetadata[]; currentWorkspaceId: AllWorkspace['id'] | null; - open: boolean; - onClose: () => void; onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void; onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void; onNewWorkspace: () => void; @@ -66,10 +70,15 @@ interface WorkspaceModalProps { onMoveWorkspace: (activeId: string, overId: string) => void; } -const AccountMenu = () => { +const AccountMenu = ({ + onOpenAccountSetting, + onSignOut, +}: { + onOpenAccountSetting: () => void; + onSignOut: () => void; +}) => { const t = useAFFiNEI18N(); - const setOpen = useSetAtom(openSettingModalAtom); - const { jumpToIndex } = useNavigateHelper(); + return (
{ } data-testid="editor-option-menu-import" - onClick={useCallback(() => { - setOpen(prev => ({ ...prev, open: true, activeTab: 'account' })); - }, [setOpen])} + onClick={onOpenAccountSetting} > {t['com.affine.workspace.cloud.account.settings']()} @@ -93,13 +100,7 @@ const AccountMenu = () => { } data-testid="editor-option-menu-import" - onClick={useCallback(() => { - signOutCloud() - .then(() => { - jumpToIndex(); - }) - .catch(console.error); - }, [jumpToIndex])} + onClick={onSignOut} > {t['com.affine.workspace.cloud.account.logout']()} @@ -152,52 +153,108 @@ const CloudWorkSpaceList = ({ ); }; -export const WorkspaceListModal = ({ - disabled, - open, - onClose, - workspaces, - onClickWorkspace, - onClickWorkspaceSetting, - onNewWorkspace, - onAddWorkspace, - currentWorkspaceId, - onMoveWorkspace, -}: WorkspaceModalProps) => { +export const UserWithWorkspaceList = ({ + onEventEnd, +}: { + onEventEnd?: () => void; +}) => { + const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom); + + const { jumpToSubPath, jumpToIndex } = 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 = useSetAtom(openSettingModalAtom); + const setSettingModalAtom = useSetAtom(openSettingModalAtom); + const t = useAFFiNEI18N(); const setOpen = useSetAtom(authAtom); const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom); // TODO: AFFiNE Cloud support const { data: session, status } = useSession(); - const isLoggedIn = status === 'authenticated' ? true : false; - const anchorEl = document.getElementById('current-workspace'); - const cloudWorkspaces = workspaces.filter( - ({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD - ) as (AffineCloudWorkspace | LocalWorkspace)[]; - const localWorkspaces = workspaces.filter( - ({ flavour }) => flavour === WorkspaceFlavour.LOCAL - ) as (AffineCloudWorkspace | LocalWorkspace)[]; - // FIXME: replace mui popover + const isLoggedIn = useMemo(() => status === 'authenticated', [status]); + const cloudWorkspaces = useMemo( + () => + workspaces.filter( + ({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD + ) as (AffineCloudWorkspace | LocalWorkspace)[], + [workspaces] + ); + 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 ( - + <> {!isLoggedIn ? ( @@ -211,7 +268,6 @@ export const WorkspaceListModal = ({ openModal: true, })); } - onClose(); }} data-testid="cloud-signin-button" > @@ -242,7 +298,12 @@ export const WorkspaceListModal = ({ {session?.user.email} } + items={ + + } contentOptions={{ side: 'right', sideOffset: 30, @@ -263,9 +324,6 @@ export const WorkspaceListModal = ({ {isLoggedIn && cloudWorkspaces.length !== 0 ? ( <> - + ); }; diff --git a/apps/core/src/components/pure/workspace-list-modal/styles.ts b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/styles.ts similarity index 100% rename from apps/core/src/components/pure/workspace-list-modal/styles.ts rename to apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/styles.ts diff --git a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/index.css.ts b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/index.css.ts similarity index 88% rename from apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/index.css.ts rename to apps/core/src/components/pure/workspace-slider-bar/workspace-card/index.css.ts index eddd87c98c..465f98986e 100644 --- a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/index.css.ts +++ b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/index.css.ts @@ -1,7 +1,4 @@ import { createVar, keyframes, style } from '@vanilla-extract/css'; -export const workspaceAvatarStyle = style({ - flexShrink: 0, -}); export const speedVar = createVar('speedVar'); diff --git a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx similarity index 76% rename from apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx rename to apps/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx index dca54d7fa8..9bd6e50741 100644 --- a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx +++ b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx @@ -1,4 +1,3 @@ -import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { CloudWorkspaceIcon, @@ -6,11 +5,14 @@ import { NoNetworkIcon, UnsyncIcon, } from '@blocksuite/icons'; +import { Avatar } from '@toeverything/components/avatar'; 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 { atom, useAtomValue, useSetAtom } from 'jotai'; +import { atom, useSetAtom } from 'jotai'; import { - type KeyboardEvent, + forwardRef, + type HTMLAttributes, type MouseEvent, useCallback, useMemo, @@ -20,7 +22,6 @@ import { import { useDatasourceSync } from '../../../../hooks/use-datasource-sync'; import { useSystemOnline } from '../../../../hooks/use-system-online'; import type { AllWorkspace } from '../../../../shared'; -import { workspaceAvatarStyle } from './index.css'; import { Loading } from './loading-icon'; import { StyledSelectorContainer, @@ -29,13 +30,10 @@ import { StyledWorkspaceStatus, } from './styles'; -export interface WorkspaceSelectorProps { - currentWorkspace: AllWorkspace; - onClick: () => void; -} - const hoverAtom = atom(false); - +// FIXME: +// 1. Remove mui style +// 2. Refactor the code to improve readability const CloudWorkspaceStatus = () => { return ( <> @@ -161,47 +159,37 @@ const WorkspaceStatus = ({ ); }; -/** - * @todo-Doma Co-locate WorkspaceListModal with {@link WorkspaceSelector}, - * because it's never used elsewhere. - */ -export const WorkspaceSelector = ({ - currentWorkspace, - onClick, -}: WorkspaceSelectorProps) => { +export const WorkspaceCard = forwardRef< + HTMLDivElement, + { + currentWorkspace: AllWorkspace; + } & HTMLAttributes +>(({ currentWorkspace, ...props }, ref) => { const [name] = useBlockSuiteWorkspaceName( currentWorkspace.blockSuiteWorkspace ); - // Open dialog when `Enter` or `Space` pressed - // TODO-Doma Refactor with `@radix-ui/react-dialog` or other libraries that handle these out of the box and be accessible by default - // 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 [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl( + currentWorkspace.blockSuiteWorkspace ); - const isHovered = useAtomValue(hoverAtom); return ( - @@ -211,4 +199,6 @@ export const WorkspaceSelector = ({ ); -}; +}); + +WorkspaceCard.displayName = 'WorkspaceCard'; diff --git a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/loading-icon.tsx b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/loading-icon.tsx similarity index 100% rename from apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/loading-icon.tsx rename to apps/core/src/components/pure/workspace-slider-bar/workspace-card/loading-icon.tsx diff --git a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/styles.ts b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts similarity index 68% rename from apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/styles.ts rename to apps/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts index 1aa77e2000..c6da05f9dc 100644 --- a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/styles.ts +++ b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts @@ -1,22 +1,16 @@ import { displayFlex, textEllipsis } from '@affine/component'; import { styled } from '@affine/component'; -export const StyledSelectorContainer = styled('div')(({ - disableHoverBackground, -}: { - disableHoverBackground: boolean; -}) => { - return { - height: '58px', - display: 'flex', - alignItems: 'center', - padding: '0 6px', - borderRadius: '8px', - color: 'var(--affine-text-primary-color)', - ':hover': { - cursor: 'pointer', - background: disableHoverBackground ? '' : 'var(--affine-hover-color)', - }, - }; +export const StyledSelectorContainer = styled('div')({ + height: '58px', + display: 'flex', + alignItems: 'center', + padding: '0 6px', + borderRadius: '8px', + color: 'var(--affine-text-primary-color)', + ':hover': { + cursor: 'pointer', + background: 'var(--affine-hover-color)', + }, }); export const StyledSelectorWrapper = styled('div')(() => { diff --git a/apps/core/src/components/root-app-sidebar/index.tsx b/apps/core/src/components/root-app-sidebar/index.tsx index c83bc9c6cf..a28a9b97b9 100644 --- a/apps/core/src/components/root-app-sidebar/index.tsx +++ b/apps/core/src/components/root-app-sidebar/index.tsx @@ -20,10 +20,17 @@ import { } from '@blocksuite/icons'; import type { Page } from '@blocksuite/store'; import { useDroppable } from '@dnd-kit/core'; -import { NoSsr } from '@mui/material'; +import { Popover } from '@toeverything/components/popover'; import { useAtom } from 'jotai'; -import type { ReactElement } from 'react'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import type { HTMLAttributes, ReactElement } from 'react'; +import { + forwardRef, + Suspense, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { useHistoryAtom } from '../../atoms/history'; 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 { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button'; 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'; export type RootAppSidebarProps = { isPublicWorkspace: boolean; onOpenQuickSearchModal: () => void; onOpenSettingModal: () => void; - onOpenWorkspaceListModal: () => void; currentWorkspace: AllWorkspace; openPage: (pageId: string) => void; createPage: () => Page; @@ -52,7 +59,7 @@ export type RootAppSidebarProps = { }; }; -const RouteMenuLinkItem = React.forwardRef< +const RouteMenuLinkItem = forwardRef< HTMLDivElement, { currentPath: string; // todo: pass through useRouter? @@ -60,7 +67,7 @@ const RouteMenuLinkItem = React.forwardRef< icon: ReactElement; children?: ReactElement; isDraggedOver?: boolean; - } & React.HTMLAttributes + } & HTMLAttributes >(({ currentPath, path, icon, children, isDraggedOver, ...props }, ref) => { // Force active style when a page is dragged over const active = isDraggedOver || currentPath === path; @@ -94,7 +101,6 @@ export const RootAppSidebar = ({ currentPath, paths, onOpenQuickSearchModal, - onOpenWorkspaceListModal, onOpenSettingModal, }: RootAppSidebarProps): ReactElement => { const currentWorkspaceId = currentWorkspace.id; @@ -102,6 +108,7 @@ export const RootAppSidebar = ({ const { backToAll } = useCollectionManager(currentCollectionsAtom); const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace; const t = useAFFiNEI18N(); + const [openUserWorkspaceList, setOpenUserWorkspaceList] = useState(false); const onClickNewPage = useCallback(async () => { const page = createPage(); await page.waitForLoaded(); @@ -152,6 +159,9 @@ export const RootAppSidebar = ({ const trashDroppable = useDroppable({ id: DROPPABLE_SIDEBAR_TRASH, }); + const closeUserWorkspaceList = useCallback(() => { + setOpenUserWorkspaceList(false); + }, []); return ( <> @@ -166,12 +176,27 @@ export const RootAppSidebar = ({ } > - - + + + } + contentOptions={{ + // hide trigger + sideOffset: -58, + onInteractOutside: closeUserWorkspaceList, + onEscapeKeyDown: closeUserWorkspaceList, + }} + > + { + setOpenUserWorkspaceList(true); + }, [])} /> - + { @@ -178,10 +173,6 @@ export const WorkspaceLayoutInner = ({ return page; }, [currentWorkspace.blockSuiteWorkspace, helper]); - const handleOpenWorkspaceListModal = useCallback(() => { - setOpenWorkspacesModal(true); - }, [setOpenWorkspacesModal]); - const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom); const handleOpenQuickSearchModal = useCallback(() => { setOpenQuickSearchModalAtom(true); @@ -255,7 +246,6 @@ export const WorkspaceLayoutInner = ({ onOpenQuickSearchModal={handleOpenQuickSearchModal} onOpenSettingModal={handleOpenSettingModal} currentWorkspace={currentWorkspace} - onOpenWorkspaceListModal={handleOpenWorkspaceListModal} openPage={useCallback( (pageId: string) => { assertExists(currentWorkspace); diff --git a/apps/core/src/pages/index.tsx b/apps/core/src/pages/index.tsx index 57a1daae9f..21a940f54a 100644 --- a/apps/core/src/pages/index.tsx +++ b/apps/core/src/pages/index.tsx @@ -7,6 +7,8 @@ import { lazy } from 'react'; import type { LoaderFunction } 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(() => import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({ default: AllWorkspaceModals, @@ -54,5 +56,22 @@ export const loader: LoaderFunction = async () => { }; export const Component = () => { - return ; + // TODO: We need a no workspace page + return ( + <> +
+ +
+ + + ); }; diff --git a/apps/core/src/providers/modal-provider.tsx b/apps/core/src/providers/modal-provider.tsx index 6a49f1abc1..fa95760e38 100644 --- a/apps/core/src/providers/modal-provider.tsx +++ b/apps/core/src/providers/modal-provider.tsx @@ -1,20 +1,8 @@ import { WorkspaceSubPath } from '@affine/env/workspace'; -import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { assertExists } from '@blocksuite/global/utils'; -import { arrayMove } from '@dnd-kit/sortable'; -import { - currentPageIdAtom, - currentWorkspaceIdAtom, -} from '@toeverything/infra/atom'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { useAtom } from 'jotai'; import type { ReactElement } from 'react'; -import { - lazy, - startTransition, - Suspense, - useCallback, - useTransition, -} from 'react'; +import { lazy, Suspense, useCallback } from 'react'; import type { SettingAtom } from '../atoms'; import { @@ -22,7 +10,6 @@ import { openCreateWorkspaceModalAtom, openDisableCloudAlertModalAtom, openSettingModalAtom, - openWorkspacesModalAtom, } from '../atoms'; import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; 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(() => import('../components/affine/create-workspace-modal').then(module => ({ default: module.CreateWorkspaceModal, @@ -161,90 +142,14 @@ export function CurrentWorkspaceModals() { } export const AllWorkspaceModals = (): ReactElement => { - const [openWorkspacesModal, setOpenWorkspacesModal] = useAtom( - openWorkspacesModalAtom - ); const [isOpenCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom( openCreateWorkspaceModalAtom ); 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 ( <> - - { - 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])} - /> - { onCreate={useCallback( id => { setOpenCreateWorkspaceModal(false); - setOpenWorkspacesModal(false); // if jumping immediately, the page may stuck in loading state // not sure why yet .. here is a workaround setTimeout(() => { jumpToSubPath(id, WorkspaceSubPath.ALL); }); }, - [jumpToSubPath, setOpenCreateWorkspaceModal, setOpenWorkspacesModal] + [jumpToSubPath, setOpenCreateWorkspaceModal] )} /> diff --git a/apps/server/src/modules/users/resolver.ts b/apps/server/src/modules/users/resolver.ts index dd673a12d8..e9abdfd9e6 100644 --- a/apps/server/src/modules/users/resolver.ts +++ b/apps/server/src/modules/users/resolver.ts @@ -63,6 +63,11 @@ export class DeleteAccount { @Field() success!: boolean; } +@ObjectType() +export class RemoveAvatar { + @Field() + success!: boolean; +} @ObjectType() 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) @Mutation(() => DeleteAccount) async deleteAccount(@CurrentUser() user: UserType): Promise { diff --git a/apps/server/src/schema.gql b/apps/server/src/schema.gql index c4e73edc8c..1f70307a52 100644 --- a/apps/server/src/schema.gql +++ b/apps/server/src/schema.gql @@ -34,6 +34,10 @@ type DeleteAccount { success: Boolean! } +type RemoveAvatar { + success: Boolean! +} + type AddToNewFeaturesWaitingList { email: String! @@ -187,6 +191,9 @@ type Mutation { """Upload user avatar""" uploadAvatar(id: String!, avatar: Upload!): UserType! + + """Remove user avatar""" + removeAvatar: RemoveAvatar! deleteAccount: DeleteAccount! addToNewFeaturesWaitingList(type: NewFeaturesKind!, email: String!): AddToNewFeaturesWaitingList! signUp(name: String!, email: String!, password: String!): UserType! diff --git a/apps/server/src/tests/app.e2e.ts b/apps/server/src/tests/app.e2e.ts index b8b1c7f44d..05763196b2 100644 --- a/apps/server/src/tests/app.e2e.ts +++ b/apps/server/src/tests/app.e2e.ts @@ -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 png = await Transformer.fromRgbaPixels( Buffer.alloc(400 * 400 * 4).fill(255), @@ -157,6 +157,27 @@ test('should be able to upload avatar', async t => { .expect(res => { 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): Promise<{ diff --git a/apps/storybook/src/stories/workspace-avatar.stories.tsx b/apps/storybook/src/stories/workspace-avatar.stories.tsx deleted file mode 100644 index ead6128f7c..0000000000 --- a/apps/storybook/src/stories/workspace-avatar.stories.tsx +++ /dev/null @@ -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; - -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 = props => { - return ; -}; - -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 = props => { - return ; -}; - -BlobExample.args = { - size: 40, -}; - -export const Empty: StoryFn = props => { - return ; -}; - -Empty.args = { - size: 40, -}; diff --git a/packages/component/src/components/card/workspace-card/index.tsx b/packages/component/src/components/card/workspace-card/index.tsx index 5911625013..433f244bd0 100644 --- a/packages/component/src/components/card/workspace-card/index.tsx +++ b/packages/component/src/components/card/workspace-card/index.tsx @@ -2,11 +2,12 @@ import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; 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 { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react'; import { useCallback } from 'react'; -import { WorkspaceAvatar } from '../../workspace-avatar'; import { StyledCard, StyledSettingLink, @@ -68,6 +69,7 @@ export const WorkspaceCard = ({ // const t = useAFFiNEI18N(); const workspace = useStaticBlockSuiteWorkspace(meta.id); const [name] = useBlockSuiteWorkspaceName(workspace); + const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace); return ( - - + {name} diff --git a/packages/component/src/components/member-components/accept-invite-page.tsx b/packages/component/src/components/member-components/accept-invite-page.tsx index 80af002417..574a19a8ec 100644 --- a/packages/component/src/components/member-components/accept-invite-page.tsx +++ b/packages/component/src/components/member-components/accept-invite-page.tsx @@ -23,12 +23,6 @@ export const AcceptInvitePage = ({ url={inviteInfo.user.avatarUrl || ''} name={inviteInfo.user.name} size={20} - // FIXME: fix it in @toeverything/components/avatar - imageProps={{ - style: { - objectFit: 'cover', - }, - }} /> {inviteInfo.user.name} {t['invited you to join']()} @@ -37,7 +31,7 @@ export const AcceptInvitePage = ({ name={inviteInfo.workspace.name} size={20} style={{ marginLeft: 4 }} - colorfulFallback={true} + colorfulFallback /> {inviteInfo.workspace.name} diff --git a/packages/component/src/components/user-avatar/index.tsx b/packages/component/src/components/user-avatar/index.tsx deleted file mode 100644 index 617995f6ea..0000000000 --- a/packages/component/src/components/user-avatar/index.tsx +++ /dev/null @@ -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 ( - - - - {name?.slice(0, 1) || 'A'} - - - ); -}; diff --git a/packages/component/src/components/user-avatar/style.css.ts b/packages/component/src/components/user-avatar/style.css.ts deleted file mode 100644 index 837d0b656a..0000000000 --- a/packages/component/src/components/user-avatar/style.css.ts +++ /dev/null @@ -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', -}); diff --git a/packages/component/src/components/workspace-avatar/default-avatar.tsx b/packages/component/src/components/workspace-avatar/default-avatar.tsx deleted file mode 100644 index 6e8855768d..0000000000 --- a/packages/component/src/components/workspace-avatar/default-avatar.tsx +++ /dev/null @@ -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(); - - const [topColor, middleColor, bottomColor] = colors; - const [isHover, setIsHover] = useState(false); - - return ( -
{ - timer.current = window.setTimeout(() => { - setIsHover(true); - }, 300); - }} - onMouseLeave={() => { - clearTimeout(timer.current); - setIsHover(false); - }} - > -
-
-
-
- ); -}; -export default DefaultAvatar; diff --git a/packages/component/src/components/workspace-avatar/index.css.ts b/packages/component/src/components/workspace-avatar/index.css.ts deleted file mode 100644 index fa875566b9..0000000000 --- a/packages/component/src/components/workspace-avatar/index.css.ts +++ /dev/null @@ -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', -}); diff --git a/packages/component/src/components/workspace-avatar/index.tsx b/packages/component/src/components/workspace-avatar/index.tsx deleted file mode 100644 index 4dd8c2b71c..0000000000 --- a/packages/component/src/components/workspace-avatar/index.tsx +++ /dev/null @@ -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 { - workspace: Workspace; -} - -export const BlockSuiteWorkspaceAvatar = ({ - size, - workspace, - ...props -}: BlockSuiteWorkspaceAvatar) => { - const [avatar] = useBlockSuiteWorkspaceAvatarUrl(workspace); - const [name] = useBlockSuiteWorkspaceName(workspace); - - return ( - - - - - - - ); -}; - -export const WorkspaceAvatar = ({ - size = 20, - workspace, - ...props -}: WorkspaceAvatarProps) => { - if (workspace) { - return ( - - ); - } - return ( - - - - - - ); -}; diff --git a/packages/graphql/src/graphql/index.ts b/packages/graphql/src/graphql/index.ts index 4f1d11930e..88758d93d7 100644 --- a/packages/graphql/src/graphql/index.ts +++ b/packages/graphql/src/graphql/index.ts @@ -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 = { id: 'revokeMemberPermissionMutation' as const, operationName: 'revokeMemberPermission', diff --git a/packages/graphql/src/graphql/remove-avatar.gql b/packages/graphql/src/graphql/remove-avatar.gql new file mode 100644 index 0000000000..f4e62c24f1 --- /dev/null +++ b/packages/graphql/src/graphql/remove-avatar.gql @@ -0,0 +1,5 @@ +mutation removeAvatar { + removeAvatar { + success + } +} diff --git a/packages/graphql/src/schema.ts b/packages/graphql/src/schema.ts index 4987331f5c..8381dedbd9 100644 --- a/packages/graphql/src/schema.ts +++ b/packages/graphql/src/schema.ts @@ -308,6 +308,13 @@ export type LeaveWorkspaceMutation = { leaveWorkspace: boolean; }; +export type RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>; + +export type RemoveAvatarMutation = { + __typename?: 'Mutation'; + removeAvatar: { __typename?: 'RemoveAvatar'; success: boolean }; +}; + export type RevokeMemberPermissionMutationVariables = Exact<{ workspaceId: Scalars['String']['input']; userId: Scalars['String']['input']; @@ -596,6 +603,11 @@ export type Mutations = variables: LeaveWorkspaceMutationVariables; response: LeaveWorkspaceMutation; } + | { + name: 'removeAvatarMutation'; + variables: RemoveAvatarMutationVariables; + response: RemoveAvatarMutation; + } | { name: 'revokeMemberPermissionMutation'; variables: RevokeMemberPermissionMutationVariables; diff --git a/packages/hooks/src/use-block-suite-workspace-avatar-url.ts b/packages/hooks/src/use-block-suite-workspace-avatar-url.ts index f90a923cc3..38fc4a2d48 100644 --- a/packages/hooks/src/use-block-suite-workspace-avatar-url.ts +++ b/packages/hooks/src/use-block-suite-workspace-avatar-url.ts @@ -24,8 +24,12 @@ export function useBlockSuiteWorkspaceAvatarUrl( fallbackData: null, }); const setAvatar = useCallback( - async (file: File) => { + async (file: File | null) => { assertExists(blockSuiteWorkspace); + if (!file) { + blockSuiteWorkspace.meta.setAvatar(''); + return; + } const blob = new Blob([file], { type: file.type }); const blobs = await blockSuiteWorkspace.blobs; const blobId = await blobs.set(blob); diff --git a/plugins/copilot/package.json b/plugins/copilot/package.json index dcd8198d13..4868f9ef61 100644 --- a/plugins/copilot/package.json +++ b/plugins/copilot/package.json @@ -16,7 +16,7 @@ "dependencies": { "@affine/component": "workspace:*", "@affine/sdk": "workspace:*", - "@toeverything/components": "^0.0.38", + "@toeverything/components": "^0.0.41", "idb": "^7.1.1", "langchain": "^0.0.138", "marked": "^7.0.5", diff --git a/plugins/hello-world/package.json b/plugins/hello-world/package.json index 1040adcf18..12d78a8c1c 100644 --- a/plugins/hello-world/package.json +++ b/plugins/hello-world/package.json @@ -18,7 +18,7 @@ "@affine/component": "workspace:*", "@affine/sdk": "workspace:*", "@blocksuite/icons": "^2.1.33", - "@toeverything/components": "^0.0.38" + "@toeverything/components": "^0.0.41" }, "devDependencies": { "@affine/plugin-cli": "workspace:*" diff --git a/plugins/image-preview/package.json b/plugins/image-preview/package.json index b2ee093445..6ae2889ba1 100644 --- a/plugins/image-preview/package.json +++ b/plugins/image-preview/package.json @@ -17,7 +17,7 @@ "@affine/component": "workspace:*", "@affine/sdk": "workspace:*", "@blocksuite/icons": "^2.1.33", - "@toeverything/components": "^0.0.38", + "@toeverything/components": "^0.0.41", "@toeverything/theme": "^0.7.15", "clsx": "^2.0.0", "foxact": "^0.2.20", diff --git a/plugins/outline/package.json b/plugins/outline/package.json index 97d9edb48a..6fd73a5bac 100644 --- a/plugins/outline/package.json +++ b/plugins/outline/package.json @@ -18,7 +18,7 @@ "@affine/component": "workspace:*", "@affine/sdk": "workspace:*", "@blocksuite/icons": "^2.1.33", - "@toeverything/components": "^0.0.38" + "@toeverything/components": "^0.0.41" }, "devDependencies": { "@affine/plugin-cli": "workspace:*", diff --git a/tests/affine-local/e2e/local-first-avatar.spec.ts b/tests/affine-local/e2e/local-first-avatar.spec.ts index 44e27770f9..5cfa515514 100644 --- a/tests/affine-local/e2e/local-first-avatar.spec.ts +++ b/tests/affine-local/e2e/local-first-avatar.spec.ts @@ -8,7 +8,7 @@ import { } from '@affine-test/kit/utils/page-logic'; 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, workspace, }) => { @@ -40,6 +40,22 @@ test('should create a page with a local first avatar', async ({ .getAttribute('src'); // out user uploaded avatar 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(); expect(currentWorkspace.flavour).toContain('local'); diff --git a/yarn.lock b/yarn.lock index 391bfaa65c..a0d18ea66b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -231,7 +231,7 @@ __metadata: "@affine/component": "workspace:*" "@affine/plugin-cli": "workspace:*" "@affine/sdk": "workspace:*" - "@toeverything/components": ^0.0.38 + "@toeverything/components": ^0.0.41 "@types/marked": ^5.0.1 idb: ^7.1.1 jotai: ^2.4.1 @@ -282,7 +282,7 @@ __metadata: "@sentry/webpack-plugin": ^2.7.0 "@svgr/webpack": ^8.1.0 "@swc/core": ^1.3.81 - "@toeverything/components": ^0.0.38 + "@toeverything/components": ^0.0.41 "@types/lodash-es": ^4.17.9 "@types/webpack-env": ^1.18.1 async-call-rpc: ^6.3.1 @@ -458,7 +458,7 @@ __metadata: "@affine/plugin-cli": "workspace:*" "@affine/sdk": "workspace:*" "@blocksuite/icons": ^2.1.33 - "@toeverything/components": ^0.0.38 + "@toeverything/components": ^0.0.41 languageName: unknown linkType: soft @@ -484,7 +484,7 @@ __metadata: "@affine/plugin-cli": "workspace:*" "@affine/sdk": "workspace:*" "@blocksuite/icons": ^2.1.33 - "@toeverything/components": ^0.0.38 + "@toeverything/components": ^0.0.41 "@toeverything/theme": ^0.7.15 clsx: ^2.0.0 foxact: ^0.2.20 @@ -580,7 +580,7 @@ __metadata: "@affine/plugin-cli": "workspace:*" "@affine/sdk": "workspace:*" "@blocksuite/icons": ^2.1.33 - "@toeverything/components": ^0.0.38 + "@toeverything/components": ^0.0.41 jotai: ^2.4.1 react: 18.2.0 react-dom: 18.2.0 @@ -9356,6 +9356,40 @@ __metadata: languageName: node 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": version: 1.1.2 resolution: "@radix-ui/react-popper@npm:1.1.2" @@ -12474,20 +12508,21 @@ __metadata: languageName: node linkType: hard -"@toeverything/components@npm:^0.0.38": - version: 0.0.38 - resolution: "@toeverything/components@npm:0.0.38" +"@toeverything/components@npm:^0.0.41": + version: 0.0.41 + resolution: "@toeverything/components@npm:0.0.41" dependencies: "@blocksuite/icons": ^2.1.33 "@radix-ui/react-dialog": ^1.0.4 "@radix-ui/react-dropdown-menu": ^2.0.5 + "@radix-ui/react-popover": ^1.0.6 "@radix-ui/react-tooltip": ^1.0.6 peerDependencies: "@radix-ui/react-avatar": ^1 clsx: ^2 react: ^18 react-dom: ^18 - checksum: 8771f0439be2db0acffa1d6f73a2c49155acf35c5a0538c3391f98f5dfc7961f6e88e48dfc9e5dae2847b3a31a720a58dc79075db024758b74b72bcfe15eadd0 + checksum: 15ee1cba8ea7880c9aae9fb18f8f8c44a183b64972e29e852d55e097a74ec994c1b2d22d6708746055e741112decffed1db14c345f5300733969bac69ddecf87 languageName: node linkType: hard