fix: app icon could not only change background (#35537)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
非法操作 2026-04-24 15:59:37 +08:00 committed by GitHub
parent 5e336c47fd
commit 9bd5c2f8ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 56 additions and 30 deletions

View File

@ -1080,11 +1080,6 @@
"count": 1
}
},
"web/app/components/base/emoji-picker/Inner.tsx": {
"react/set-state-in-effect": {
"count": 1
}
},
"web/app/components/base/emoji-picker/index.tsx": {
"no-restricted-imports": {
"count": 1

View File

@ -1,3 +1,4 @@
import type { ComponentProps } from 'react'
import type { Area } from 'react-easy-crop'
import type { ImageFile } from '@/types/app'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
@ -122,11 +123,11 @@ describe('AppIconPicker', () => {
})
}
const renderPicker = () => {
const renderPicker = (props: Partial<ComponentProps<typeof AppIconPicker>> = {}) => {
const onSelect = vi.fn()
const onClose = vi.fn()
const { container } = render(<AppIconPicker onSelect={onSelect} onClose={onClose} />)
const { container } = render(<AppIconPicker onSelect={onSelect} onClose={onClose} {...props} />)
return { onSelect, onClose, container }
}
@ -220,6 +221,20 @@ describe('AppIconPicker', () => {
expect(onSelect).not.toHaveBeenCalled()
})
it('should submit the initial emoji when provided', async () => {
const { onSelect } = renderPicker({ initialEmoji: { icon: 'rabbit', background: '#E4FBCC' } })
await userEvent.click(screen.getByText(/ok/i))
await waitFor(() => {
expect(onSelect).toHaveBeenCalledWith({
type: 'emoji',
icon: 'rabbit',
background: '#E4FBCC',
})
})
})
})
describe('Image Upload', () => {

View File

@ -34,12 +34,17 @@ export type AppIconSelection = AppIconEmojiSelection | AppIconImageSelection
type AppIconPickerProps = {
onSelect?: (payload: AppIconSelection) => void
onClose?: () => void
initialEmoji?: {
icon: string
background?: string | null
}
className?: string
}
const AppIconPicker: FC<AppIconPickerProps> = ({
onSelect,
onClose,
initialEmoji,
className,
}) => {
const { t } = useTranslation()
@ -138,7 +143,14 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
</div>
)}
{activeTab === 'emoji' && <EmojiPickerInner className={cn('flex-1 overflow-hidden pt-2')} onSelect={handleSelectEmoji} />}
{activeTab === 'emoji' && (
<EmojiPickerInner
className={cn('flex-1 overflow-hidden pt-2')}
emoji={initialEmoji?.icon}
background={initialEmoji?.background ?? undefined}
onSelect={handleSelectEmoji}
/>
)}
{activeTab === 'image' && <ImageInput className={cn('flex-1 overflow-hidden')} onImageInput={handleImageInput} />}
<Divider className="m-0" />

View File

@ -45,20 +45,21 @@ type IEmojiPickerInnerProps = {
}
const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
emoji,
background,
onSelect,
className,
}) => {
const { categories } = data as EmojiMartData
const [selectedEmoji, setSelectedEmoji] = useState('')
const [selectedBackground, setSelectedBackground] = useState(backgroundColors[0])
const [showStyleColors, setShowStyleColors] = useState(false)
const [selectedEmoji, setSelectedEmoji] = useState(emoji || '')
const [selectedBackground, setSelectedBackground] = useState(background || backgroundColors[0])
const [showStyleColors, setShowStyleColors] = useState(!!emoji)
const [searchedEmojis, setSearchedEmojis] = useState<string[]>([])
const [isSearching, setIsSearching] = useState(false)
React.useEffect(() => {
if (selectedEmoji) {
setShowStyleColors(true)
/* v8 ignore next 2 - @preserve */
if (selectedBackground)
onSelect?.(selectedEmoji, selectedBackground)
@ -105,6 +106,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
className="inline-flex h-10 w-10 items-center justify-center rounded-lg"
onClick={() => {
setSelectedEmoji(emoji)
setShowStyleColors(true)
}}
>
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1" data-testid={`emoji-search-result-${emoji}`}>
@ -130,6 +132,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
className="inline-flex h-10 w-10 items-center justify-center rounded-lg"
onClick={() => {
setSelectedEmoji(emoji)
setShowStyleColors(true)
}}
>
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1" data-testid={`emoji-container-${emoji}`}>

View File

@ -45,6 +45,15 @@ describe('EmojiPickerInner', () => {
expect(screen.getByText('food'))!.toBeInTheDocument()
expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument()
})
it('initializes selected emoji and background when provided', async () => {
render(<EmojiPickerInner emoji="rabbit" background="#E4FBCC" onSelect={mockOnSelect} />)
expect(screen.getByText('Choose Style'))!.toBeInTheDocument()
await waitFor(() => {
expect(mockOnSelect).toHaveBeenCalledWith('rabbit', '#E4FBCC')
})
})
})
describe('User Interactions', () => {

View File

@ -359,7 +359,7 @@ describe('CreateAppModal', () => {
}
})
it('should reset emoji icon to initial props when picker is cancelled', async () => {
it('should allow changing only the background for the current emoji icon', async () => {
vi.useFakeTimers()
try {
const { onConfirm } = await setup({
@ -370,22 +370,14 @@ describe('CreateAppModal', () => {
fireEvent.click(getAppIconTrigger())
const categoryLabel = screen.getByText('people')
const emojiGrid = categoryLabel.nextElementSibling
const clickableEmojiWrapper = emojiGrid?.firstElementChild
if (!(clickableEmojiWrapper instanceof HTMLElement))
throw new Error('Failed to locate emoji wrapper')
fireEvent.click(clickableEmojiWrapper)
const colorOption = Array.from(document.querySelectorAll('[style^="background:"]'))
.find(element => element.getAttribute('style')?.includes('#E4FBCC'))
if (!(colorOption instanceof HTMLElement) || !(colorOption.parentElement instanceof HTMLElement))
throw new Error('Failed to locate background color option')
fireEvent.click(colorOption.parentElement)
fireEvent.click(screen.getByRole('button', { name: 'app.iconPicker.ok' }))
expect(screen.queryByRole('button', { name: 'app.iconPicker.cancel' })).not.toBeInTheDocument()
fireEvent.click(getAppIconTrigger())
fireEvent.click(screen.getByRole('button', { name: 'app.iconPicker.cancel' }))
expect(screen.queryByRole('button', { name: 'app.iconPicker.cancel' })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.create/ }))
await act(async () => {
vi.advanceTimersByTime(300)
@ -396,7 +388,7 @@ describe('CreateAppModal', () => {
expect(payload).toMatchObject({
icon_type: 'emoji',
icon: '🤖',
icon_background: '#FFEAD5',
icon_background: '#E4FBCC',
})
}
finally {

View File

@ -206,14 +206,14 @@ const CreateAppModal = ({
</Modal>
{showAppIconPicker && (
<AppIconPicker
initialEmoji={appIcon.type === 'emoji'
? { icon: appIcon.icon, background: appIcon.background }
: undefined}
onSelect={(payload) => {
setAppIcon(payload)
setShowAppIconPicker(false)
}}
onClose={() => {
setAppIcon(appIconType === 'image'
? { type: 'image' as const, url: appIconUrl, fileId: _appIcon }
: { type: 'emoji' as const, icon: _appIcon, background: appIconBackground })
setShowAppIconPicker(false)
}}
/>