mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 01:18:05 +08:00
fix(plugin-tasks): handle error actions by source and clear item after marketplace install
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import type { PluginStatus } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TaskStatus } from '@/app/components/plugins/types'
|
||||
import { PluginSource, TaskStatus } from '@/app/components/plugins/types'
|
||||
// Import mocked modules
|
||||
import { useMutationClearTaskPlugin, usePluginTaskList } from '@/service/use-plugins'
|
||||
import PluginTaskList from '../components/plugin-task-list'
|
||||
@ -30,6 +30,7 @@ vi.mock('@/context/i18n', () => ({
|
||||
const createMockPlugin = (overrides: Partial<PluginStatus> = {}): PluginStatus => ({
|
||||
plugin_unique_identifier: `plugin-${Math.random().toString(36).substr(2, 9)}`,
|
||||
plugin_id: 'test-plugin',
|
||||
source: PluginSource.marketplace,
|
||||
status: TaskStatus.running,
|
||||
message: '',
|
||||
icon: 'test-icon.png',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { PluginInfoFromMarketPlace, PluginStatus } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { PluginCategoryEnum, TaskStatus } from '@/app/components/plugins/types'
|
||||
import { PluginCategoryEnum, PluginSource, TaskStatus } from '@/app/components/plugins/types'
|
||||
import { fetchPluginInfoFromMarketPlace } from '@/service/plugins'
|
||||
|
||||
import ErrorPluginItem from '../error-plugin-item'
|
||||
@ -43,6 +43,7 @@ function createMarketplaceResponse(identifier: string, version: string) {
|
||||
const createPlugin = (overrides: Partial<PluginStatus> = {}): PluginStatus => ({
|
||||
plugin_unique_identifier: 'org/plugin:1.0.0',
|
||||
plugin_id: 'org/plugin',
|
||||
source: PluginSource.marketplace,
|
||||
status: TaskStatus.failed,
|
||||
message: '',
|
||||
icon: 'icon.png',
|
||||
@ -102,7 +103,7 @@ describe('ErrorPluginItem', () => {
|
||||
it('should show marketplace error message for marketplace plugins', () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'org/my-plugin' })}
|
||||
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/my-plugin' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -115,7 +116,7 @@ describe('ErrorPluginItem', () => {
|
||||
it('should show github error message for github plugins', () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'https://github.com/user/repo' })}
|
||||
plugin={createPlugin({ source: PluginSource.github, plugin_id: 'https://github.com/user/repo' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -128,7 +129,7 @@ describe('ErrorPluginItem', () => {
|
||||
it('should show unknown error message for unknown source plugins', () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'local-only-plugin' })}
|
||||
plugin={createPlugin({ source: PluginSource.local, plugin_id: 'local-only-plugin' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -156,7 +157,7 @@ describe('ErrorPluginItem', () => {
|
||||
it('should show "Install from Marketplace" button for marketplace plugins', () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'org/my-plugin' })}
|
||||
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/my-plugin' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -169,7 +170,7 @@ describe('ErrorPluginItem', () => {
|
||||
it('should show "Install from GitHub" button for github plugins', () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'https://github.com/user/repo' })}
|
||||
plugin={createPlugin({ source: PluginSource.github, plugin_id: 'https://github.com/user/repo' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -182,7 +183,7 @@ describe('ErrorPluginItem', () => {
|
||||
it('should not show action button for unknown source plugins', () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'local-only-plugin' })}
|
||||
plugin={createPlugin({ source: PluginSource.local, plugin_id: 'local-only-plugin' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -191,6 +192,20 @@ describe('ErrorPluginItem', () => {
|
||||
|
||||
expect(screen.queryByText(/plugin\.task\.installFrom/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use source instead of plugin_id heuristics when deciding button text', () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ source: PluginSource.github, plugin_id: 'org/my-plugin' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/plugin\.task\.installFromGithub/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/plugin\.task\.installFromMarketplace/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
@ -218,7 +233,7 @@ describe('ErrorPluginItem', () => {
|
||||
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'org/my-plugin' })}
|
||||
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/my-plugin' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -240,7 +255,7 @@ describe('ErrorPluginItem', () => {
|
||||
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'org/my-plugin' })}
|
||||
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/my-plugin' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -263,7 +278,7 @@ describe('ErrorPluginItem', () => {
|
||||
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'org/my-plugin' })}
|
||||
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/my-plugin' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -282,7 +297,7 @@ describe('ErrorPluginItem', () => {
|
||||
it('should not fetch when plugin_id has fewer than 2 parts', async () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'single-part' })}
|
||||
plugin={createPlugin({ source: PluginSource.local, plugin_id: 'single-part' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -296,10 +311,10 @@ describe('ErrorPluginItem', () => {
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should detect github source with github in URL', () => {
|
||||
it('should render github action when source is github even if plugin_id looks like a URL', () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'http://github.com/user/repo' })}
|
||||
plugin={createPlugin({ source: PluginSource.github, plugin_id: 'http://github.com/user/repo' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
@ -309,15 +324,16 @@ describe('ErrorPluginItem', () => {
|
||||
expect(screen.getByText(/plugin\.task\.installFromGithub/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close install modal when onSuccess is called', async () => {
|
||||
it('should close install modal and clear the error item when onSuccess is called', async () => {
|
||||
mockFetch.mockResolvedValueOnce(createMarketplaceResponse('org/p:1.0.0', '1.0.0'))
|
||||
const onClear = vi.fn()
|
||||
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'org/p' })}
|
||||
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/p' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
onClear={onClear}
|
||||
/>,
|
||||
)
|
||||
|
||||
@ -330,19 +346,35 @@ describe('ErrorPluginItem', () => {
|
||||
fireEvent.click(screen.getByText('Success'))
|
||||
|
||||
expect(screen.queryByTestId('install-modal')).not.toBeInTheDocument()
|
||||
expect(onClear).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should detect github source when id contains github keyword', () => {
|
||||
it('should show unknown action state for local source even if id contains github keyword', () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ plugin_id: 'my-github-plugin' })}
|
||||
plugin={createPlugin({ source: PluginSource.local, plugin_id: 'my-github-plugin' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/plugin\.task\.installFromGithub/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/plugin\.task\.installFromGithub/)).not.toBeInTheDocument()
|
||||
expect(screen.getByText(/plugin\.task\.errorMsg\.unknown/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show unknown error message for debugging source plugins', () => {
|
||||
render(
|
||||
<ErrorPluginItem
|
||||
plugin={createPlugin({ source: PluginSource.debugging, plugin_id: 'remote-plugin' })}
|
||||
getIconUrl={mockGetIconUrl}
|
||||
language="en_US"
|
||||
onClear={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/plugin\.task\.errorMsg\.unknown/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/plugin\.task\.installFrom/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { PluginStatus } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { TaskStatus } from '@/app/components/plugins/types'
|
||||
import { PluginSource, TaskStatus } from '@/app/components/plugins/types'
|
||||
import PluginItem from '../plugin-item'
|
||||
|
||||
vi.mock('@/app/components/plugins/card/base/card-icon', () => ({
|
||||
@ -14,6 +14,7 @@ const mockGetIconUrl = vi.fn((icon: string) => `https://example.com/icons/${icon
|
||||
const createPlugin = (overrides: Partial<PluginStatus> = {}): PluginStatus => ({
|
||||
plugin_unique_identifier: 'org/plugin:1.0.0',
|
||||
plugin_id: 'org/plugin',
|
||||
source: PluginSource.marketplace,
|
||||
status: TaskStatus.running,
|
||||
message: '',
|
||||
icon: 'icon.png',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { PluginStatus } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { TaskStatus } from '@/app/components/plugins/types'
|
||||
import { PluginSource, TaskStatus } from '@/app/components/plugins/types'
|
||||
import PluginSection from '../plugin-section'
|
||||
|
||||
vi.mock('@/app/components/plugins/card/base/card-icon', () => ({
|
||||
@ -14,6 +14,7 @@ const mockGetIconUrl = vi.fn((icon: string) => `https://icons/${icon}`)
|
||||
const createPlugin = (id: string, name: string, message = ''): PluginStatus => ({
|
||||
plugin_unique_identifier: id,
|
||||
plugin_id: `org/${name.toLowerCase()}`,
|
||||
source: PluginSource.marketplace,
|
||||
status: TaskStatus.running,
|
||||
message,
|
||||
icon: `${name.toLowerCase()}.png`,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { PluginStatus } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { TaskStatus } from '@/app/components/plugins/types'
|
||||
import { PluginSource, TaskStatus } from '@/app/components/plugins/types'
|
||||
import PluginTaskList from '../plugin-task-list'
|
||||
|
||||
vi.mock('@/app/components/plugins/card/base/card-icon', () => ({
|
||||
@ -26,6 +26,7 @@ const mockGetIconUrl = vi.fn((icon: string) => `https://icons/${icon}`)
|
||||
const createPlugin = (id: string, name: string, overrides: Partial<PluginStatus> = {}): PluginStatus => ({
|
||||
plugin_unique_identifier: id,
|
||||
plugin_id: `org/${name.toLowerCase()}`,
|
||||
source: PluginSource.marketplace,
|
||||
status: TaskStatus.running,
|
||||
message: '',
|
||||
icon: `${name.toLowerCase()}.png`,
|
||||
|
||||
@ -5,19 +5,10 @@ import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import { PluginSource } from '@/app/components/plugins/types'
|
||||
import { fetchPluginInfoFromMarketPlace } from '@/service/plugins'
|
||||
import PluginItem from './plugin-item'
|
||||
|
||||
type PluginSource = 'marketplace' | 'github' | 'unknown'
|
||||
|
||||
function getPluginSource(pluginId: string): PluginSource {
|
||||
if (pluginId.includes('/') && !pluginId.startsWith('http'))
|
||||
return 'marketplace'
|
||||
if (pluginId.startsWith('http') || pluginId.includes('github'))
|
||||
return 'github'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
type ErrorPluginItemProps = {
|
||||
plugin: PluginStatus
|
||||
getIconUrl: (icon: string) => string
|
||||
@ -27,7 +18,7 @@ type ErrorPluginItemProps = {
|
||||
|
||||
const ErrorPluginItem: FC<ErrorPluginItemProps> = ({ plugin, getIconUrl, language, onClear }) => {
|
||||
const { t } = useTranslation()
|
||||
const source = getPluginSource(plugin.plugin_id)
|
||||
const source = plugin.source
|
||||
const [showInstallModal, setShowInstallModal] = useState(false)
|
||||
const [installPayload, setInstallPayload] = useState<{ uniqueIdentifier: string, manifest: Plugin } | null>(null)
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
@ -75,16 +66,16 @@ const ErrorPluginItem: FC<ErrorPluginItemProps> = ({ plugin, getIconUrl, languag
|
||||
}
|
||||
}, [plugin.plugin_id, plugin.labels, plugin.icon])
|
||||
|
||||
const errorMsgKey = {
|
||||
marketplace: 'task.errorMsg.marketplace',
|
||||
github: 'task.errorMsg.github',
|
||||
unknown: 'task.errorMsg.unknown',
|
||||
}[source] as 'task.errorMsg.marketplace'
|
||||
const errorMsgKey: 'task.errorMsg.marketplace' | 'task.errorMsg.github' | 'task.errorMsg.unknown' = source === PluginSource.marketplace
|
||||
? 'task.errorMsg.marketplace'
|
||||
: source === PluginSource.github
|
||||
? 'task.errorMsg.github'
|
||||
: 'task.errorMsg.unknown'
|
||||
|
||||
const errorMsg = t(errorMsgKey, { ns: 'plugin' })
|
||||
|
||||
const renderAction = () => {
|
||||
if (source === 'marketplace') {
|
||||
if (source === PluginSource.marketplace) {
|
||||
return (
|
||||
<div className="pt-1">
|
||||
<Button variant="secondary" size="small" loading={isFetching} onClick={handleInstallFromMarketplace}>
|
||||
@ -93,7 +84,7 @@ const ErrorPluginItem: FC<ErrorPluginItemProps> = ({ plugin, getIconUrl, languag
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (source === 'github') {
|
||||
if (source === PluginSource.github) {
|
||||
return (
|
||||
<div className="pt-1">
|
||||
<Button variant="secondary" size="small">
|
||||
@ -130,7 +121,10 @@ const ErrorPluginItem: FC<ErrorPluginItemProps> = ({ plugin, getIconUrl, languag
|
||||
uniqueIdentifier={installPayload.uniqueIdentifier}
|
||||
manifest={installPayload.manifest}
|
||||
onClose={() => setShowInstallModal(false)}
|
||||
onSuccess={() => setShowInstallModal(false)}
|
||||
onSuccess={() => {
|
||||
setShowInstallModal(false)
|
||||
onClear()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -432,6 +432,7 @@ export enum TaskStatus {
|
||||
export type PluginStatus = {
|
||||
plugin_unique_identifier: string
|
||||
plugin_id: string
|
||||
source: PluginSource
|
||||
status: TaskStatus
|
||||
message: string
|
||||
icon: string
|
||||
|
||||
Reference in New Issue
Block a user