mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
refactor(web): restore avatar composition API
This commit is contained in:
parent
d32bc1a364
commit
a4a7d5c2fa
@ -1,5 +1,5 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { Avatar } from '..'
|
import { Avatar, AvatarFallback, AvatarImage, AvatarRoot } from '..'
|
||||||
|
|
||||||
describe('Avatar', () => {
|
describe('Avatar', () => {
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
@ -60,6 +60,23 @@ describe('Avatar', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Primitives', () => {
|
||||||
|
it('should support composed avatar usage through exported primitives', () => {
|
||||||
|
render(
|
||||||
|
<AvatarRoot size="sm" data-testid="avatar-root">
|
||||||
|
<AvatarImage src="https://example.com/avatar.jpg" alt="Jane Doe" />
|
||||||
|
<AvatarFallback size="sm" style={{ backgroundColor: 'rgb(1, 2, 3)' }}>
|
||||||
|
J
|
||||||
|
</AvatarFallback>
|
||||||
|
</AvatarRoot>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('avatar-root')).toHaveClass('size-6')
|
||||||
|
expect(screen.getByText('J')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('J')).toHaveStyle({ backgroundColor: 'rgb(1, 2, 3)' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Edge Cases', () => {
|
describe('Edge Cases', () => {
|
||||||
it('should handle empty string name gracefully', () => {
|
it('should handle empty string name gracefully', () => {
|
||||||
const { container } = render(<Avatar name="" avatar={null} />)
|
const { container } = render(<Avatar name="" avatar={null} />)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||||
import { Avatar } from '.'
|
import { Avatar, AvatarFallback, AvatarRoot } from '.'
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Base/Data Display/Avatar',
|
title: 'Base/Data Display/Avatar',
|
||||||
@ -84,3 +84,27 @@ export const AllFallbackSizes: Story = {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ComposedFallback: Story = {
|
||||||
|
render: () => (
|
||||||
|
<AvatarRoot size="xl">
|
||||||
|
<AvatarFallback size="xl" style={{ backgroundColor: '#2563eb' }}>
|
||||||
|
C
|
||||||
|
</AvatarFallback>
|
||||||
|
</AvatarRoot>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
source: {
|
||||||
|
language: 'tsx',
|
||||||
|
code: `
|
||||||
|
<AvatarRoot size="xl">
|
||||||
|
<AvatarFallback size="xl" style={{ backgroundColor: '#2563eb' }}>
|
||||||
|
C
|
||||||
|
</AvatarFallback>
|
||||||
|
</AvatarRoot>
|
||||||
|
`.trim(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@ -21,31 +21,18 @@ export type AvatarProps = {
|
|||||||
avatar: string | null
|
avatar: string | null
|
||||||
size?: AvatarSize
|
size?: AvatarSize
|
||||||
className?: string
|
className?: string
|
||||||
textClassName?: string
|
|
||||||
onError?: (hasError: boolean) => void
|
|
||||||
backgroundColor?: string
|
|
||||||
onLoadingStatusChange?: (status: ImageLoadingStatus) => void
|
onLoadingStatusChange?: (status: ImageLoadingStatus) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type AvatarRootProps = React.ComponentPropsWithRef<typeof BaseAvatar.Root> & {
|
export type AvatarRootProps = React.ComponentPropsWithRef<typeof BaseAvatar.Root> & {
|
||||||
size?: AvatarSize
|
size?: AvatarSize
|
||||||
hasAvatar?: boolean
|
|
||||||
backgroundColor?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarRoot({
|
export function AvatarRoot({
|
||||||
size = 'md',
|
size = 'md',
|
||||||
className,
|
className,
|
||||||
hasAvatar = false,
|
|
||||||
backgroundColor,
|
|
||||||
style,
|
|
||||||
...props
|
...props
|
||||||
}: AvatarRootProps) {
|
}: AvatarRootProps) {
|
||||||
const resolvedStyle: React.CSSProperties = {
|
|
||||||
...(backgroundColor && !hasAvatar ? { backgroundColor } : {}),
|
|
||||||
...style,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar.Root
|
<BaseAvatar.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -53,45 +40,35 @@ function AvatarRoot({
|
|||||||
avatarSizeClasses[size].root,
|
avatarSizeClasses[size].root,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
style={resolvedStyle}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AvatarFallbackProps = React.ComponentPropsWithRef<typeof BaseAvatar.Fallback> & {
|
export type AvatarFallbackProps = React.ComponentPropsWithRef<typeof BaseAvatar.Fallback> & {
|
||||||
size?: AvatarSize
|
size?: AvatarSize
|
||||||
textClassName?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarFallback({
|
export function AvatarFallback({
|
||||||
size = 'md',
|
size = 'md',
|
||||||
textClassName,
|
|
||||||
className,
|
className,
|
||||||
style,
|
|
||||||
...props
|
...props
|
||||||
}: AvatarFallbackProps) {
|
}: AvatarFallbackProps) {
|
||||||
const resolvedStyle: React.CSSProperties = {
|
|
||||||
...style,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar.Fallback
|
<BaseAvatar.Fallback
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex size-full items-center justify-center font-medium text-white',
|
'flex size-full items-center justify-center font-medium text-white',
|
||||||
avatarSizeClasses[size].text,
|
avatarSizeClasses[size].text,
|
||||||
textClassName,
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
style={resolvedStyle}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AvatarImageProps = React.ComponentPropsWithRef<typeof BaseAvatar.Image>
|
export type AvatarImageProps = React.ComponentPropsWithRef<typeof BaseAvatar.Image>
|
||||||
|
|
||||||
function AvatarImage({
|
export function AvatarImage({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: AvatarImageProps) {
|
}: AvatarImageProps) {
|
||||||
@ -108,34 +85,18 @@ export const Avatar = ({
|
|||||||
avatar,
|
avatar,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
className,
|
className,
|
||||||
textClassName,
|
|
||||||
onError,
|
|
||||||
backgroundColor,
|
|
||||||
onLoadingStatusChange,
|
onLoadingStatusChange,
|
||||||
}: AvatarProps) => {
|
}: AvatarProps) => {
|
||||||
const handleLoadingStatusChange = (status: ImageLoadingStatus) => {
|
|
||||||
onLoadingStatusChange?.(status)
|
|
||||||
if (status === 'error')
|
|
||||||
onError?.(true)
|
|
||||||
if (status === 'loaded')
|
|
||||||
onError?.(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AvatarRoot
|
<AvatarRoot size={size} className={className}>
|
||||||
size={size}
|
|
||||||
className={className}
|
|
||||||
backgroundColor={backgroundColor}
|
|
||||||
hasAvatar={Boolean(avatar)}
|
|
||||||
>
|
|
||||||
{avatar && (
|
{avatar && (
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
src={avatar}
|
src={avatar}
|
||||||
alt={name}
|
alt={name}
|
||||||
onLoadingStatusChange={handleLoadingStatusChange}
|
onLoadingStatusChange={onLoadingStatusChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AvatarFallback size={size} textClassName={textClassName}>
|
<AvatarFallback size={size}>
|
||||||
{name?.[0]?.toLocaleUpperCase()}
|
{name?.[0]?.toLocaleUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</AvatarRoot>
|
</AvatarRoot>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { AvatarSize } from '@/app/components/base/ui/avatar'
|
import type { AvatarSize } from '@/app/components/base/ui/avatar'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { Avatar } from '@/app/components/base/ui/avatar'
|
import { AvatarFallback, AvatarImage, AvatarRoot } from '@/app/components/base/ui/avatar'
|
||||||
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
|
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
|
||||||
@ -59,13 +59,20 @@ export const UserAvatarList: FC<UserAvatarListProps> = memo(({
|
|||||||
className="relative"
|
className="relative"
|
||||||
style={{ zIndex: visibleUsers.length - index }}
|
style={{ zIndex: visibleUsers.length - index }}
|
||||||
>
|
>
|
||||||
<Avatar
|
<AvatarRoot size={size} className="ring-2 ring-components-panel-bg">
|
||||||
name={user.name}
|
{user.avatar_url && (
|
||||||
avatar={user.avatar_url || null}
|
<AvatarImage
|
||||||
size={size}
|
src={user.avatar_url}
|
||||||
className="ring-2 ring-components-panel-bg"
|
alt={user.name}
|
||||||
backgroundColor={userColor}
|
/>
|
||||||
/>
|
)}
|
||||||
|
<AvatarFallback
|
||||||
|
size={size}
|
||||||
|
style={userColor ? { backgroundColor: userColor } : undefined}
|
||||||
|
>
|
||||||
|
{user.name?.[0]?.toLocaleUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</AvatarRoot>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useReactFlow, useViewport } from 'reactflow'
|
import { useReactFlow, useViewport } from 'reactflow'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import InlineDeleteConfirm from '@/app/components/base/inline-delete-confirm'
|
import InlineDeleteConfirm from '@/app/components/base/inline-delete-confirm'
|
||||||
import { Avatar } from '@/app/components/base/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage, AvatarRoot } from '@/app/components/base/ui/avatar'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -124,13 +124,20 @@ const ThreadMessage: FC<{
|
|||||||
return (
|
return (
|
||||||
<div className={cn('flex gap-3 pt-1', className)}>
|
<div className={cn('flex gap-3 pt-1', className)}>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<Avatar
|
<AvatarRoot size="sm" className={cn('h-8 w-8 rounded-full')}>
|
||||||
name={authorName}
|
{avatarUrl && (
|
||||||
avatar={avatarUrl || null}
|
<AvatarImage
|
||||||
size="sm"
|
src={avatarUrl}
|
||||||
className={cn('h-8 w-8 rounded-full')}
|
alt={authorName}
|
||||||
backgroundColor={userColor}
|
/>
|
||||||
/>
|
)}
|
||||||
|
<AvatarFallback
|
||||||
|
size="sm"
|
||||||
|
style={userColor ? { backgroundColor: userColor } : undefined}
|
||||||
|
>
|
||||||
|
{authorName?.[0]?.toLocaleUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</AvatarRoot>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1 pb-4 text-text-primary last:pb-0">
|
<div className="min-w-0 flex-1 pb-4 text-text-primary last:pb-0">
|
||||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useReactFlow } from 'reactflow'
|
import { useReactFlow } from 'reactflow'
|
||||||
import { Avatar } from '@/app/components/base/ui/avatar'
|
import { AvatarFallback, AvatarImage, AvatarRoot } from '@/app/components/base/ui/avatar'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@ -123,6 +123,8 @@ const OnlineUsers = () => {
|
|||||||
{visibleUsers.map((user, index) => {
|
{visibleUsers.map((user, index) => {
|
||||||
const isCurrentUser = user.user_id === currentUserId
|
const isCurrentUser = user.user_id === currentUserId
|
||||||
const userColor = isCurrentUser ? undefined : getUserColor(user.user_id)
|
const userColor = isCurrentUser ? undefined : getUserColor(user.user_id)
|
||||||
|
const avatarUrl = getAvatarUrl(user)
|
||||||
|
const displayName = user.username || fallbackUsername
|
||||||
return (
|
return (
|
||||||
<Tooltip key={`${user.sid}-${index}`}>
|
<Tooltip key={`${user.sid}-${index}`}>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
@ -135,13 +137,20 @@ const OnlineUsers = () => {
|
|||||||
style={{ zIndex: visibleUsers.length - index }}
|
style={{ zIndex: visibleUsers.length - index }}
|
||||||
onClick={() => !isCurrentUser && jumpToUserCursor(user.user_id)}
|
onClick={() => !isCurrentUser && jumpToUserCursor(user.user_id)}
|
||||||
>
|
>
|
||||||
<Avatar
|
<AvatarRoot size="sm" className="ring-1 ring-components-panel-bg">
|
||||||
name={user.username || fallbackUsername}
|
{avatarUrl && (
|
||||||
avatar={getAvatarUrl(user)}
|
<AvatarImage
|
||||||
size="sm"
|
src={avatarUrl}
|
||||||
className="ring-1 ring-components-panel-bg"
|
alt={displayName}
|
||||||
backgroundColor={userColor}
|
/>
|
||||||
/>
|
)}
|
||||||
|
<AvatarFallback
|
||||||
|
size="sm"
|
||||||
|
style={userColor ? { backgroundColor: userColor } : undefined}
|
||||||
|
>
|
||||||
|
{displayName?.[0]?.toLocaleUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</AvatarRoot>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
@ -190,6 +199,8 @@ const OnlineUsers = () => {
|
|||||||
{onlineUsers.map((user) => {
|
{onlineUsers.map((user) => {
|
||||||
const isCurrentUser = user.user_id === currentUserId
|
const isCurrentUser = user.user_id === currentUserId
|
||||||
const userColor = isCurrentUser ? undefined : getUserColor(user.user_id)
|
const userColor = isCurrentUser ? undefined : getUserColor(user.user_id)
|
||||||
|
const avatarUrl = getAvatarUrl(user)
|
||||||
|
const displayName = user.username || fallbackUsername
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={user.sid}
|
key={user.sid}
|
||||||
@ -205,12 +216,20 @@ const OnlineUsers = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar
|
<AvatarRoot size="sm">
|
||||||
name={user.username || fallbackUsername}
|
{avatarUrl && (
|
||||||
avatar={getAvatarUrl(user)}
|
<AvatarImage
|
||||||
size="sm"
|
src={avatarUrl}
|
||||||
backgroundColor={userColor}
|
alt={displayName}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
<AvatarFallback
|
||||||
|
size="sm"
|
||||||
|
style={userColor ? { backgroundColor: userColor } : undefined}
|
||||||
|
>
|
||||||
|
{displayName?.[0]?.toLocaleUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</AvatarRoot>
|
||||||
</div>
|
</div>
|
||||||
{renderDisplayName(
|
{renderDisplayName(
|
||||||
user,
|
user,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user