mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-04 19:11:57 +08:00
feat: add tooltip in user & workspace setting (#4260)
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
import { FlexWrapper, Input, toast, Wrapper } from '@affine/component';
|
import { FlexWrapper, Input, Wrapper } from '@affine/component';
|
||||||
|
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
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, DoneIcon } from '@blocksuite/icons';
|
import { CameraIcon } from '@blocksuite/icons';
|
||||||
import { IconButton } 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 clsx from 'clsx';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import {
|
import {
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
startTransition,
|
startTransition,
|
||||||
@@ -24,6 +27,7 @@ export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
|
|||||||
|
|
||||||
export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
|
|
||||||
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||||
workspace.blockSuiteWorkspace
|
workspace.blockSuiteWorkspace
|
||||||
@@ -34,13 +38,18 @@ 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) => {
|
||||||
setName(name);
|
setName(name);
|
||||||
toast(t['Update workspace name success']());
|
pushNotification({
|
||||||
|
title: t['Update workspace name success'](),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[setName, t]
|
[pushNotification, setName, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSetInput = useCallback((value: string) => {
|
const handleSetInput = useCallback((value: string) => {
|
||||||
@@ -64,23 +73,34 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.profileWrapper}>
|
<div className={style.profileWrapper}>
|
||||||
<div className={clsx(style.avatarWrapper, { disable: !isOwner })}>
|
<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"
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(style.avatarWrapper, { disable: !isOwner })}
|
||||||
|
ref={setTooltipContainer}
|
||||||
>
|
>
|
||||||
<>
|
<Upload
|
||||||
<div className="camera-icon-wrapper">
|
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||||
<CameraIcon />
|
fileChange={update}
|
||||||
</div>
|
data-testid="upload-avatar"
|
||||||
<WorkspaceAvatar
|
>
|
||||||
size={56}
|
<>
|
||||||
workspace={workspace.blockSuiteWorkspace}
|
<div className="camera-icon-wrapper">
|
||||||
/>
|
<CameraIcon />
|
||||||
</>
|
</div>
|
||||||
</Upload>
|
<WorkspaceAvatar
|
||||||
</div>
|
size={56}
|
||||||
|
workspace={workspace.blockSuiteWorkspace}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</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>
|
||||||
<FlexWrapper alignItems="center" flexGrow="1">
|
<FlexWrapper alignItems="center" flexGrow="1">
|
||||||
@@ -97,16 +117,15 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
|||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
/>
|
/>
|
||||||
{input === workspace.blockSuiteWorkspace.meta.name ? null : (
|
{input === workspace.blockSuiteWorkspace.meta.name ? null : (
|
||||||
<IconButton
|
<Button
|
||||||
data-testid="save-workspace-name"
|
data-testid="save-workspace-name"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
active={true}
|
|
||||||
style={{
|
style={{
|
||||||
marginLeft: '12px',
|
marginLeft: '12px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DoneIcon />
|
{t['com.affine.editCollection.save']()}
|
||||||
</IconButton>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</FlexWrapper>
|
</FlexWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const avatarWrapper = style({
|
|||||||
height: '56px',
|
height: '56px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
flexShrink: '0',
|
flexShrink: '0',
|
||||||
selectors: {
|
selectors: {
|
||||||
@@ -43,8 +42,9 @@ globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
});
|
});
|
||||||
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
||||||
width: '100%',
|
width: '56px',
|
||||||
height: '100%',
|
height: '56px',
|
||||||
|
borderRadius: '50%',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
display: 'none',
|
display: 'none',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import { UserAvatar } from '@affine/component/user-avatar';
|
|||||||
import { allBlobSizesQuery, uploadAvatarMutation } from '@affine/graphql';
|
import { allBlobSizesQuery, 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, DoneIcon } from '@blocksuite/icons';
|
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
|
||||||
import { Button, IconButton } 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, Suspense, useCallback, useState } from 'react';
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ export const AvatarAndName = () => {
|
|||||||
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 [input, setInput] = useState<string>(user.name);
|
||||||
|
|
||||||
const { trigger: avatarTrigger } = useMutation({
|
const { trigger: avatarTrigger } = useMutation({
|
||||||
@@ -56,25 +59,32 @@ 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">
|
||||||
<div className={style.avatarWrapper}>
|
<Tooltip
|
||||||
<Upload
|
content={t['Click to replace photo']()}
|
||||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
portalOptions={{
|
||||||
fileChange={handleUpdateUserAvatar}
|
container: tooltipContainer,
|
||||||
data-testid="upload-user-avatar"
|
}}
|
||||||
>
|
>
|
||||||
<>
|
<div className={style.avatarWrapper} ref={setTooltipContainer}>
|
||||||
<div className="camera-icon-wrapper">
|
<Upload
|
||||||
<CameraIcon />
|
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||||
</div>
|
fileChange={handleUpdateUserAvatar}
|
||||||
<UserAvatar
|
data-testid="upload-user-avatar"
|
||||||
size={56}
|
>
|
||||||
name={user.name}
|
<>
|
||||||
url={user.image}
|
<div className="camera-icon-wrapper">
|
||||||
className="avatar"
|
<CameraIcon />
|
||||||
/>
|
</div>
|
||||||
</>
|
<UserAvatar
|
||||||
</Upload>
|
size={56}
|
||||||
</div>
|
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>
|
||||||
@@ -90,18 +100,17 @@ export const AvatarAndName = () => {
|
|||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
/>
|
/>
|
||||||
{input && input === user.name ? null : (
|
{input && input === user.name ? null : (
|
||||||
<IconButton
|
<Button
|
||||||
data-testid="save-user-name"
|
data-testid="save-user-name"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleUpdateUserName(input);
|
handleUpdateUserName(input);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--affine-primary-color)',
|
|
||||||
marginLeft: '12px',
|
marginLeft: '12px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DoneIcon />
|
{t['com.affine.editCollection.save']()}
|
||||||
</IconButton>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</FlexWrapper>
|
</FlexWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const avatarWrapper = style({
|
|||||||
height: '56px',
|
height: '56px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
flexShrink: '0',
|
flexShrink: '0',
|
||||||
selectors: {
|
selectors: {
|
||||||
@@ -28,8 +27,9 @@ globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
});
|
});
|
||||||
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
||||||
width: '100%',
|
width: '56px',
|
||||||
height: '100%',
|
height: '56px',
|
||||||
|
borderRadius: '50%',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
display: 'none',
|
display: 'none',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|||||||
@@ -571,6 +571,8 @@
|
|||||||
"Workspace Settings with name": "{{name}}'s Settings",
|
"Workspace Settings with name": "{{name}}'s Settings",
|
||||||
"Workspace Type": "Workspace Type",
|
"Workspace Type": "Workspace Type",
|
||||||
"You cannot delete the last workspace": "You cannot delete the last workspace",
|
"You cannot delete the last workspace": "You cannot delete the last workspace",
|
||||||
|
"Click to replace photo": "Click to replace photo",
|
||||||
|
"Remove photo": "Remove photo",
|
||||||
"Removed successfully": "Removed successfully",
|
"Removed successfully": "Removed successfully",
|
||||||
"Successfully enabled AFFiNE Cloud": "Successfully enabled AFFiNE Cloud"
|
"Successfully enabled AFFiNE Cloud": "Successfully enabled AFFiNE Cloud"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user