feat: knowledge pipeline (#25360)
Signed-off-by: -LAN- <laipz8200@outlook.com> Co-authored-by: twwu <twwu@dify.ai> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: jyong <718720800@qq.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com> Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: quicksand <quicksandzn@gmail.com> Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: Yongtao Huang <yongtaoh2022@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: Hanqing Zhao <sherry9277@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Harry <xh001x@hotmail.com>
This commit is contained in:
159
web/app/components/base/app-icon/index.spec.tsx
Normal file
159
web/app/components/base/app-icon/index.spec.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import AppIcon from './index'
|
||||
|
||||
// Mock emoji-mart initialization
|
||||
jest.mock('emoji-mart', () => ({
|
||||
init: jest.fn(),
|
||||
}))
|
||||
|
||||
// Mock emoji data
|
||||
jest.mock('@emoji-mart/data', () => ({}))
|
||||
|
||||
// Mock the ahooks useHover hook
|
||||
jest.mock('ahooks', () => ({
|
||||
useHover: jest.fn(() => false),
|
||||
}))
|
||||
|
||||
describe('AppIcon', () => {
|
||||
beforeEach(() => {
|
||||
// Mock custom element
|
||||
if (!customElements.get('em-emoji')) {
|
||||
customElements.define('em-emoji', class extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
// Mock basic functionality
|
||||
connectedCallback() {
|
||||
this.innerHTML = '🤖'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Reset mocks
|
||||
require('ahooks').useHover.mockReset().mockReturnValue(false)
|
||||
})
|
||||
|
||||
it('renders default emoji when no icon or image is provided', () => {
|
||||
render(<AppIcon />)
|
||||
const emojiElement = document.querySelector('em-emoji')
|
||||
expect(emojiElement).toBeInTheDocument()
|
||||
expect(emojiElement?.getAttribute('id')).toBe('🤖')
|
||||
})
|
||||
|
||||
it('renders with custom emoji when icon is provided', () => {
|
||||
render(<AppIcon icon='smile' />)
|
||||
const emojiElement = document.querySelector('em-emoji')
|
||||
expect(emojiElement).toBeInTheDocument()
|
||||
expect(emojiElement?.getAttribute('id')).toBe('smile')
|
||||
})
|
||||
|
||||
it('renders image when iconType is image and imageUrl is provided', () => {
|
||||
render(<AppIcon iconType='image' imageUrl='test-image.jpg' />)
|
||||
const imgElement = screen.getByAltText('app icon')
|
||||
expect(imgElement).toBeInTheDocument()
|
||||
expect(imgElement).toHaveAttribute('src', 'test-image.jpg')
|
||||
})
|
||||
|
||||
it('renders innerIcon when provided', () => {
|
||||
render(<AppIcon innerIcon={<div data-testid='inner-icon'>Custom Icon</div>} />)
|
||||
const innerIcon = screen.getByTestId('inner-icon')
|
||||
expect(innerIcon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies size classes correctly', () => {
|
||||
const { container: xsContainer } = render(<AppIcon size='xs' />)
|
||||
expect(xsContainer.firstChild).toHaveClass('w-4 h-4 rounded-[4px]')
|
||||
|
||||
const { container: tinyContainer } = render(<AppIcon size='tiny' />)
|
||||
expect(tinyContainer.firstChild).toHaveClass('w-6 h-6 rounded-md')
|
||||
|
||||
const { container: smallContainer } = render(<AppIcon size='small' />)
|
||||
expect(smallContainer.firstChild).toHaveClass('w-8 h-8 rounded-lg')
|
||||
|
||||
const { container: mediumContainer } = render(<AppIcon size='medium' />)
|
||||
expect(mediumContainer.firstChild).toHaveClass('w-9 h-9 rounded-[10px]')
|
||||
|
||||
const { container: largeContainer } = render(<AppIcon size='large' />)
|
||||
expect(largeContainer.firstChild).toHaveClass('w-10 h-10 rounded-[10px]')
|
||||
|
||||
const { container: xlContainer } = render(<AppIcon size='xl' />)
|
||||
expect(xlContainer.firstChild).toHaveClass('w-12 h-12 rounded-xl')
|
||||
|
||||
const { container: xxlContainer } = render(<AppIcon size='xxl' />)
|
||||
expect(xxlContainer.firstChild).toHaveClass('w-14 h-14 rounded-2xl')
|
||||
})
|
||||
|
||||
it('applies rounded class when rounded=true', () => {
|
||||
const { container } = render(<AppIcon rounded />)
|
||||
expect(container.firstChild).toHaveClass('rounded-full')
|
||||
})
|
||||
|
||||
it('applies custom background color', () => {
|
||||
const { container } = render(<AppIcon background='#FF5500' />)
|
||||
expect(container.firstChild).toHaveStyle('background: #FF5500')
|
||||
})
|
||||
|
||||
it('uses default background color when no background is provided for non-image icons', () => {
|
||||
const { container } = render(<AppIcon />)
|
||||
expect(container.firstChild).toHaveStyle('background: #FFEAD5')
|
||||
})
|
||||
|
||||
it('does not apply background style for image icons', () => {
|
||||
const { container } = render(<AppIcon iconType='image' imageUrl='test.jpg' background='#FF5500' />)
|
||||
// Should not have the background style from the prop
|
||||
expect(container.firstChild).not.toHaveStyle('background: #FF5500')
|
||||
})
|
||||
|
||||
it('calls onClick handler when clicked', () => {
|
||||
const handleClick = jest.fn()
|
||||
const { container } = render(<AppIcon onClick={handleClick} />)
|
||||
fireEvent.click(container.firstChild!)
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<AppIcon className='custom-class' />)
|
||||
expect(container.firstChild).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('does not display edit icon when showEditIcon=false', () => {
|
||||
render(<AppIcon />)
|
||||
const editIcon = screen.queryByRole('svg')
|
||||
expect(editIcon).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays edit icon when showEditIcon=true and hovering', () => {
|
||||
// Mock the useHover hook to return true for this test
|
||||
require('ahooks').useHover.mockReturnValue(true)
|
||||
|
||||
render(<AppIcon showEditIcon />)
|
||||
const editIcon = document.querySelector('svg')
|
||||
expect(editIcon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not display edit icon when showEditIcon=true but not hovering', () => {
|
||||
// useHover returns false by default from our mock setup
|
||||
render(<AppIcon showEditIcon />)
|
||||
const editIcon = document.querySelector('svg')
|
||||
expect(editIcon).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles conditional isValidImageIcon check correctly', () => {
|
||||
// Case 1: Valid image icon
|
||||
const { rerender } = render(
|
||||
<AppIcon iconType='image' imageUrl='test.jpg' />,
|
||||
)
|
||||
expect(screen.getByAltText('app icon')).toBeInTheDocument()
|
||||
|
||||
// Case 2: Invalid - missing image URL
|
||||
rerender(<AppIcon iconType='image' imageUrl={null} />)
|
||||
expect(screen.queryByAltText('app icon')).not.toBeInTheDocument()
|
||||
|
||||
// Case 3: Invalid - wrong icon type
|
||||
rerender(<AppIcon iconType='emoji' imageUrl='test.jpg' />)
|
||||
expect(screen.queryByAltText('app icon')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { type FC, useRef } from 'react'
|
||||
import { init } from 'emoji-mart'
|
||||
import data from '@emoji-mart/data'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { useHover } from 'ahooks'
|
||||
import { RiEditLine } from '@remixicon/react'
|
||||
|
||||
init({ data })
|
||||
|
||||
@@ -20,20 +21,21 @@ export type AppIconProps = {
|
||||
className?: string
|
||||
innerIcon?: React.ReactNode
|
||||
coverElement?: React.ReactNode
|
||||
showEditIcon?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
const appIconVariants = cva(
|
||||
'flex items-center justify-center relative text-lg rounded-lg grow-0 shrink-0 overflow-hidden leading-none',
|
||||
'flex items-center justify-center relative grow-0 shrink-0 overflow-hidden leading-none border-[0.5px] border-divider-regular',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: 'w-4 h-4 text-xs',
|
||||
tiny: 'w-6 h-6 text-base',
|
||||
small: 'w-8 h-8 text-xl',
|
||||
medium: 'w-9 h-9 text-[22px]',
|
||||
large: 'w-10 h-10 text-[24px]',
|
||||
xl: 'w-12 h-12 text-[28px]',
|
||||
xxl: 'w-14 h-14 text-[32px]',
|
||||
xs: 'w-4 h-4 text-xs rounded-[4px]',
|
||||
tiny: 'w-6 h-6 text-base rounded-md',
|
||||
small: 'w-8 h-8 text-xl rounded-lg',
|
||||
medium: 'w-9 h-9 text-[22px] rounded-[10px]',
|
||||
large: 'w-10 h-10 text-[24px] rounded-[10px]',
|
||||
xl: 'w-12 h-12 text-[28px] rounded-xl',
|
||||
xxl: 'w-14 h-14 text-[32px] rounded-2xl',
|
||||
},
|
||||
rounded: {
|
||||
true: 'rounded-full',
|
||||
@@ -44,6 +46,46 @@ const appIconVariants = cva(
|
||||
rounded: false,
|
||||
},
|
||||
})
|
||||
const EditIconWrapperVariants = cva(
|
||||
'absolute left-0 top-0 z-10 flex items-center justify-center bg-background-overlay-alt',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: 'w-4 h-4 rounded-[4px]',
|
||||
tiny: 'w-6 h-6 rounded-md',
|
||||
small: 'w-8 h-8 rounded-lg',
|
||||
medium: 'w-9 h-9 rounded-[10px]',
|
||||
large: 'w-10 h-10 rounded-[10px]',
|
||||
xl: 'w-12 h-12 rounded-xl',
|
||||
xxl: 'w-14 h-14 rounded-2xl',
|
||||
},
|
||||
rounded: {
|
||||
true: 'rounded-full',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'medium',
|
||||
rounded: false,
|
||||
},
|
||||
})
|
||||
const EditIconVariants = cva(
|
||||
'text-text-primary-on-surface',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: 'size-3',
|
||||
tiny: 'size-3.5',
|
||||
small: 'size-5',
|
||||
medium: 'size-[22px]',
|
||||
large: 'size-6',
|
||||
xl: 'size-7',
|
||||
xxl: 'size-8',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'medium',
|
||||
},
|
||||
})
|
||||
const AppIcon: FC<AppIconProps> = ({
|
||||
size = 'medium',
|
||||
rounded = false,
|
||||
@@ -55,21 +97,35 @@ const AppIcon: FC<AppIconProps> = ({
|
||||
innerIcon,
|
||||
coverElement,
|
||||
onClick,
|
||||
showEditIcon = false,
|
||||
}) => {
|
||||
const isValidImageIcon = iconType === 'image' && imageUrl
|
||||
const Icon = (icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />
|
||||
const wrapperRef = useRef<HTMLSpanElement>(null)
|
||||
const isHovering = useHover(wrapperRef)
|
||||
|
||||
return <span
|
||||
className={classNames(appIconVariants({ size, rounded }), className)}
|
||||
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isValidImageIcon
|
||||
|
||||
? <img src={imageUrl} className="h-full w-full" alt="app icon" />
|
||||
: (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />))
|
||||
}
|
||||
{coverElement}
|
||||
</span>
|
||||
return (
|
||||
<span
|
||||
ref={wrapperRef}
|
||||
className={classNames(appIconVariants({ size, rounded }), className)}
|
||||
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{
|
||||
isValidImageIcon
|
||||
? <img src={imageUrl} className='h-full w-full' alt='app icon' />
|
||||
: (innerIcon || Icon)
|
||||
}
|
||||
{
|
||||
showEditIcon && isHovering && (
|
||||
<div className={EditIconWrapperVariants({ size, rounded })}>
|
||||
<RiEditLine className={EditIconVariants({ size })} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{coverElement}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AppIcon)
|
||||
|
||||
Reference in New Issue
Block a user