mirror of
https://github.com/langgenius/dify.git
synced 2026-03-12 10:38:54 +08:00
Merge remote-tracking branch 'origin/main' into feat/model-plugins-implementing
This commit is contained in:
@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ImageInput from '@/app/components/base/app-icon-picker/ImageInput'
|
||||
import getCroppedImg from '@/app/components/base/app-icon-picker/utils'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
|
||||
@ -103,7 +103,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||
<>
|
||||
<div>
|
||||
<div className="group relative">
|
||||
<Avatar {...props} onError={(x: boolean) => setOnAvatarError(x)} />
|
||||
<Avatar {...props} onLoadingStatusChange={status => setOnAvatarError(status === 'error')} />
|
||||
<div
|
||||
className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
|
||||
@ -4,6 +4,7 @@ import type { App } from '@/types/app'
|
||||
import {
|
||||
RiGraduationCapFill,
|
||||
} from '@remixicon/react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
@ -15,11 +16,11 @@ import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import Collapse from '@/app/components/header/account-setting/collapse'
|
||||
import { IS_CE_EDITION, validPassword } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { useAppList } from '@/service/use-apps'
|
||||
import { commonQueryKeys, useUserProfile } from '@/service/use-common'
|
||||
import DeleteAccount from '../delete-account'
|
||||
|
||||
import AvatarWithEdit from './AvatarWithEdit'
|
||||
@ -37,7 +38,10 @@ export default function AccountPage() {
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const { data: appList } = useAppList({ page: 1, limit: 100, name: '' })
|
||||
const apps = appList?.data || []
|
||||
const { mutateUserProfile, userProfile } = useAppContext()
|
||||
const queryClient = useQueryClient()
|
||||
const { data: userProfileResp } = useUserProfile()
|
||||
const userProfile = userProfileResp?.profile
|
||||
const mutateUserProfile = () => queryClient.invalidateQueries({ queryKey: commonQueryKeys.userProfile })
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
|
||||
@ -53,6 +57,9 @@ export default function AccountPage() {
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
const [showUpdateEmail, setShowUpdateEmail] = useState(false)
|
||||
|
||||
if (!userProfile)
|
||||
return null
|
||||
|
||||
const handleEditName = () => {
|
||||
setEditNameModalVisible(true)
|
||||
setEditName(userProfile.name)
|
||||
@ -149,7 +156,7 @@ export default function AccountPage() {
|
||||
<h4 className="text-text-primary title-2xl-semi-bold">{t('account.myAccount', { ns: 'common' })}</h4>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6">
|
||||
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size={64} />
|
||||
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size="3xl" />
|
||||
<div className="ml-4">
|
||||
<p className="text-text-primary system-xl-semibold">
|
||||
{userProfile.name}
|
||||
|
||||
@ -7,12 +7,11 @@ import { useRouter } from 'next/navigation'
|
||||
import { Fragment } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { useLogout, useUserProfile } from '@/service/use-common'
|
||||
|
||||
export type IAppSelector = {
|
||||
isMobile: boolean
|
||||
@ -21,10 +20,15 @@ export type IAppSelector = {
|
||||
export default function AppSelector() {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
const { data: userProfileResp } = useUserProfile()
|
||||
const userProfile = userProfileResp?.profile
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
|
||||
const { mutateAsync: logout } = useLogout()
|
||||
|
||||
if (!userProfile)
|
||||
return null
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
|
||||
@ -50,7 +54,7 @@ export default function AppSelector() {
|
||||
${open && 'bg-components-panel-bg-blur'}
|
||||
`}
|
||||
>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} />
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} />
|
||||
</MenuButton>
|
||||
</div>
|
||||
<Transition
|
||||
@ -84,7 +88,7 @@ export default function AppSelector() {
|
||||
</div>
|
||||
<div className="system-xs-regular break-all text-text-tertiary">{userProfile.email}</div>
|
||||
</div>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} />
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} />
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
@ -11,14 +11,13 @@ import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useIsLogin } from '@/service/use-common'
|
||||
import { useIsLogin, useUserProfile } from '@/service/use-common'
|
||||
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
|
||||
|
||||
function buildReturnUrl(pathname: string, search: string) {
|
||||
@ -62,7 +61,8 @@ export default function OAuthAuthorize() {
|
||||
const searchParams = useSearchParams()
|
||||
const client_id = decodeURIComponent(searchParams.get('client_id') || '')
|
||||
const redirect_uri = decodeURIComponent(searchParams.get('redirect_uri') || '')
|
||||
const { userProfile } = useAppContext()
|
||||
const { data: userProfileResp } = useUserProfile()
|
||||
const userProfile = userProfileResp?.profile
|
||||
const { data: authAppInfo, isLoading: isOAuthLoading, isError } = useOAuthAppInfo(client_id, redirect_uri)
|
||||
const { mutateAsync: authorize, isPending: authorizing } = useAuthorizeOAuthApp()
|
||||
const hasNotifiedRef = useRef(false)
|
||||
@ -138,7 +138,7 @@ export default function OAuthAuthorize() {
|
||||
{isLoggedIn && userProfile && (
|
||||
<div className="flex items-center justify-between rounded-xl bg-background-section-burn-inverted p-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
|
||||
<div>
|
||||
<div className="system-md-semi-bold text-text-secondary">{userProfile.name}</div>
|
||||
<div className="text-text-tertiary system-xs-regular">{userProfile.email}</div>
|
||||
|
||||
@ -10,7 +10,7 @@ import { SubjectType } from '@/models/access-control'
|
||||
import { useSearchForWhiteListCandidates } from '@/service/access-control'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import useAccessControlStore from '../../../../context/access-control-store'
|
||||
import Avatar from '../../base/avatar'
|
||||
import { Avatar } from '../../base/avatar'
|
||||
import Button from '../../base/button'
|
||||
import Checkbox from '../../base/checkbox'
|
||||
import Input from '../../base/input'
|
||||
@ -203,7 +203,7 @@ function MemberItem({ member }: MemberItemProps) {
|
||||
<div className="flex grow items-center">
|
||||
<div className="mr-2 h-5 w-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid">
|
||||
<div className="bg-access-app-icon-mask-bg flex h-full w-full items-center justify-center">
|
||||
<Avatar className="h-[14px] w-[14px]" textClassName="text-[12px]" avatar={null} name={member.name} />
|
||||
<Avatar size="xxs" avatar={null} name={member.name} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="system-sm-medium mr-1 text-text-secondary">{member.name}</p>
|
||||
|
||||
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { useAppWhiteListSubjects } from '@/service/access-control'
|
||||
import useAccessControlStore from '../../../../context/access-control-store'
|
||||
import Avatar from '../../base/avatar'
|
||||
import { Avatar } from '../../base/avatar'
|
||||
import Loading from '../../base/loading'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import AddMemberOrGroupDialog from './add-member-or-group-pop'
|
||||
@ -106,7 +106,7 @@ function MemberItem({ member }: MemberItemProps) {
|
||||
}, [member, setSpecificMembers, specificMembers])
|
||||
return (
|
||||
<BaseItem
|
||||
icon={<Avatar className="h-[14px] w-[14px]" textClassName="text-[12px]" avatar={null} name={member.name} />}
|
||||
icon={<Avatar size="xxs" avatar={null} name={member.name} />}
|
||||
onRemove={handleRemoveMember}
|
||||
>
|
||||
<p className="system-xs-regular text-text-primary">{member.name}</p>
|
||||
|
||||
@ -91,7 +91,7 @@ vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/avatar', () => ({
|
||||
default: ({ name }: { name: string }) => <div data-testid="avatar">{name}</div>,
|
||||
Avatar: ({ name }: { name: string }) => <div data-testid="avatar">{name}</div>,
|
||||
}))
|
||||
|
||||
const createModelAndParameter = (overrides: Partial<ModelAndParameter> = {}): ModelAndParameter => ({
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { useChat } from '@/app/components/base/chat/chat/hooks'
|
||||
import { getLastAnswer } from '@/app/components/base/chat/utils'
|
||||
@ -149,7 +149,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
onSend={doSend}
|
||||
showPromptLog
|
||||
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={40} />}
|
||||
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="xl" />}
|
||||
allToolIcons={allToolIcons}
|
||||
hideLogModal
|
||||
noSpacing
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ChatConfig, ChatItem, OnSend } from '@/app/components/base/chat/ty
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import { memo, useCallback, useImperativeHandle, useMemo } from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { useChat } from '@/app/components/base/chat/chat/hooks'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
|
||||
@ -168,7 +168,7 @@ const DebugWithSingleModel = (
|
||||
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
||||
onStopResponding={handleStop}
|
||||
showPromptLog
|
||||
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={40} />}
|
||||
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="xl" />}
|
||||
allToolIcons={allToolIcons}
|
||||
onAnnotationEdited={handleAnnotationEdited}
|
||||
onAnnotationAdded={handleAnnotationAdded}
|
||||
|
||||
@ -1,308 +1,114 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import Avatar from '../index'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { Avatar } from '../index'
|
||||
|
||||
describe('Avatar', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering tests - verify component renders correctly in different states
|
||||
describe('Rendering', () => {
|
||||
it('should render img element with correct alt and src when avatar URL is provided', () => {
|
||||
const avatarUrl = 'https://example.com/avatar.jpg'
|
||||
const props = { name: 'John Doe', avatar: avatarUrl }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
it('should render img element when avatar URL is provided', () => {
|
||||
render(<Avatar name="John Doe" avatar="https://example.com/avatar.jpg" />)
|
||||
|
||||
const img = screen.getByRole('img', { name: 'John Doe' })
|
||||
expect(img).toBeInTheDocument()
|
||||
expect(img).toHaveAttribute('src', avatarUrl)
|
||||
expect(img).toHaveAttribute('src', 'https://example.com/avatar.jpg')
|
||||
})
|
||||
|
||||
it('should render fallback div with uppercase initial when avatar is null', () => {
|
||||
const props = { name: 'alice', avatar: null }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
it('should render fallback with uppercase initial when avatar is null', () => {
|
||||
render(<Avatar name="alice" avatar={null} />)
|
||||
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props tests - verify all props are applied correctly
|
||||
describe('Props', () => {
|
||||
describe('size prop', () => {
|
||||
it.each([
|
||||
{ size: undefined, expected: '30px', label: 'default (30px)' },
|
||||
{ size: 50, expected: '50px', label: 'custom (50px)' },
|
||||
])('should apply $label size to img element', ({ size, expected }) => {
|
||||
const props = { name: 'Test', avatar: 'https://example.com/avatar.jpg', size }
|
||||
it('should render both image and fallback when avatar is provided', () => {
|
||||
render(<Avatar name="John" avatar="https://example.com/avatar.jpg" />)
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
expect(screen.getByRole('img')).toHaveStyle({
|
||||
width: expected,
|
||||
height: expected,
|
||||
fontSize: expected,
|
||||
lineHeight: expected,
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply size to fallback div when avatar is null', () => {
|
||||
const props = { name: 'Test', avatar: null, size: 40 }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('T')
|
||||
const outerDiv = textElement.parentElement as HTMLElement
|
||||
expect(outerDiv).toHaveStyle({ width: '40px', height: '40px' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('className prop', () => {
|
||||
it('should merge className with default avatar classes on img', () => {
|
||||
const props = {
|
||||
name: 'Test',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
className: 'custom-class',
|
||||
}
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const img = screen.getByRole('img')
|
||||
expect(img).toHaveClass('custom-class')
|
||||
expect(img).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600')
|
||||
})
|
||||
|
||||
it('should merge className with default avatar classes on fallback div', () => {
|
||||
const props = {
|
||||
name: 'Test',
|
||||
avatar: null,
|
||||
className: 'my-custom-class',
|
||||
}
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('T')
|
||||
const outerDiv = textElement.parentElement as HTMLElement
|
||||
expect(outerDiv).toHaveClass('my-custom-class')
|
||||
expect(outerDiv).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600')
|
||||
})
|
||||
})
|
||||
|
||||
describe('textClassName prop', () => {
|
||||
it('should apply textClassName to the initial text element', () => {
|
||||
const props = {
|
||||
name: 'Test',
|
||||
avatar: null,
|
||||
textClassName: 'custom-text-class',
|
||||
}
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('T')
|
||||
expect(textElement).toHaveClass('custom-text-class')
|
||||
expect(textElement).toHaveClass('scale-[0.4]', 'text-center', 'text-white')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// State Management tests - verify useState and useEffect behavior
|
||||
describe('State Management', () => {
|
||||
it('should switch to fallback when image fails to load', async () => {
|
||||
const props = { name: 'John', avatar: 'https://example.com/broken.jpg' }
|
||||
render(<Avatar {...props} />)
|
||||
const img = screen.getByRole('img')
|
||||
|
||||
fireEvent.error(img)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should reset error state when avatar URL changes', async () => {
|
||||
const initialProps = { name: 'John', avatar: 'https://example.com/broken.jpg' }
|
||||
const { rerender } = render(<Avatar {...initialProps} />)
|
||||
const img = screen.getByRole('img')
|
||||
|
||||
// First, trigger error
|
||||
fireEvent.error(img)
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
|
||||
rerender(<Avatar name="John" avatar="https://example.com/new-avatar.jpg" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('img')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.queryByText('J')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not reset error state if avatar becomes null', async () => {
|
||||
const initialProps = { name: 'John', avatar: 'https://example.com/broken.jpg' }
|
||||
const { rerender } = render(<Avatar {...initialProps} />)
|
||||
|
||||
// Trigger error
|
||||
fireEvent.error(screen.getByRole('img'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
rerender(<Avatar name="John" avatar={null} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByRole('img')).toBeInTheDocument()
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Event Handlers tests - verify onError callback behavior
|
||||
describe('Event Handlers', () => {
|
||||
it('should call onError with true when image fails to load', () => {
|
||||
const onErrorMock = vi.fn()
|
||||
const props = {
|
||||
name: 'John',
|
||||
avatar: 'https://example.com/broken.jpg',
|
||||
onError: onErrorMock,
|
||||
}
|
||||
render(<Avatar {...props} />)
|
||||
describe('Size variants', () => {
|
||||
it.each([
|
||||
{ size: 'xxs' as const, expectedClass: 'size-4' },
|
||||
{ size: 'xs' as const, expectedClass: 'size-5' },
|
||||
{ size: 'sm' as const, expectedClass: 'size-6' },
|
||||
{ size: 'md' as const, expectedClass: 'size-8' },
|
||||
{ size: 'lg' as const, expectedClass: 'size-9' },
|
||||
{ size: 'xl' as const, expectedClass: 'size-10' },
|
||||
{ size: '2xl' as const, expectedClass: 'size-12' },
|
||||
{ size: '3xl' as const, expectedClass: 'size-16' },
|
||||
])('should apply $expectedClass for size="$size"', ({ size, expectedClass }) => {
|
||||
const { container } = render(<Avatar name="Test" avatar={null} size={size} />)
|
||||
|
||||
fireEvent.error(screen.getByRole('img'))
|
||||
|
||||
expect(onErrorMock).toHaveBeenCalledTimes(1)
|
||||
expect(onErrorMock).toHaveBeenCalledWith(true)
|
||||
const root = container.firstElementChild as HTMLElement
|
||||
expect(root).toHaveClass(expectedClass)
|
||||
})
|
||||
|
||||
it('should call onError with false when image loads successfully', () => {
|
||||
const onErrorMock = vi.fn()
|
||||
const props = {
|
||||
name: 'John',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
onError: onErrorMock,
|
||||
}
|
||||
render(<Avatar {...props} />)
|
||||
it('should default to md size when size is not specified', () => {
|
||||
const { container } = render(<Avatar name="Test" avatar={null} />)
|
||||
|
||||
fireEvent.load(screen.getByRole('img'))
|
||||
|
||||
expect(onErrorMock).toHaveBeenCalledTimes(1)
|
||||
expect(onErrorMock).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should not throw when onError is not provided', async () => {
|
||||
const props = { name: 'John', avatar: 'https://example.com/broken.jpg' }
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
expect(() => fireEvent.error(screen.getByRole('img'))).not.toThrow()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
const root = container.firstElementChild as HTMLElement
|
||||
expect(root).toHaveClass('size-8')
|
||||
})
|
||||
})
|
||||
|
||||
describe('className prop', () => {
|
||||
it('should merge className with avatar variant classes on root', () => {
|
||||
const { container } = render(
|
||||
<Avatar name="Test" avatar={null} className="custom-class" />,
|
||||
)
|
||||
|
||||
const root = container.firstElementChild as HTMLElement
|
||||
expect(root).toHaveClass('custom-class')
|
||||
expect(root).toHaveClass('rounded-full', 'bg-primary-600')
|
||||
})
|
||||
})
|
||||
|
||||
// Edge Cases tests - verify handling of unusual inputs
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty string name gracefully', () => {
|
||||
const props = { name: '', avatar: null }
|
||||
const { container } = render(<Avatar name="" avatar={null} />)
|
||||
|
||||
const { container } = render(<Avatar {...props} />)
|
||||
|
||||
// Note: Using querySelector here because empty name produces no visible text,
|
||||
// making semantic queries (getByRole, getByText) impossible
|
||||
const textElement = container.querySelector('.text-white') as HTMLElement
|
||||
expect(textElement).toBeInTheDocument()
|
||||
expect(textElement.textContent).toBe('')
|
||||
const fallback = container.querySelector('.text-white') as HTMLElement
|
||||
expect(fallback).toBeInTheDocument()
|
||||
expect(fallback.textContent).toBe('')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ name: '中文名', expected: '中', label: 'Chinese characters' },
|
||||
{ name: '123User', expected: '1', label: 'number' },
|
||||
])('should display first character when name starts with $label', ({ name, expected }) => {
|
||||
const props = { name, avatar: null }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
render(<Avatar name={name} avatar={null} />)
|
||||
|
||||
expect(screen.getByText(expected)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty string avatar as falsy value', () => {
|
||||
const props = { name: 'Test', avatar: '' as string | null }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
render(<Avatar name="Test" avatar={'' as string | null} />)
|
||||
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('T')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined className and textClassName', () => {
|
||||
const props = { name: 'Test', avatar: null }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('T')
|
||||
const outerDiv = textElement.parentElement as HTMLElement
|
||||
expect(outerDiv).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ size: 0, expected: '0px', label: 'zero' },
|
||||
{ size: 1000, expected: '1000px', label: 'very large' },
|
||||
])('should handle $label size value', ({ size, expected }) => {
|
||||
const props = { name: 'Test', avatar: null, size }
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('T')
|
||||
const outerDiv = textElement.parentElement as HTMLElement
|
||||
expect(outerDiv).toHaveStyle({ width: expected, height: expected })
|
||||
})
|
||||
})
|
||||
|
||||
// Combined props tests - verify props work together correctly
|
||||
describe('Combined Props', () => {
|
||||
it('should apply all props correctly when used together', () => {
|
||||
const onErrorMock = vi.fn()
|
||||
const props = {
|
||||
name: 'Test User',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
size: 64,
|
||||
className: 'custom-avatar',
|
||||
onError: onErrorMock,
|
||||
}
|
||||
describe('onLoadingStatusChange', () => {
|
||||
it('should render image when avatar and onLoadingStatusChange are provided', () => {
|
||||
render(
|
||||
<Avatar
|
||||
name="John"
|
||||
avatar="https://example.com/avatar.jpg"
|
||||
onLoadingStatusChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const img = screen.getByRole('img')
|
||||
expect(img).toHaveAttribute('alt', 'Test User')
|
||||
expect(img).toHaveAttribute('src', 'https://example.com/avatar.jpg')
|
||||
expect(img).toHaveStyle({ width: '64px', height: '64px' })
|
||||
expect(img).toHaveClass('custom-avatar')
|
||||
|
||||
// Trigger load to verify onError callback
|
||||
fireEvent.load(img)
|
||||
expect(onErrorMock).toHaveBeenCalledWith(false)
|
||||
expect(screen.getByRole('img')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply all fallback props correctly when used together', () => {
|
||||
const props = {
|
||||
name: 'Fallback User',
|
||||
avatar: null,
|
||||
size: 48,
|
||||
className: 'fallback-custom',
|
||||
textClassName: 'custom-text-style',
|
||||
}
|
||||
it('should not render image when avatar is null even with onLoadingStatusChange', () => {
|
||||
const onStatusChange = vi.fn()
|
||||
render(
|
||||
<Avatar name="John" avatar={null} onLoadingStatusChange={onStatusChange} />,
|
||||
)
|
||||
|
||||
render(<Avatar {...props} />)
|
||||
|
||||
const textElement = screen.getByText('F')
|
||||
const outerDiv = textElement.parentElement as HTMLElement
|
||||
expect(outerDiv).toHaveClass('fallback-custom')
|
||||
expect(outerDiv).toHaveStyle({ width: '48px', height: '48px' })
|
||||
expect(textElement).toHaveClass('custom-text-style')
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import Avatar from '.'
|
||||
import { Avatar } from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Data Display/Avatar',
|
||||
@ -7,12 +7,12 @@ const meta = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Initials or image-based avatar used across contacts and member lists. Falls back to the first letter when the image fails to load.',
|
||||
component: 'Initials or image-based avatar built on Base UI. Falls back to the first letter when the image fails to load.',
|
||||
},
|
||||
source: {
|
||||
language: 'tsx',
|
||||
code: `
|
||||
<Avatar name="Alex Doe" avatar="https://cloud.dify.ai/logo/logo.svg" size={40} />
|
||||
<Avatar name="Alex Doe" avatar="https://i.pravatar.cc/96?u=avatar-default" size="xl" />
|
||||
`.trim(),
|
||||
},
|
||||
},
|
||||
@ -20,8 +20,8 @@ const meta = {
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
name: 'Alex Doe',
|
||||
avatar: 'https://cloud.dify.ai/logo/logo.svg',
|
||||
size: 40,
|
||||
avatar: 'https://i.pravatar.cc/96?u=avatar-default',
|
||||
size: 'xl',
|
||||
},
|
||||
} satisfies Meta<typeof Avatar>
|
||||
|
||||
@ -40,23 +40,20 @@ export const WithFallback: Story = {
|
||||
source: {
|
||||
language: 'tsx',
|
||||
code: `
|
||||
<Avatar name="Fallback" avatar={null} size={40} />
|
||||
<Avatar name="Fallback" avatar={null} size="xl" />
|
||||
`.trim(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomSizes: Story = {
|
||||
export const AllSizes: Story = {
|
||||
render: args => (
|
||||
<div className="flex items-end gap-4">
|
||||
{[24, 32, 48, 64].map(size => (
|
||||
{(['xxs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'] as const).map(size => (
|
||||
<div key={size} className="flex flex-col items-center gap-2">
|
||||
<Avatar {...args} size={size} avatar="https://i.pravatar.cc/96?u=size-test" />
|
||||
<span className="text-xs text-text-tertiary">
|
||||
{size}
|
||||
px
|
||||
</span>
|
||||
<span className="text-xs text-text-tertiary">{size}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -66,7 +63,7 @@ export const CustomSizes: Story = {
|
||||
source: {
|
||||
language: 'tsx',
|
||||
code: `
|
||||
{[24, 32, 48, 64].map(size => (
|
||||
{(['xxs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'] as const).map(size => (
|
||||
<Avatar key={size} name="Size Test" size={size} avatar="https://i.pravatar.cc/96?u=size-test" />
|
||||
))}
|
||||
`.trim(),
|
||||
@ -74,3 +71,16 @@ export const CustomSizes: Story = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const AllFallbackSizes: Story = {
|
||||
render: args => (
|
||||
<div className="flex items-end gap-4">
|
||||
{(['xxs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'] as const).map(size => (
|
||||
<div key={size} className="flex flex-col items-center gap-2">
|
||||
<Avatar {...args} size={size} avatar={null} name="Alex" />
|
||||
<span className="text-xs text-text-tertiary">{size}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
@ -1,64 +1,52 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { ImageLoadingStatus } from '@base-ui/react/avatar'
|
||||
import { Avatar as BaseAvatar } from '@base-ui/react/avatar'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const SIZES = {
|
||||
'xxs': { root: 'size-4', text: 'text-[7px]' },
|
||||
'xs': { root: 'size-5', text: 'text-[8px]' },
|
||||
'sm': { root: 'size-6', text: 'text-[10px]' },
|
||||
'md': { root: 'size-8', text: 'text-xs' },
|
||||
'lg': { root: 'size-9', text: 'text-sm' },
|
||||
'xl': { root: 'size-10', text: 'text-base' },
|
||||
'2xl': { root: 'size-12', text: 'text-xl' },
|
||||
'3xl': { root: 'size-16', text: 'text-2xl' },
|
||||
} as const
|
||||
|
||||
export type AvatarSize = keyof typeof SIZES
|
||||
|
||||
export type AvatarProps = {
|
||||
name: string
|
||||
avatar: string | null
|
||||
size?: number
|
||||
size?: AvatarSize
|
||||
className?: string
|
||||
textClassName?: string
|
||||
onError?: (x: boolean) => void
|
||||
onLoadingStatusChange?: (status: ImageLoadingStatus) => void
|
||||
}
|
||||
const Avatar = ({
|
||||
|
||||
const BASE_CLASS = 'relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-primary-600'
|
||||
|
||||
export const Avatar = ({
|
||||
name,
|
||||
avatar,
|
||||
size = 30,
|
||||
size = 'md',
|
||||
className,
|
||||
textClassName,
|
||||
onError,
|
||||
onLoadingStatusChange,
|
||||
}: AvatarProps) => {
|
||||
const avatarClassName = 'shrink-0 flex items-center rounded-full bg-primary-600'
|
||||
const style = { width: `${size}px`, height: `${size}px`, fontSize: `${size}px`, lineHeight: `${size}px` }
|
||||
const [imgError, setImgError] = useState(false)
|
||||
|
||||
const handleError = () => {
|
||||
setImgError(true)
|
||||
onError?.(true)
|
||||
}
|
||||
|
||||
// after uploaded, api would first return error imgs url: '.../files//file-preview/...'. Then return the right url, Which caused not show the avatar
|
||||
useEffect(() => {
|
||||
if (avatar && imgError)
|
||||
setImgError(false)
|
||||
}, [avatar])
|
||||
|
||||
if (avatar && !imgError) {
|
||||
return (
|
||||
<img
|
||||
className={cn(avatarClassName, className)}
|
||||
style={style}
|
||||
alt={name}
|
||||
src={avatar}
|
||||
onError={handleError}
|
||||
onLoad={() => onError?.(false)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const sizeConfig = SIZES[size]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(avatarClassName, className)}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className={cn(textClassName, 'scale-[0.4] text-center text-white')}
|
||||
style={style}
|
||||
>
|
||||
{name && name[0].toLocaleUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<BaseAvatar.Root className={cn(BASE_CLASS, sizeConfig.root, className)}>
|
||||
{avatar && (
|
||||
<BaseAvatar.Image
|
||||
src={avatar}
|
||||
alt={name}
|
||||
className="absolute inset-0 size-full object-cover"
|
||||
onLoadingStatusChange={onLoadingStatusChange}
|
||||
/>
|
||||
)}
|
||||
<BaseAvatar.Fallback className={cn('font-medium text-white', sizeConfig.text)}>
|
||||
{name?.[0]?.toLocaleUpperCase()}
|
||||
</BaseAvatar.Fallback>
|
||||
</BaseAvatar.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default Avatar
|
||||
|
||||
@ -23,7 +23,7 @@ import { submitHumanInputForm as submitHumanInputFormService } from '@/service/w
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { formatBooleanInputs } from '@/utils/model-config'
|
||||
import Avatar from '../../avatar'
|
||||
import { Avatar } from '../../avatar'
|
||||
import Chat from '../chat'
|
||||
import { useChat } from '../chat/hooks'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
|
||||
@ -351,7 +351,7 @@ const ChatWrapper = () => {
|
||||
<Avatar
|
||||
avatar={initUserVariables.avatar_url}
|
||||
name={initUserVariables.name || 'user'}
|
||||
size={40}
|
||||
size="xl"
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
import { submitHumanInputForm as submitHumanInputFormService } from '@/service/workflow'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Avatar from '../../avatar'
|
||||
import { Avatar } from '../../avatar'
|
||||
import Chat from '../chat'
|
||||
import { useChat } from '../chat/hooks'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
|
||||
@ -337,7 +337,7 @@ const ChatWrapper = () => {
|
||||
<Avatar
|
||||
avatar={initUserVariables.avatar_url}
|
||||
name={initUserVariables.name || 'user'}
|
||||
size={40}
|
||||
size="xl"
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
|
||||
@ -4,7 +4,7 @@ import { useDebounceFn } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Input from '@/app/components/base/input'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@ -106,7 +106,7 @@ const PermissionSelector = ({
|
||||
isOnlyMe && (
|
||||
<>
|
||||
<div className="flex size-6 shrink-0 items-center justify-center">
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={20} />
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="xs" />
|
||||
</div>
|
||||
<div className="system-sm-regular grow p-1 text-components-input-text-filled">
|
||||
{t('form.permissionsOnlyMe', { ns: 'datasetSettings' })}
|
||||
@ -135,7 +135,7 @@ const PermissionSelector = ({
|
||||
<Avatar
|
||||
avatar={selectedMembers[0].avatar_url}
|
||||
name={selectedMembers[0].name}
|
||||
size={20}
|
||||
size="xs"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -146,13 +146,13 @@ const PermissionSelector = ({
|
||||
avatar={selectedMembers[0].avatar_url}
|
||||
name={selectedMembers[0].name}
|
||||
className="absolute left-0 top-0 z-0"
|
||||
size={16}
|
||||
size="xxs"
|
||||
/>
|
||||
<Avatar
|
||||
avatar={selectedMembers[1].avatar_url}
|
||||
name={selectedMembers[1].name}
|
||||
className="absolute bottom-0 right-0 z-10"
|
||||
size={16}
|
||||
size="xxs"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@ -182,7 +182,7 @@ const PermissionSelector = ({
|
||||
{/* Only me */}
|
||||
<Item
|
||||
leftIcon={
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className="shrink-0" size={24} />
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className="shrink-0" size="sm" />
|
||||
}
|
||||
text={t('form.permissionsOnlyMe', { ns: 'datasetSettings' })}
|
||||
onClick={onSelectOnlyMe}
|
||||
@ -226,7 +226,7 @@ const PermissionSelector = ({
|
||||
{showMe && (
|
||||
<MemberItem
|
||||
leftIcon={
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className="shrink-0" size={24} />
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className="shrink-0" size="sm" />
|
||||
}
|
||||
name={userProfile.name}
|
||||
email={userProfile.email}
|
||||
@ -237,7 +237,7 @@ const PermissionSelector = ({
|
||||
{filteredMemberList.map(member => (
|
||||
<MemberItem
|
||||
leftIcon={
|
||||
<Avatar avatar={member.avatar_url} name={member.name} className="shrink-0" size={24} />
|
||||
<Avatar avatar={member.avatar_url} name={member.name} className="shrink-0" size="sm" />
|
||||
}
|
||||
name={member.name}
|
||||
email={member.email}
|
||||
|
||||
@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/app/components/base/ui/dropdown-menu'
|
||||
@ -140,7 +140,7 @@ export default function AppSelector() {
|
||||
aria-label={t('account.account', { ns: 'common' })}
|
||||
className={cn('inline-flex items-center rounded-[20px] p-0.5 hover:bg-background-default-dodge', isAccountMenuOpen && 'bg-background-default-dodge')}
|
||||
>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
sideOffset={6}
|
||||
@ -160,7 +160,7 @@ export default function AppSelector() {
|
||||
</div>
|
||||
<div className="break-all text-text-tertiary system-xs-regular">{userProfile.email}</div>
|
||||
</div>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
|
||||
</div>
|
||||
<AccountMenuRouteItem
|
||||
href="/account"
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { InvitationResult } from '@/models/common'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { NUM_INFINITE } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
@ -120,7 +120,7 @@ const MembersPage = () => {
|
||||
accounts.map(account => (
|
||||
<div key={account.id} className="flex border-b border-divider-subtle">
|
||||
<div className="flex grow items-center px-3 py-2">
|
||||
<Avatar avatar={account.avatar_url} size={24} className="mr-2" name={account.name} />
|
||||
<Avatar avatar={account.avatar_url} size="sm" className="mr-2" name={account.name} />
|
||||
<div className="">
|
||||
<div className="text-text-secondary system-sm-medium">
|
||||
{account.name}
|
||||
|
||||
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
@ -69,7 +69,7 @@ const MemberSelector: FC<Props> = ({
|
||||
)}
|
||||
{currentValue && (
|
||||
<>
|
||||
<Avatar avatar={currentValue.avatar_url} size={24} name={currentValue.name} />
|
||||
<Avatar avatar={currentValue.avatar_url} size="sm" name={currentValue.name} />
|
||||
<div className="grow truncate text-text-secondary system-sm-medium">{currentValue.name}</div>
|
||||
<div className="text-text-quaternary system-xs-regular">{currentValue.email}</div>
|
||||
</>
|
||||
@ -98,7 +98,7 @@ const MemberSelector: FC<Props> = ({
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Avatar avatar={account.avatar_url} size={24} name={account.name} />
|
||||
<Avatar avatar={account.avatar_url} size="sm" name={account.name} />
|
||||
<div className="grow truncate text-text-secondary system-sm-medium">{account.name}</div>
|
||||
<div className="text-text-quaternary system-xs-regular">{account.email}</div>
|
||||
</div>
|
||||
|
||||
@ -3,7 +3,7 @@ import type { Member } from '@/models/common'
|
||||
import { RiCloseCircleFill, RiErrorWarningFill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
@ -34,7 +34,7 @@ const EmailItem = ({
|
||||
{isError && (
|
||||
<RiErrorWarningFill className="h-4 w-4 text-text-destructive" />
|
||||
)}
|
||||
{!isError && <Avatar avatar={data.avatar_url} size={16} name={data.name || data.email} />}
|
||||
{!isError && <Avatar avatar={data.avatar_url} size="xxs" name={data.name || data.email} />}
|
||||
<div title={data.email} className="system-xs-regular max-w-[500px] truncate text-text-primary">
|
||||
{email === data.email ? data.name : data.email}
|
||||
{email === data.email && <span className="system-xs-regular text-text-tertiary">{t('members.you', { ns: 'common' })}</span>}
|
||||
|
||||
@ -4,7 +4,7 @@ import type { Recipient } from '@/app/components/workflow/nodes/human-input/type
|
||||
import type { Member } from '@/models/common'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@ -65,7 +65,7 @@ const MemberList: FC<Props> = ({ searchValue, list, value, onSearchChange, onSel
|
||||
onSelect(account.id)
|
||||
}}
|
||||
>
|
||||
<Avatar className={cn(value.some(item => item.user_id === account.id) && 'opacity-50')} avatar={account.avatar_url} size={24} name={account.name} />
|
||||
<Avatar className={cn(value.some(item => item.user_id === account.id) && 'opacity-50')} avatar={account.avatar_url} size="sm" name={account.name} />
|
||||
<div className={cn('grow', value.some(item => item.user_id === account.id) && 'opacity-50')}>
|
||||
<div className="system-sm-medium text-text-secondary">
|
||||
{account.name}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Triangle } from '@/app/components/base/icons/src/public/education'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -34,7 +34,7 @@ const UserInfo = () => {
|
||||
className="mr-4"
|
||||
avatar={userProfile.avatar_url}
|
||||
name={userProfile.name}
|
||||
size={48}
|
||||
size="2xl"
|
||||
/>
|
||||
<div className="pt-1.5">
|
||||
<div className="system-md-semibold text-text-primary">
|
||||
|
||||
@ -1436,11 +1436,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/avatar/index.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/block-input/index.stories.tsx": {
|
||||
"no-console": {
|
||||
"count": 2
|
||||
|
||||
@ -151,7 +151,7 @@ export default antfu(
|
||||
},
|
||||
{
|
||||
name: 'dify/base-ui-primitives',
|
||||
files: ['app/components/base/ui/**/*.tsx'],
|
||||
files: ['app/components/base/ui/**/*.tsx', 'app/components/base/avatar/**/*.tsx'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': 'off',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user